@genspectrum/dashboard-components 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/custom-elements.json +1013 -931
- package/dist/dashboard-components.js +350 -171
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +48 -57
- package/dist/style.css +74 -23
- package/package.json +2 -2
- package/src/preact/aggregatedData/aggregate.tsx +28 -28
- package/src/preact/components/error-boundary.stories.tsx +62 -0
- package/src/preact/components/error-boundary.tsx +31 -0
- package/src/preact/components/error-display.stories.tsx +24 -3
- package/src/preact/components/error-display.tsx +14 -1
- package/src/preact/components/loading-display.stories.tsx +6 -6
- package/src/preact/components/loading-display.tsx +1 -1
- package/src/preact/components/no-data-display.tsx +5 -1
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +17 -0
- package/src/preact/dateRangeSelector/date-range-selector.tsx +33 -5
- package/src/preact/locationFilter/location-filter.stories.tsx +23 -6
- package/src/preact/locationFilter/location-filter.tsx +29 -18
- package/src/preact/mutationComparison/mutation-comparison.tsx +29 -25
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +17 -2
- package/src/preact/mutationFilter/mutation-filter.tsx +25 -7
- package/src/preact/mutations/mutations.tsx +23 -23
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +44 -28
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +43 -31
- package/src/preact/textInput/text-input.tsx +26 -3
- package/src/web-components/app.stories.ts +1 -2
- package/src/web-components/app.ts +4 -2
- package/src/web-components/index.ts +1 -1
- package/src/web-components/input/{date-range-selector-component.stories.ts → gs-date-range-selector.stories.ts} +19 -2
- package/src/web-components/input/{date-range-selector-component.tsx → gs-date-range-selector.tsx} +12 -0
- package/src/web-components/input/{location-filter-component.stories.ts → gs-location-filter.stories.ts} +29 -4
- package/src/web-components/input/{location-filter-component.tsx → gs-location-filter.tsx} +12 -1
- package/src/web-components/input/{mutation-filter-component.stories.ts → gs-mutation-filter.stories.ts} +20 -4
- package/src/web-components/input/{mutation-filter-component.tsx → gs-mutation-filter.tsx} +36 -5
- package/src/web-components/input/{text-input-component.stories.ts → gs-text-input.stories.ts} +31 -3
- package/src/web-components/input/{text-input-component.tsx → gs-text-input.tsx} +12 -0
- package/src/web-components/input/index.ts +4 -4
- package/src/web-components/visualization/data_visualization_statistical_analysis.mdx +26 -0
- package/src/web-components/{display/aggregate-component.stories.ts → visualization/gs-aggregate.stories.ts} +5 -6
- package/src/web-components/{display/aggregate-component.tsx → visualization/gs-aggregate.tsx} +1 -1
- package/src/web-components/{display/mutation-comparison-component.stories.ts → visualization/gs-mutation-comparison.stories.ts} +8 -9
- package/src/web-components/{display/mutation-comparison-component.tsx → visualization/gs-mutation-comparison.tsx} +1 -1
- package/src/web-components/{display/mutations-component.stories.ts → visualization/gs-mutations.stories.ts} +6 -7
- package/src/web-components/{display/mutations-component.tsx → visualization/gs-mutations.tsx} +2 -2
- package/src/web-components/{display/prevalence-over-time-component.stories.ts → visualization/gs-prevalence-over-time.stories.ts} +1 -2
- package/src/web-components/{display/prevalence-over-time-component.tsx → visualization/gs-prevalence-over-time.tsx} +3 -1
- package/src/web-components/{display/relative-growth-advantage-component.stories.ts → visualization/gs-relative-growth-advantage.stories.ts} +1 -2
- package/src/web-components/visualization/index.ts +5 -0
- package/src/web-components/display/index.ts +0 -5
- /package/src/web-components/{display/relative-growth-advantage-component.tsx → visualization/gs-relative-growth-advantage.tsx} +0 -0
|
@@ -119,31 +119,18 @@ export declare class DateRangeSelectorComponent extends PreactLitAdapter {
|
|
|
119
119
|
* If the value is invalid, the component will default to `'last6Months'`.
|
|
120
120
|
*/
|
|
121
121
|
initialValue: 'custom' | 'allTimes' | 'last2Weeks' | 'lastMonth' | 'last2Months' | 'last3Months' | 'last6Months' | string | undefined;
|
|
122
|
+
/**
|
|
123
|
+
* The width of the component.
|
|
124
|
+
*
|
|
125
|
+
* If not set, the component will take the full width of its container.
|
|
126
|
+
*
|
|
127
|
+
* The width should be a string with a unit in css style, e.g. '100%', '500px' or '50vw'.
|
|
128
|
+
* If the unit is %, the size will be relative to the container of the component.
|
|
129
|
+
*/
|
|
130
|
+
width: string | undefined;
|
|
122
131
|
render(): JSX_2.Element;
|
|
123
132
|
}
|
|
124
133
|
|
|
125
|
-
declare class Deletion implements Mutation {
|
|
126
|
-
readonly segment: string | undefined;
|
|
127
|
-
readonly valueAtReference: string | undefined;
|
|
128
|
-
readonly position: number;
|
|
129
|
-
readonly code: string;
|
|
130
|
-
constructor(segment: string | undefined, valueAtReference: string | undefined, position: number);
|
|
131
|
-
equals(other: Mutation): boolean;
|
|
132
|
-
toString(): string;
|
|
133
|
-
static parse(mutationStr: string): Deletion | null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
declare class Insertion implements Mutation {
|
|
137
|
-
readonly segment: string | undefined;
|
|
138
|
-
readonly position: number;
|
|
139
|
-
readonly insertedSymbols: string;
|
|
140
|
-
readonly code: string;
|
|
141
|
-
constructor(segment: string | undefined, position: number, insertedSymbols: string);
|
|
142
|
-
equals(other: Mutation): boolean;
|
|
143
|
-
toString(): string;
|
|
144
|
-
static parse(mutationStr: string): Insertion | null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
134
|
declare type LapisFilter = Record<string, string | number | null | boolean>;
|
|
148
135
|
|
|
149
136
|
/**
|
|
@@ -186,17 +173,18 @@ export declare class LocationFilterComponent extends PreactLitAdapter {
|
|
|
186
173
|
* (e.g., `fields = ['continent', 'country', 'city']`).
|
|
187
174
|
*/
|
|
188
175
|
fields: string[];
|
|
176
|
+
/**
|
|
177
|
+
* The width of the component.
|
|
178
|
+
*
|
|
179
|
+
* If not set, the component will take the full width of its container.
|
|
180
|
+
*
|
|
181
|
+
* The width should be a string with a unit in css style, e.g. '100%', '500px' or '50vw'.
|
|
182
|
+
* If the unit is %, the size will be relative to the container of the component.
|
|
183
|
+
*/
|
|
184
|
+
width: string | undefined;
|
|
189
185
|
render(): JSX_2.Element;
|
|
190
186
|
}
|
|
191
187
|
|
|
192
|
-
declare interface Mutation {
|
|
193
|
-
readonly segment: string | undefined;
|
|
194
|
-
readonly position: number;
|
|
195
|
-
readonly code: string;
|
|
196
|
-
equals(other: Mutation): boolean;
|
|
197
|
-
toString(): string;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
188
|
/**
|
|
201
189
|
* This component allows to compare mutations between different variants.
|
|
202
190
|
* A variant is defined by its LAPIS filter.
|
|
@@ -314,7 +302,24 @@ export declare class MutationFilterComponent extends PreactLitAdapter {
|
|
|
314
302
|
* - an array of strings of valid mutations.
|
|
315
303
|
* - an object with the keys `nucleotideMutations`, `aminoAcidMutations`, `nucleotideInsertions` and `aminoAcidInsertions` and corresponding string arrays.
|
|
316
304
|
*/
|
|
317
|
-
initialValue:
|
|
305
|
+
initialValue: {
|
|
306
|
+
nucleotideMutations: string[];
|
|
307
|
+
aminoAcidMutations: string[];
|
|
308
|
+
nucleotideInsertions: string[];
|
|
309
|
+
aminoAcidInsertions: string[];
|
|
310
|
+
} | string[] | undefined;
|
|
311
|
+
/**
|
|
312
|
+
* The size of the component.
|
|
313
|
+
*
|
|
314
|
+
* If not set, the component will take the full width of its container with height 700px.
|
|
315
|
+
*
|
|
316
|
+
* The width and height should be a string with a unit in css style, e.g. '100%', '500px' or '50vh'.
|
|
317
|
+
* If the unit is %, the size will be relative to the container of the component.
|
|
318
|
+
*/
|
|
319
|
+
size: {
|
|
320
|
+
width?: string;
|
|
321
|
+
height?: string;
|
|
322
|
+
} | undefined;
|
|
318
323
|
render(): JSX_2.Element;
|
|
319
324
|
}
|
|
320
325
|
|
|
@@ -588,29 +593,6 @@ export declare class RelativeGrowthAdvantageComponent extends PreactLitAdapter {
|
|
|
588
593
|
render(): JSX_2.Element;
|
|
589
594
|
}
|
|
590
595
|
|
|
591
|
-
declare type SelectedFilters = {
|
|
592
|
-
nucleotideMutations: (Substitution | Deletion)[];
|
|
593
|
-
aminoAcidMutations: (Substitution | Deletion)[];
|
|
594
|
-
nucleotideInsertions: Insertion[];
|
|
595
|
-
aminoAcidInsertions: Insertion[];
|
|
596
|
-
};
|
|
597
|
-
|
|
598
|
-
declare type SelectedMutationFilterStrings = {
|
|
599
|
-
[Key in keyof SelectedFilters]: string[];
|
|
600
|
-
};
|
|
601
|
-
|
|
602
|
-
declare class Substitution implements Mutation {
|
|
603
|
-
readonly segment: string | undefined;
|
|
604
|
-
readonly valueAtReference: string | undefined;
|
|
605
|
-
readonly substitutionValue: string | undefined;
|
|
606
|
-
readonly position: number;
|
|
607
|
-
readonly code: string;
|
|
608
|
-
constructor(segment: string | undefined, valueAtReference: string | undefined, substitutionValue: string | undefined, position: number);
|
|
609
|
-
equals(other: Mutation): boolean;
|
|
610
|
-
toString(): string;
|
|
611
|
-
static parse(mutationStr: string): Substitution | null;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
596
|
/**
|
|
615
597
|
*
|
|
616
598
|
* ## Context
|
|
@@ -640,6 +622,15 @@ export declare class TextInputComponent extends PreactLitAdapter {
|
|
|
640
622
|
* The placeholder text to display in the input field.
|
|
641
623
|
*/
|
|
642
624
|
placeholderText: string | undefined;
|
|
625
|
+
/**
|
|
626
|
+
* The width of the component.
|
|
627
|
+
*
|
|
628
|
+
* If not set, the component will take the full width of its container.
|
|
629
|
+
*
|
|
630
|
+
* The width should be a string with a unit in css style, e.g. '100%', '500px' or '50vw'.
|
|
631
|
+
* If the unit is %, the size will be relative to the container of the component.
|
|
632
|
+
*/
|
|
633
|
+
width: string | undefined;
|
|
643
634
|
render(): JSX_2.Element;
|
|
644
635
|
}
|
|
645
636
|
|
|
@@ -678,14 +669,14 @@ declare global {
|
|
|
678
669
|
|
|
679
670
|
declare global {
|
|
680
671
|
interface HTMLElementTagNameMap {
|
|
681
|
-
'gs-
|
|
672
|
+
'gs-aggregate-component': AggregateComponent;
|
|
682
673
|
}
|
|
683
674
|
}
|
|
684
675
|
|
|
685
676
|
|
|
686
677
|
declare global {
|
|
687
678
|
interface HTMLElementTagNameMap {
|
|
688
|
-
'gs-
|
|
679
|
+
'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
|
|
689
680
|
}
|
|
690
681
|
}
|
|
691
682
|
|
|
@@ -725,7 +716,7 @@ declare global {
|
|
|
725
716
|
|
|
726
717
|
declare global {
|
|
727
718
|
interface HTMLElementTagNameMap {
|
|
728
|
-
'gs-mutation-filter':
|
|
719
|
+
'gs-mutation-filter': MutationFilterComponent;
|
|
729
720
|
}
|
|
730
721
|
interface HTMLElementEventMap {
|
|
731
722
|
'gs-mutation-filter-changed': CustomEvent<SelectedMutationFilterStrings>;
|
package/dist/style.css
CHANGED
|
@@ -1008,6 +1008,35 @@ html {
|
|
|
1008
1008
|
max-width: 1536px;
|
|
1009
1009
|
}
|
|
1010
1010
|
}
|
|
1011
|
+
.alert {
|
|
1012
|
+
display: grid;
|
|
1013
|
+
width: 100%;
|
|
1014
|
+
grid-auto-flow: row;
|
|
1015
|
+
align-content: flex-start;
|
|
1016
|
+
align-items: center;
|
|
1017
|
+
justify-items: center;
|
|
1018
|
+
gap: 1rem;
|
|
1019
|
+
text-align: center;
|
|
1020
|
+
border-radius: var(--rounded-box, 1rem);
|
|
1021
|
+
border-width: 1px;
|
|
1022
|
+
--tw-border-opacity: 1;
|
|
1023
|
+
border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity)));
|
|
1024
|
+
padding: 1rem;
|
|
1025
|
+
--tw-text-opacity: 1;
|
|
1026
|
+
color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity)));
|
|
1027
|
+
--alert-bg: var(--fallback-b2,oklch(var(--b2)/1));
|
|
1028
|
+
--alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1));
|
|
1029
|
+
background-color: var(--alert-bg);
|
|
1030
|
+
}
|
|
1031
|
+
@media (min-width: 640px) {
|
|
1032
|
+
|
|
1033
|
+
.alert {
|
|
1034
|
+
grid-auto-flow: column;
|
|
1035
|
+
grid-template-columns: auto minmax(auto,1fr);
|
|
1036
|
+
justify-items: start;
|
|
1037
|
+
text-align: start;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1011
1040
|
.avatar.placeholder > div {
|
|
1012
1041
|
display: flex;
|
|
1013
1042
|
align-items: center;
|
|
@@ -1516,29 +1545,6 @@ html {
|
|
|
1516
1545
|
.select[multiple] {
|
|
1517
1546
|
height: auto;
|
|
1518
1547
|
}
|
|
1519
|
-
.stack {
|
|
1520
|
-
display: inline-grid;
|
|
1521
|
-
place-items: center;
|
|
1522
|
-
align-items: flex-end;
|
|
1523
|
-
}
|
|
1524
|
-
.stack > * {
|
|
1525
|
-
grid-column-start: 1;
|
|
1526
|
-
grid-row-start: 1;
|
|
1527
|
-
transform: translateY(10%) scale(0.9);
|
|
1528
|
-
z-index: 1;
|
|
1529
|
-
width: 100%;
|
|
1530
|
-
opacity: 0.6;
|
|
1531
|
-
}
|
|
1532
|
-
.stack > *:nth-child(2) {
|
|
1533
|
-
transform: translateY(5%) scale(0.95);
|
|
1534
|
-
z-index: 2;
|
|
1535
|
-
opacity: 0.8;
|
|
1536
|
-
}
|
|
1537
|
-
.stack > *:nth-child(1) {
|
|
1538
|
-
transform: translateY(0) scale(1);
|
|
1539
|
-
z-index: 3;
|
|
1540
|
-
opacity: 1;
|
|
1541
|
-
}
|
|
1542
1548
|
.steps {
|
|
1543
1549
|
display: inline-grid;
|
|
1544
1550
|
grid-auto-flow: column;
|
|
@@ -1638,6 +1644,13 @@ input.tab:checked + .tab-content,
|
|
|
1638
1644
|
--tw-bg-opacity: 1;
|
|
1639
1645
|
background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
|
|
1640
1646
|
}
|
|
1647
|
+
.alert-error {
|
|
1648
|
+
border-color: var(--fallback-er,oklch(var(--er)/0.2));
|
|
1649
|
+
--tw-text-opacity: 1;
|
|
1650
|
+
color: var(--fallback-erc,oklch(var(--erc)/var(--tw-text-opacity)));
|
|
1651
|
+
--alert-bg: var(--fallback-er,oklch(var(--er)/1));
|
|
1652
|
+
--alert-bg-mix: var(--fallback-b1,oklch(var(--b1)/1));
|
|
1653
|
+
}
|
|
1641
1654
|
.btm-nav > *.disabled,
|
|
1642
1655
|
.btm-nav > *[disabled] {
|
|
1643
1656
|
pointer-events: none;
|
|
@@ -2186,6 +2199,30 @@ input.tab:checked + .tab-content,
|
|
|
2186
2199
|
background-position: calc(0% + 12px) calc(1px + 50%),
|
|
2187
2200
|
calc(0% + 16px) calc(1px + 50%);
|
|
2188
2201
|
}
|
|
2202
|
+
.skeleton {
|
|
2203
|
+
border-radius: var(--rounded-box, 1rem);
|
|
2204
|
+
--tw-bg-opacity: 1;
|
|
2205
|
+
background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity)));
|
|
2206
|
+
will-change: background-position;
|
|
2207
|
+
animation: skeleton 1.8s ease-in-out infinite;
|
|
2208
|
+
background-image: linear-gradient(
|
|
2209
|
+
105deg,
|
|
2210
|
+
transparent 0%,
|
|
2211
|
+
transparent 40%,
|
|
2212
|
+
var(--fallback-b1,oklch(var(--b1)/1)) 50%,
|
|
2213
|
+
transparent 60%,
|
|
2214
|
+
transparent 100%
|
|
2215
|
+
);
|
|
2216
|
+
background-size: 200% auto;
|
|
2217
|
+
background-repeat: no-repeat;
|
|
2218
|
+
background-position-x: -50%;
|
|
2219
|
+
}
|
|
2220
|
+
@media (prefers-reduced-motion) {
|
|
2221
|
+
|
|
2222
|
+
.skeleton {
|
|
2223
|
+
animation-duration: 15s;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2189
2226
|
@keyframes skeleton {
|
|
2190
2227
|
|
|
2191
2228
|
from {
|
|
@@ -2811,6 +2848,10 @@ input.tab:checked + .tab-content,
|
|
|
2811
2848
|
.min-w-0 {
|
|
2812
2849
|
min-width: 0px;
|
|
2813
2850
|
}
|
|
2851
|
+
.min-w-max {
|
|
2852
|
+
min-width: -moz-max-content;
|
|
2853
|
+
min-width: max-content;
|
|
2854
|
+
}
|
|
2814
2855
|
.max-w-screen-lg {
|
|
2815
2856
|
max-width: 1024px;
|
|
2816
2857
|
}
|
|
@@ -2853,6 +2894,9 @@ input.tab:checked + .tab-content,
|
|
|
2853
2894
|
.overflow-auto {
|
|
2854
2895
|
overflow: auto;
|
|
2855
2896
|
}
|
|
2897
|
+
.overflow-scroll {
|
|
2898
|
+
overflow: scroll;
|
|
2899
|
+
}
|
|
2856
2900
|
.whitespace-nowrap {
|
|
2857
2901
|
white-space: nowrap;
|
|
2858
2902
|
}
|
|
@@ -2871,6 +2915,9 @@ input.tab:checked + .tab-content,
|
|
|
2871
2915
|
.rounded-lg {
|
|
2872
2916
|
border-radius: 0.5rem;
|
|
2873
2917
|
}
|
|
2918
|
+
.rounded-md {
|
|
2919
|
+
border-radius: 0.375rem;
|
|
2920
|
+
}
|
|
2874
2921
|
.rounded-none {
|
|
2875
2922
|
border-radius: 0px;
|
|
2876
2923
|
}
|
|
@@ -2996,6 +3043,10 @@ input.tab:checked + .tab-content,
|
|
|
2996
3043
|
--tw-text-opacity: 1;
|
|
2997
3044
|
color: rgb(75 85 99 / var(--tw-text-opacity));
|
|
2998
3045
|
}
|
|
3046
|
+
.text-red-700 {
|
|
3047
|
+
--tw-text-opacity: 1;
|
|
3048
|
+
color: rgb(185 28 28 / var(--tw-text-opacity));
|
|
3049
|
+
}
|
|
2999
3050
|
.underline {
|
|
3000
3051
|
text-decoration-line: underline;
|
|
3001
3052
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genspectrum/dashboard-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "GenSpectrum web components for building dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"lint:lit-analyzer": "lit-analyzer",
|
|
36
36
|
"generate-manifest": "npx custom-elements-manifest analyze --litelement --globs src/web-components/**",
|
|
37
37
|
"generate-manifest:watch": "npm run generate-manifest -- --watch",
|
|
38
|
-
"format": "prettier \"**/*.{
|
|
38
|
+
"format": "prettier \"**/*.{ts,tsx,json,md,mdx,mjs,cjs}\" --write",
|
|
39
39
|
"check-format": "prettier --check \"**/*.{ts,tsx,json,md,mdx,mjs,cjs}\"",
|
|
40
40
|
"check-types": "tsc --noEmit",
|
|
41
41
|
"check-dependencies": "depcheck",
|
|
@@ -6,6 +6,7 @@ import { type AggregateData, queryAggregateData } from '../../query/queryAggrega
|
|
|
6
6
|
import { type LapisFilter } from '../../types';
|
|
7
7
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
8
8
|
import { CsvDownloadButton } from '../components/csv-download-button';
|
|
9
|
+
import { ErrorBoundary } from '../components/error-boundary';
|
|
9
10
|
import { ErrorDisplay } from '../components/error-display';
|
|
10
11
|
import Headline from '../components/headline';
|
|
11
12
|
import Info from '../components/info';
|
|
@@ -17,21 +18,38 @@ import { useQuery } from '../useQuery';
|
|
|
17
18
|
|
|
18
19
|
export type View = 'table';
|
|
19
20
|
|
|
20
|
-
export
|
|
21
|
+
export type AggregateProps = {
|
|
22
|
+
size?: Size;
|
|
23
|
+
headline?: string;
|
|
24
|
+
} & AggregateInnerProps;
|
|
25
|
+
|
|
26
|
+
export interface AggregateInnerProps {
|
|
21
27
|
filter: LapisFilter;
|
|
22
28
|
fields: string[];
|
|
23
29
|
views: View[];
|
|
24
|
-
size?: Size;
|
|
25
|
-
headline?: string;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
export const Aggregate: FunctionComponent<AggregateProps> = ({
|
|
29
|
-
fields,
|
|
30
33
|
views,
|
|
31
|
-
filter,
|
|
32
34
|
size,
|
|
33
|
-
headline = '
|
|
35
|
+
headline = 'Mutations',
|
|
36
|
+
filter,
|
|
37
|
+
fields,
|
|
34
38
|
}) => {
|
|
39
|
+
const defaultSize = { height: '600px', width: '100%' };
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<ErrorBoundary size={size} defaultSize={defaultSize} headline={headline}>
|
|
43
|
+
<ResizeContainer size={size} defaultSize={defaultSize}>
|
|
44
|
+
<Headline heading={headline}>
|
|
45
|
+
<AggregateInner fields={fields} filter={filter} views={views} />
|
|
46
|
+
</Headline>
|
|
47
|
+
</ResizeContainer>
|
|
48
|
+
</ErrorBoundary>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const AggregateInner: FunctionComponent<AggregateInnerProps> = ({ fields, views, filter }) => {
|
|
35
53
|
const lapis = useContext(LapisUrlContext);
|
|
36
54
|
|
|
37
55
|
const { data, error, isLoading } = useQuery(async () => {
|
|
@@ -39,36 +57,18 @@ export const Aggregate: FunctionComponent<AggregateProps> = ({
|
|
|
39
57
|
}, [filter, fields, lapis]);
|
|
40
58
|
|
|
41
59
|
if (isLoading) {
|
|
42
|
-
return
|
|
43
|
-
<Headline heading={headline}>
|
|
44
|
-
<LoadingDisplay />
|
|
45
|
-
</Headline>
|
|
46
|
-
);
|
|
60
|
+
return <LoadingDisplay />;
|
|
47
61
|
}
|
|
48
62
|
|
|
49
63
|
if (error !== null) {
|
|
50
|
-
return
|
|
51
|
-
<Headline heading={headline}>
|
|
52
|
-
<ErrorDisplay error={error} />
|
|
53
|
-
</Headline>
|
|
54
|
-
);
|
|
64
|
+
return <ErrorDisplay error={error} />;
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
if (data === null) {
|
|
58
|
-
return
|
|
59
|
-
<Headline heading={headline}>
|
|
60
|
-
<NoDataDisplay />
|
|
61
|
-
</Headline>
|
|
62
|
-
);
|
|
68
|
+
return <NoDataDisplay />;
|
|
63
69
|
}
|
|
64
70
|
|
|
65
|
-
return
|
|
66
|
-
<ResizeContainer size={size} defaultSize={{ height: '700px', width: '100%' }}>
|
|
67
|
-
<Headline heading={headline}>
|
|
68
|
-
<AggregatedDataTabs data={data} views={views} fields={fields} />
|
|
69
|
-
</Headline>
|
|
70
|
-
</ResizeContainer>
|
|
71
|
-
);
|
|
71
|
+
return <AggregatedDataTabs data={data} views={views} fields={fields} />;
|
|
72
72
|
};
|
|
73
73
|
|
|
74
74
|
type AggregatedDataTabsProps = {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
+
import { expect, waitFor, within } from '@storybook/test';
|
|
3
|
+
|
|
4
|
+
import { ErrorBoundary } from './error-boundary';
|
|
5
|
+
|
|
6
|
+
const meta: Meta = {
|
|
7
|
+
title: 'Component/Error boundary',
|
|
8
|
+
component: ErrorBoundary,
|
|
9
|
+
parameters: { fetchMock: {} },
|
|
10
|
+
argTypes: {
|
|
11
|
+
size: { control: 'object' },
|
|
12
|
+
defaultSize: { control: 'object' },
|
|
13
|
+
headline: { control: 'text' },
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
|
|
19
|
+
export const ErrorBoundaryWithoutErrorStory: StoryObj = {
|
|
20
|
+
render: (args) => (
|
|
21
|
+
<ErrorBoundary size={args.size} defaultSize={args.defaultSize} headline={args.headline}>
|
|
22
|
+
<div>Some content</div>
|
|
23
|
+
</ErrorBoundary>
|
|
24
|
+
),
|
|
25
|
+
args: {
|
|
26
|
+
size: { height: '600px', width: '100%' },
|
|
27
|
+
defaultSize: { height: '600px', width: '100%' },
|
|
28
|
+
headline: 'Some headline',
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
play: async ({ canvasElement }) => {
|
|
32
|
+
const canvas = within(canvasElement);
|
|
33
|
+
const content = canvas.getByText('Some content', { exact: false });
|
|
34
|
+
await waitFor(() => expect(content).toBeInTheDocument());
|
|
35
|
+
await waitFor(() => expect(canvas.queryByText('Some headline')).not.toBeInTheDocument());
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const ErrorBoundaryWithErrorStory: StoryObj = {
|
|
40
|
+
render: (args) => (
|
|
41
|
+
<ErrorBoundary size={args.size} defaultSize={args.defaultSize} headline={args.headline}>
|
|
42
|
+
<ContentThatThrowsError />
|
|
43
|
+
</ErrorBoundary>
|
|
44
|
+
),
|
|
45
|
+
args: {
|
|
46
|
+
size: { height: '600px', width: '100%' },
|
|
47
|
+
defaultSize: { height: '600px', width: '100%' },
|
|
48
|
+
headline: 'Some headline',
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
play: async ({ canvasElement }) => {
|
|
52
|
+
const canvas = within(canvasElement);
|
|
53
|
+
const content = canvas.queryByText('Some content.', { exact: false });
|
|
54
|
+
await waitFor(() => expect(content).not.toBeInTheDocument());
|
|
55
|
+
await waitFor(() => expect(canvas.getByText('Some headline')).toBeInTheDocument());
|
|
56
|
+
await waitFor(() => expect(canvas.getByText('Error')).toBeInTheDocument());
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const ContentThatThrowsError = () => {
|
|
61
|
+
throw new Error('Some error');
|
|
62
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { FunctionComponent } from 'preact';
|
|
2
|
+
import { useErrorBoundary } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import { ErrorDisplay } from './error-display';
|
|
5
|
+
import { ResizeContainer, type Size } from './resize-container';
|
|
6
|
+
import Headline from '../components/headline';
|
|
7
|
+
|
|
8
|
+
export const ErrorBoundary: FunctionComponent<{ size?: Size; defaultSize: Size; headline?: string }> = ({
|
|
9
|
+
size,
|
|
10
|
+
defaultSize,
|
|
11
|
+
headline,
|
|
12
|
+
children,
|
|
13
|
+
}) => {
|
|
14
|
+
const [internalError] = useErrorBoundary();
|
|
15
|
+
|
|
16
|
+
if (internalError) {
|
|
17
|
+
console.error(internalError);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (internalError) {
|
|
21
|
+
return (
|
|
22
|
+
<ResizeContainer defaultSize={defaultSize} size={size}>
|
|
23
|
+
<Headline heading={headline}>
|
|
24
|
+
<ErrorDisplay error={internalError} />
|
|
25
|
+
</Headline>
|
|
26
|
+
</ResizeContainer>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return <>{children}</>;
|
|
31
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
2
|
import { expect, waitFor, within } from '@storybook/test';
|
|
3
3
|
|
|
4
|
-
import { ErrorDisplay } from './error-display';
|
|
4
|
+
import { ErrorDisplay, UserFacingError } from './error-display';
|
|
5
|
+
import { ResizeContainer } from './resize-container';
|
|
5
6
|
|
|
6
7
|
const meta: Meta = {
|
|
7
8
|
title: 'Component/Error',
|
|
@@ -12,11 +13,31 @@ const meta: Meta = {
|
|
|
12
13
|
export default meta;
|
|
13
14
|
|
|
14
15
|
export const ErrorStory: StoryObj = {
|
|
15
|
-
render: () =>
|
|
16
|
+
render: () => (
|
|
17
|
+
<ResizeContainer defaultSize={{ height: '600px', width: '100%' }}>
|
|
18
|
+
<ErrorDisplay error={new Error('some message')} />
|
|
19
|
+
</ResizeContainer>
|
|
20
|
+
),
|
|
16
21
|
|
|
17
22
|
play: async ({ canvasElement }) => {
|
|
18
23
|
const canvas = within(canvasElement);
|
|
19
|
-
const error = canvas.getByText('
|
|
24
|
+
const error = canvas.getByText('Oops! Something went wrong.', { exact: false });
|
|
20
25
|
await waitFor(() => expect(error).toBeInTheDocument());
|
|
26
|
+
await waitFor(() => expect(canvas.queryByText('some message')).not.toBeInTheDocument());
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const UserFacingErrorStory: StoryObj = {
|
|
31
|
+
render: () => (
|
|
32
|
+
<ResizeContainer defaultSize={{ height: '600px', width: '100%' }}>
|
|
33
|
+
<ErrorDisplay error={new UserFacingError('some message')} />
|
|
34
|
+
</ResizeContainer>
|
|
35
|
+
),
|
|
36
|
+
|
|
37
|
+
play: async ({ canvasElement }) => {
|
|
38
|
+
const canvas = within(canvasElement);
|
|
39
|
+
const error = canvas.getByText('Oops! Something went wrong.', { exact: false });
|
|
40
|
+
await waitFor(() => expect(error).toBeInTheDocument());
|
|
41
|
+
await waitFor(() => expect(canvas.getByText('some message')).toBeInTheDocument());
|
|
21
42
|
},
|
|
22
43
|
};
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
|
|
3
|
+
export class UserFacingError extends Error {
|
|
4
|
+
constructor(message: string) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'UserFacingError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) => {
|
|
4
|
-
return
|
|
11
|
+
return (
|
|
12
|
+
<div className='h-full w-full rounded-md border-2 border-gray-100 p-2 flex items-center justify-center flex-col'>
|
|
13
|
+
<div className='text-red-700 font-bold'>Error</div>
|
|
14
|
+
<div>Oops! Something went wrong.</div>
|
|
15
|
+
{error instanceof UserFacingError && <div className='text-sm text-gray-600'>{error.message}</div>}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
5
18
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
-
import { expect, waitFor, within } from '@storybook/test';
|
|
3
2
|
|
|
4
3
|
import { LoadingDisplay } from './loading-display';
|
|
4
|
+
import { ResizeContainer } from './resize-container';
|
|
5
5
|
|
|
6
6
|
const meta: Meta = {
|
|
7
7
|
title: 'Component/Loading',
|
|
@@ -12,9 +12,9 @@ const meta: Meta = {
|
|
|
12
12
|
export default meta;
|
|
13
13
|
|
|
14
14
|
export const LoadingStory: StoryObj = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
render: () => (
|
|
16
|
+
<ResizeContainer defaultSize={{ height: '600px', width: '100%' }}>
|
|
17
|
+
<LoadingDisplay />
|
|
18
|
+
</ResizeContainer>
|
|
19
|
+
),
|
|
20
20
|
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
|
|
3
3
|
export const NoDataDisplay: FunctionComponent = () => {
|
|
4
|
-
return
|
|
4
|
+
return (
|
|
5
|
+
<div className='h-full w-full rounded-md border-2 border-gray-100 p-2 flex items-center justify-center'>
|
|
6
|
+
<div>No data available.</div>
|
|
7
|
+
</div>
|
|
8
|
+
);
|
|
5
9
|
};
|
|
@@ -40,11 +40,27 @@ const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
|
40
40
|
'CustomDateRange',
|
|
41
41
|
],
|
|
42
42
|
},
|
|
43
|
+
customSelectOptions: {
|
|
44
|
+
control: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
earliestDate: {
|
|
49
|
+
control: {
|
|
50
|
+
type: 'text',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
width: {
|
|
54
|
+
control: {
|
|
55
|
+
type: 'text',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
43
58
|
},
|
|
44
59
|
args: {
|
|
45
60
|
customSelectOptions: [{ label: 'CustomDateRange', dateFrom: '2021-01-01', dateTo: '2021-12-31' }],
|
|
46
61
|
earliestDate: '1970-01-01',
|
|
47
62
|
initialValue: PRESET_VALUE_LAST_3_MONTHS,
|
|
63
|
+
width: '100%',
|
|
48
64
|
},
|
|
49
65
|
decorators: [withActions],
|
|
50
66
|
};
|
|
@@ -58,6 +74,7 @@ export const Primary: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
|
58
74
|
customSelectOptions={args.customSelectOptions}
|
|
59
75
|
earliestDate={args.earliestDate}
|
|
60
76
|
initialValue={args.initialValue}
|
|
77
|
+
width={args.width}
|
|
61
78
|
/>
|
|
62
79
|
</LapisUrlContext.Provider>
|
|
63
80
|
),
|