@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.
Files changed (50) hide show
  1. package/custom-elements.json +1013 -931
  2. package/dist/dashboard-components.js +350 -171
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +48 -57
  5. package/dist/style.css +74 -23
  6. package/package.json +2 -2
  7. package/src/preact/aggregatedData/aggregate.tsx +28 -28
  8. package/src/preact/components/error-boundary.stories.tsx +62 -0
  9. package/src/preact/components/error-boundary.tsx +31 -0
  10. package/src/preact/components/error-display.stories.tsx +24 -3
  11. package/src/preact/components/error-display.tsx +14 -1
  12. package/src/preact/components/loading-display.stories.tsx +6 -6
  13. package/src/preact/components/loading-display.tsx +1 -1
  14. package/src/preact/components/no-data-display.tsx +5 -1
  15. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +17 -0
  16. package/src/preact/dateRangeSelector/date-range-selector.tsx +33 -5
  17. package/src/preact/locationFilter/location-filter.stories.tsx +23 -6
  18. package/src/preact/locationFilter/location-filter.tsx +29 -18
  19. package/src/preact/mutationComparison/mutation-comparison.tsx +29 -25
  20. package/src/preact/mutationFilter/mutation-filter.stories.tsx +17 -2
  21. package/src/preact/mutationFilter/mutation-filter.tsx +25 -7
  22. package/src/preact/mutations/mutations.tsx +23 -23
  23. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +44 -28
  24. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +43 -31
  25. package/src/preact/textInput/text-input.tsx +26 -3
  26. package/src/web-components/app.stories.ts +1 -2
  27. package/src/web-components/app.ts +4 -2
  28. package/src/web-components/index.ts +1 -1
  29. package/src/web-components/input/{date-range-selector-component.stories.ts → gs-date-range-selector.stories.ts} +19 -2
  30. package/src/web-components/input/{date-range-selector-component.tsx → gs-date-range-selector.tsx} +12 -0
  31. package/src/web-components/input/{location-filter-component.stories.ts → gs-location-filter.stories.ts} +29 -4
  32. package/src/web-components/input/{location-filter-component.tsx → gs-location-filter.tsx} +12 -1
  33. package/src/web-components/input/{mutation-filter-component.stories.ts → gs-mutation-filter.stories.ts} +20 -4
  34. package/src/web-components/input/{mutation-filter-component.tsx → gs-mutation-filter.tsx} +36 -5
  35. package/src/web-components/input/{text-input-component.stories.ts → gs-text-input.stories.ts} +31 -3
  36. package/src/web-components/input/{text-input-component.tsx → gs-text-input.tsx} +12 -0
  37. package/src/web-components/input/index.ts +4 -4
  38. package/src/web-components/visualization/data_visualization_statistical_analysis.mdx +26 -0
  39. package/src/web-components/{display/aggregate-component.stories.ts → visualization/gs-aggregate.stories.ts} +5 -6
  40. package/src/web-components/{display/aggregate-component.tsx → visualization/gs-aggregate.tsx} +1 -1
  41. package/src/web-components/{display/mutation-comparison-component.stories.ts → visualization/gs-mutation-comparison.stories.ts} +8 -9
  42. package/src/web-components/{display/mutation-comparison-component.tsx → visualization/gs-mutation-comparison.tsx} +1 -1
  43. package/src/web-components/{display/mutations-component.stories.ts → visualization/gs-mutations.stories.ts} +6 -7
  44. package/src/web-components/{display/mutations-component.tsx → visualization/gs-mutations.tsx} +2 -2
  45. package/src/web-components/{display/prevalence-over-time-component.stories.ts → visualization/gs-prevalence-over-time.stories.ts} +1 -2
  46. package/src/web-components/{display/prevalence-over-time-component.tsx → visualization/gs-prevalence-over-time.tsx} +3 -1
  47. package/src/web-components/{display/relative-growth-advantage-component.stories.ts → visualization/gs-relative-growth-advantage.stories.ts} +1 -2
  48. package/src/web-components/visualization/index.ts +5 -0
  49. package/src/web-components/display/index.ts +0 -5
  50. /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: SelectedMutationFilterStrings | string[] | undefined;
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-relative-growth-advantage': RelativeGrowthAdvantageComponent;
672
+ 'gs-aggregate-component': AggregateComponent;
682
673
  }
683
674
  }
684
675
 
685
676
 
686
677
  declare global {
687
678
  interface HTMLElementTagNameMap {
688
- 'gs-aggregate-component': AggregateComponent;
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': TextInputComponent;
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.1.5",
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 \"**/*.{cjs,html,js,json,md,ts,tsx}\" --write",
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 interface AggregateProps {
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 = 'Aggregate',
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: () => <ErrorDisplay error={new Error('some message')} />,
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('Error: ', { exact: false });
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 <div>Error: {error.message}</div>;
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
- play: async ({ canvasElement }) => {
16
- const canvas = within(canvasElement);
17
- const loading = canvas.getByText('Loading...');
18
- await waitFor(() => expect(loading).toBeInTheDocument());
19
- },
15
+ render: () => (
16
+ <ResizeContainer defaultSize={{ height: '600px', width: '100%' }}>
17
+ <LoadingDisplay />
18
+ </ResizeContainer>
19
+ ),
20
20
  };
@@ -1,5 +1,5 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
 
3
3
  export const LoadingDisplay: FunctionComponent = () => {
4
- return <div>Loading...</div>;
4
+ return <div aria-label={'Loading'} className='h-full w-full skeleton' />;
5
5
  };
@@ -1,5 +1,9 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
 
3
3
  export const NoDataDisplay: FunctionComponent = () => {
4
- return <div>No data available.</div>;
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
  ),