@genspectrum/dashboard-components 0.2.0 → 0.3.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 (58) hide show
  1. package/custom-elements.json +328 -177
  2. package/dist/dashboard-components.js +376 -129
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +156 -110
  5. package/dist/style.css +179 -33
  6. package/package.json +1 -2
  7. package/src/constants.ts +1 -1
  8. package/src/lapisApi/lapisApi.ts +46 -2
  9. package/src/lapisApi/lapisTypes.ts +14 -0
  10. package/src/preact/aggregatedData/aggregate.stories.tsx +4 -2
  11. package/src/preact/aggregatedData/aggregate.tsx +8 -6
  12. package/src/preact/components/error-boundary.stories.tsx +6 -14
  13. package/src/preact/components/error-boundary.tsx +2 -11
  14. package/src/preact/components/error-display.stories.tsx +12 -5
  15. package/src/preact/components/error-display.tsx +37 -3
  16. package/src/preact/components/loading-display.stories.tsx +1 -1
  17. package/src/preact/components/resize-container.tsx +5 -14
  18. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +2 -0
  19. package/src/preact/dateRangeSelector/date-range-selector.tsx +11 -8
  20. package/src/preact/locationFilter/fetchAutocompletionList.ts +15 -1
  21. package/src/preact/locationFilter/location-filter.tsx +4 -5
  22. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +6 -3
  23. package/src/preact/mutationComparison/mutation-comparison.tsx +10 -13
  24. package/src/preact/mutationComparison/queryMutationData.ts +2 -3
  25. package/src/preact/mutationFilter/mutation-filter.stories.tsx +8 -8
  26. package/src/preact/mutationFilter/mutation-filter.tsx +7 -6
  27. package/src/preact/mutations/mutations.stories.tsx +6 -3
  28. package/src/preact/mutations/mutations.tsx +8 -6
  29. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +14 -7
  30. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +10 -8
  31. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +6 -3
  32. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +9 -7
  33. package/src/preact/textInput/text-input.stories.tsx +26 -0
  34. package/src/preact/textInput/text-input.tsx +4 -5
  35. package/src/query/queryPrevalenceOverTime.ts +4 -10
  36. package/src/types.ts +4 -1
  37. package/src/web-components/ResizeContainer.mdx +13 -0
  38. package/src/web-components/app.ts +3 -1
  39. package/src/web-components/input/gs-date-range-selector.stories.ts +10 -2
  40. package/src/web-components/input/gs-date-range-selector.tsx +26 -16
  41. package/src/web-components/input/gs-location-filter.stories.ts +5 -3
  42. package/src/web-components/input/gs-location-filter.tsx +5 -6
  43. package/src/web-components/input/gs-mutation-filter.stories.ts +11 -8
  44. package/src/web-components/input/gs-mutation-filter.tsx +38 -26
  45. package/src/web-components/input/gs-text-input.stories.ts +3 -3
  46. package/src/web-components/input/gs-text-input.tsx +10 -10
  47. package/src/web-components/input/introduction.mdx +11 -0
  48. package/src/web-components/introduction.mdx +15 -0
  49. package/src/web-components/visualization/gs-aggregate.stories.ts +19 -6
  50. package/src/web-components/visualization/gs-aggregate.tsx +31 -15
  51. package/src/web-components/visualization/gs-mutation-comparison.stories.ts +13 -7
  52. package/src/web-components/visualization/gs-mutation-comparison.tsx +26 -17
  53. package/src/web-components/visualization/gs-mutations.stories.ts +14 -8
  54. package/src/web-components/visualization/gs-mutations.tsx +18 -8
  55. package/src/web-components/visualization/gs-prevalence-over-time.stories.ts +28 -18
  56. package/src/web-components/visualization/gs-prevalence-over-time.tsx +45 -22
  57. package/src/web-components/visualization/gs-relative-growth-advantage.stories.ts +11 -5
  58. package/src/web-components/visualization/gs-relative-growth-advantage.tsx +21 -9
package/dist/style.css CHANGED
@@ -975,39 +975,6 @@ html {
975
975
  --tw-contain-paint: ;
976
976
  --tw-contain-style: ;
977
977
  }
978
- .container {
979
- width: 100%;
980
- }
981
- @media (min-width: 640px) {
982
-
983
- .container {
984
- max-width: 640px;
985
- }
986
- }
987
- @media (min-width: 768px) {
988
-
989
- .container {
990
- max-width: 768px;
991
- }
992
- }
993
- @media (min-width: 1024px) {
994
-
995
- .container {
996
- max-width: 1024px;
997
- }
998
- }
999
- @media (min-width: 1280px) {
1000
-
1001
- .container {
1002
- max-width: 1280px;
1003
- }
1004
- }
1005
- @media (min-width: 1536px) {
1006
-
1007
- .container {
1008
- max-width: 1536px;
1009
- }
1010
- }
1011
978
  .alert {
1012
979
  display: grid;
1013
980
  width: 100%;
@@ -1110,6 +1077,12 @@ html {
1110
1077
  .btn:disabled {
1111
1078
  pointer-events: none;
1112
1079
  }
1080
+ .btn-circle {
1081
+ height: 3rem;
1082
+ width: 3rem;
1083
+ border-radius: 9999px;
1084
+ padding: 0px;
1085
+ }
1113
1086
  :where(.btn:is(input[type="checkbox"])),
1114
1087
  :where(.btn:is(input[type="radio"])) {
1115
1088
  width: auto;
@@ -1262,6 +1235,17 @@ html {
1262
1235
  --glass-border-opacity: 15%;
1263
1236
  }
1264
1237
 
1238
+ .btn-ghost:hover {
1239
+ border-color: transparent;
1240
+ }
1241
+
1242
+ @supports (color: oklch(0% 0 0)) {
1243
+
1244
+ .btn-ghost:hover {
1245
+ background-color: var(--fallback-bc,oklch(var(--bc)/0.2));
1246
+ }
1247
+ }
1248
+
1265
1249
  .btn-outline.btn-primary:hover {
1266
1250
  --tw-text-opacity: 1;
1267
1251
  color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
@@ -1484,6 +1468,69 @@ html {
1484
1468
  :where(.menu li) .badge {
1485
1469
  justify-self: end;
1486
1470
  }
1471
+ .modal {
1472
+ pointer-events: none;
1473
+ position: fixed;
1474
+ inset: 0px;
1475
+ margin: 0px;
1476
+ display: grid;
1477
+ height: 100%;
1478
+ max-height: none;
1479
+ width: 100%;
1480
+ max-width: none;
1481
+ justify-items: center;
1482
+ padding: 0px;
1483
+ opacity: 0;
1484
+ overscroll-behavior: contain;
1485
+ z-index: 999;
1486
+ background-color: transparent;
1487
+ color: inherit;
1488
+ transition-duration: 200ms;
1489
+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
1490
+ transition-property: transform, opacity, visibility;
1491
+ overflow-y: hidden;
1492
+ }
1493
+ :where(.modal) {
1494
+ align-items: center;
1495
+ }
1496
+ .modal-box {
1497
+ max-height: calc(100vh - 5em);
1498
+ grid-column-start: 1;
1499
+ grid-row-start: 1;
1500
+ width: 91.666667%;
1501
+ max-width: 32rem;
1502
+ --tw-scale-x: .9;
1503
+ --tw-scale-y: .9;
1504
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1505
+ border-bottom-right-radius: var(--rounded-box, 1rem);
1506
+ border-bottom-left-radius: var(--rounded-box, 1rem);
1507
+ border-top-left-radius: var(--rounded-box, 1rem);
1508
+ border-top-right-radius: var(--rounded-box, 1rem);
1509
+ --tw-bg-opacity: 1;
1510
+ background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
1511
+ padding: 1.5rem;
1512
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
1513
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
1514
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
1515
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1516
+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
1517
+ transition-duration: 200ms;
1518
+ box-shadow: rgba(0, 0, 0, 0.25) 0px 25px 50px -12px;
1519
+ overflow-y: auto;
1520
+ overscroll-behavior: contain;
1521
+ }
1522
+ .modal-open,
1523
+ .modal:target,
1524
+ .modal-toggle:checked + .modal,
1525
+ .modal[open] {
1526
+ pointer-events: auto;
1527
+ visibility: visible;
1528
+ opacity: 1;
1529
+ }
1530
+ :root:has(:is(.modal-open, .modal:target, .modal-toggle:checked + .modal, .modal[open])) {
1531
+ overflow: hidden;
1532
+ scrollbar-gutter: stable;
1533
+ }
1487
1534
  .radio {
1488
1535
  flex-shrink: 0;
1489
1536
  --chkbg: var(--bc);
@@ -1719,6 +1766,20 @@ input.tab:checked + .tab-content,
1719
1766
  --glass-opacity: 25%;
1720
1767
  --glass-border-opacity: 15%;
1721
1768
  }
1769
+ .btn-ghost {
1770
+ border-width: 1px;
1771
+ border-color: transparent;
1772
+ background-color: transparent;
1773
+ color: currentColor;
1774
+ --tw-shadow: 0 0 #0000;
1775
+ --tw-shadow-colored: 0 0 #0000;
1776
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1777
+ outline-color: currentColor;
1778
+ }
1779
+ .btn-ghost.btn-active {
1780
+ border-color: transparent;
1781
+ background-color: var(--fallback-bc,oklch(var(--bc)/0.2));
1782
+ }
1722
1783
  .btn-outline.btn-primary {
1723
1784
  --tw-text-opacity: 1;
1724
1785
  color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)));
@@ -2040,6 +2101,29 @@ input.tab:checked + .tab-content,
2040
2101
  border-color: currentColor;
2041
2102
  opacity: 0.6;
2042
2103
  }
2104
+ .modal:not(dialog:not(.modal-open)),
2105
+ .modal::backdrop {
2106
+ background-color: #0006;
2107
+ animation: modal-pop 0.2s ease-out;
2108
+ }
2109
+ .modal-backdrop {
2110
+ z-index: -1;
2111
+ grid-column-start: 1;
2112
+ grid-row-start: 1;
2113
+ display: grid;
2114
+ align-self: stretch;
2115
+ justify-self: stretch;
2116
+ color: transparent;
2117
+ }
2118
+ .modal-open .modal-box,
2119
+ .modal-toggle:checked + .modal .modal-box,
2120
+ .modal:target .modal-box,
2121
+ .modal[open] .modal-box {
2122
+ --tw-translate-y: 0px;
2123
+ --tw-scale-x: 1;
2124
+ --tw-scale-y: 1;
2125
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
2126
+ }
2043
2127
  @keyframes modal-pop {
2044
2128
 
2045
2129
  0% {
@@ -2525,6 +2609,18 @@ input.tab:checked + .tab-content,
2525
2609
  border-radius: 9999px;
2526
2610
  padding: 0px;
2527
2611
  }
2612
+ .btn-circle:where(.btn-md) {
2613
+ height: 3rem;
2614
+ width: 3rem;
2615
+ border-radius: 9999px;
2616
+ padding: 0px;
2617
+ }
2618
+ .btn-circle:where(.btn-lg) {
2619
+ height: 4rem;
2620
+ width: 4rem;
2621
+ border-radius: 9999px;
2622
+ padding: 0px;
2623
+ }
2528
2624
  .join.join-vertical {
2529
2625
  flex-direction: column;
2530
2626
  }
@@ -2639,6 +2735,42 @@ input.tab:checked + .tab-content,
2639
2735
  margin-bottom: 0px;
2640
2736
  margin-inline-start: -1px;
2641
2737
  }
2738
+ .modal-top :where(.modal-box) {
2739
+ width: 100%;
2740
+ max-width: none;
2741
+ --tw-translate-y: -2.5rem;
2742
+ --tw-scale-x: 1;
2743
+ --tw-scale-y: 1;
2744
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
2745
+ border-bottom-right-radius: var(--rounded-box, 1rem);
2746
+ border-bottom-left-radius: var(--rounded-box, 1rem);
2747
+ border-top-left-radius: 0px;
2748
+ border-top-right-radius: 0px;
2749
+ }
2750
+ .modal-middle :where(.modal-box) {
2751
+ width: 91.666667%;
2752
+ max-width: 32rem;
2753
+ --tw-translate-y: 0px;
2754
+ --tw-scale-x: .9;
2755
+ --tw-scale-y: .9;
2756
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
2757
+ border-top-left-radius: var(--rounded-box, 1rem);
2758
+ border-top-right-radius: var(--rounded-box, 1rem);
2759
+ border-bottom-right-radius: var(--rounded-box, 1rem);
2760
+ border-bottom-left-radius: var(--rounded-box, 1rem);
2761
+ }
2762
+ .modal-bottom :where(.modal-box) {
2763
+ width: 100%;
2764
+ max-width: none;
2765
+ --tw-translate-y: 2.5rem;
2766
+ --tw-scale-x: 1;
2767
+ --tw-scale-y: 1;
2768
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
2769
+ border-top-left-radius: var(--rounded-box, 1rem);
2770
+ border-top-right-radius: var(--rounded-box, 1rem);
2771
+ border-bottom-right-radius: 0px;
2772
+ border-bottom-left-radius: 0px;
2773
+ }
2642
2774
  .steps-horizontal .step {
2643
2775
  grid-template-rows: 40px 1fr;
2644
2776
  grid-template-columns: auto;
@@ -2764,9 +2896,15 @@ input.tab:checked + .tab-content,
2764
2896
  .relative {
2765
2897
  position: relative;
2766
2898
  }
2899
+ .right-2 {
2900
+ right: 0.5rem;
2901
+ }
2767
2902
  .right-6 {
2768
2903
  right: 1.5rem;
2769
2904
  }
2905
+ .top-2 {
2906
+ top: 0.5rem;
2907
+ }
2770
2908
  .top-8 {
2771
2909
  top: 2rem;
2772
2910
  }
@@ -3003,6 +3141,10 @@ input.tab:checked + .tab-content,
3003
3141
  padding-top: 0.5rem;
3004
3142
  padding-bottom: 0.5rem;
3005
3143
  }
3144
+ .py-4 {
3145
+ padding-top: 1rem;
3146
+ padding-bottom: 1rem;
3147
+ }
3006
3148
  .text-justify {
3007
3149
  text-align: justify;
3008
3150
  }
@@ -3083,6 +3225,10 @@ input.tab:checked + .tab-content,
3083
3225
  --tw-text-opacity: 1;
3084
3226
  color: rgb(30 64 175 / var(--tw-text-opacity));
3085
3227
  }
3228
+ .hover\:text-gray-300:hover {
3229
+ --tw-text-opacity: 1;
3230
+ color: rgb(209 213 219 / var(--tw-text-opacity));
3231
+ }
3086
3232
  .hover\:text-gray-700:hover {
3087
3233
  --tw-text-opacity: 1;
3088
3234
  color: rgb(55 65 81 / var(--tw-text-opacity));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -101,7 +101,6 @@
101
101
  "postcss": "^8.4.38",
102
102
  "prettier": "^3.2.5",
103
103
  "react": "^18.3.1",
104
- "release-please": "^16.10.2",
105
104
  "storybook": "^8.0.9",
106
105
  "storybook-addon-fetch-mock": "^2.0.0",
107
106
  "tailwindcss": "^3.4.3",
package/src/constants.ts CHANGED
@@ -1,4 +1,4 @@
1
- export const LAPIS_URL = 'https://s1.int.genspectrum.org/open';
1
+ export const LAPIS_URL = 'https://lapis.cov-spectrum.org/open/v2/';
2
2
 
3
3
  export const AGGREGATED_ENDPOINT = `${LAPIS_URL}/sample/aggregated`;
4
4
  export const NUCLEOTIDE_MUTATIONS_ENDPOINT = `${LAPIS_URL}/sample/nucleotideMutations`;
@@ -3,11 +3,35 @@ import {
3
3
  aggregatedResponse,
4
4
  insertionsResponse,
5
5
  type LapisBaseRequest,
6
+ lapisError,
6
7
  type MutationsRequest,
7
8
  mutationsResponse,
9
+ problemDetail,
10
+ type ProblemDetail,
8
11
  } from './lapisTypes';
9
12
  import { type SequenceType } from '../types';
10
13
 
14
+ export class UnknownLapisError extends Error {
15
+ constructor(
16
+ message: string,
17
+ public readonly status: number,
18
+ ) {
19
+ super(message);
20
+ this.name = 'UnknownLapisError';
21
+ }
22
+ }
23
+
24
+ export class LapisError extends Error {
25
+ constructor(
26
+ message: string,
27
+ public readonly status: number,
28
+ public readonly problemDetail: ProblemDetail,
29
+ ) {
30
+ super(message);
31
+ this.name = 'LapisError';
32
+ }
33
+ }
34
+
11
35
  export async function fetchAggregated(lapisUrl: string, body: LapisBaseRequest, signal?: AbortSignal) {
12
36
  const response = await fetch(aggregatedEndpoint(lapisUrl), {
13
37
  method: 'POST',
@@ -79,9 +103,29 @@ export async function fetchReferenceGenome(lapisUrl: string, signal?: AbortSigna
79
103
  const handleErrors = async (response: Response) => {
80
104
  if (!response.ok) {
81
105
  if (response.status >= 400 && response.status < 500) {
82
- throw new Error(`${response.statusText}: ${JSON.stringify(await response.json())}`);
106
+ const json = await response.json();
107
+
108
+ const lapisErrorResult = lapisError.safeParse(json);
109
+ if (lapisErrorResult.success) {
110
+ throw new LapisError(
111
+ response.statusText + lapisErrorResult.data.error.detail,
112
+ response.status,
113
+ lapisErrorResult.data.error,
114
+ );
115
+ }
116
+
117
+ const problemDetailResult = problemDetail.safeParse(json);
118
+ if (problemDetailResult.success) {
119
+ throw new LapisError(
120
+ response.statusText + problemDetailResult.data.detail,
121
+ response.status,
122
+ problemDetailResult.data,
123
+ );
124
+ }
125
+
126
+ throw new UnknownLapisError(`${response.statusText}: ${JSON.stringify(json)}`, response.status);
83
127
  }
84
- throw new Error(`${response.statusText}: ${response.status}`);
128
+ throw new UnknownLapisError(`${response.statusText}: ${response.status}`, response.status);
85
129
  }
86
130
  };
87
131
 
@@ -49,3 +49,17 @@ function makeLapisResponse<T extends ZodTypeAny>(data: T) {
49
49
  data,
50
50
  });
51
51
  }
52
+
53
+ export const problemDetail = z.object({
54
+ title: z.string().optional(),
55
+ status: z.number(),
56
+ detail: z.string().optional(),
57
+ type: z.string(),
58
+ instance: z.string().optional(),
59
+ });
60
+
61
+ export type ProblemDetail = z.infer<typeof problemDetail>;
62
+
63
+ export const lapisError = z.object({
64
+ error: problemDetail,
65
+ });
@@ -10,7 +10,8 @@ const meta: Meta<AggregateProps> = {
10
10
  component: Aggregate,
11
11
  argTypes: {
12
12
  fields: [{ control: 'object' }],
13
- size: [{ control: 'object' }],
13
+ width: { control: 'text' },
14
+ height: { control: 'text' },
14
15
  headline: { control: 'text' },
15
16
  },
16
17
  parameters: {
@@ -49,7 +50,8 @@ export const Default: StoryObj<AggregateProps> = {
49
50
  filter: {
50
51
  country: 'USA',
51
52
  },
52
- size: { width: '100%', height: '70vh' },
53
+ width: '100%',
54
+ height: '700px',
53
55
  headline: 'Aggregate',
54
56
  },
55
57
  };
@@ -12,14 +12,15 @@ import Headline from '../components/headline';
12
12
  import Info from '../components/info';
13
13
  import { LoadingDisplay } from '../components/loading-display';
14
14
  import { NoDataDisplay } from '../components/no-data-display';
15
- import { ResizeContainer, type Size } from '../components/resize-container';
15
+ import { ResizeContainer } from '../components/resize-container';
16
16
  import Tabs from '../components/tabs';
17
17
  import { useQuery } from '../useQuery';
18
18
 
19
19
  export type View = 'table';
20
20
 
21
21
  export type AggregateProps = {
22
- size?: Size;
22
+ width: string;
23
+ height: string;
23
24
  headline?: string;
24
25
  } & AggregateInnerProps;
25
26
 
@@ -31,16 +32,17 @@ export interface AggregateInnerProps {
31
32
 
32
33
  export const Aggregate: FunctionComponent<AggregateProps> = ({
33
34
  views,
34
- size,
35
+ width,
36
+ height,
35
37
  headline = 'Mutations',
36
38
  filter,
37
39
  fields,
38
40
  }) => {
39
- const defaultSize = { height: '600px', width: '100%' };
41
+ const size = { height, width };
40
42
 
41
43
  return (
42
- <ErrorBoundary size={size} defaultSize={defaultSize} headline={headline}>
43
- <ResizeContainer size={size} defaultSize={defaultSize}>
44
+ <ErrorBoundary size={size} headline={headline}>
45
+ <ResizeContainer size={size}>
44
46
  <Headline heading={headline}>
45
47
  <AggregateInner fields={fields} filter={filter} views={views} />
46
48
  </Headline>
@@ -12,22 +12,20 @@ const meta: Meta = {
12
12
  defaultSize: { control: 'object' },
13
13
  headline: { control: 'text' },
14
14
  },
15
+ args: {
16
+ size: { height: '600px', width: '100%' },
17
+ headline: 'Some headline',
18
+ },
15
19
  };
16
20
 
17
21
  export default meta;
18
22
 
19
23
  export const ErrorBoundaryWithoutErrorStory: StoryObj = {
20
24
  render: (args) => (
21
- <ErrorBoundary size={args.size} defaultSize={args.defaultSize} headline={args.headline}>
25
+ <ErrorBoundary size={args.size} headline={args.headline}>
22
26
  <div>Some content</div>
23
27
  </ErrorBoundary>
24
28
  ),
25
- args: {
26
- size: { height: '600px', width: '100%' },
27
- defaultSize: { height: '600px', width: '100%' },
28
- headline: 'Some headline',
29
- },
30
-
31
29
  play: async ({ canvasElement }) => {
32
30
  const canvas = within(canvasElement);
33
31
  const content = canvas.getByText('Some content', { exact: false });
@@ -38,16 +36,10 @@ export const ErrorBoundaryWithoutErrorStory: StoryObj = {
38
36
 
39
37
  export const ErrorBoundaryWithErrorStory: StoryObj = {
40
38
  render: (args) => (
41
- <ErrorBoundary size={args.size} defaultSize={args.defaultSize} headline={args.headline}>
39
+ <ErrorBoundary size={args.size} headline={args.headline}>
42
40
  <ContentThatThrowsError />
43
41
  </ErrorBoundary>
44
42
  ),
45
- args: {
46
- size: { height: '600px', width: '100%' },
47
- defaultSize: { height: '600px', width: '100%' },
48
- headline: 'Some headline',
49
- },
50
-
51
43
  play: async ({ canvasElement }) => {
52
44
  const canvas = within(canvasElement);
53
45
  const content = canvas.queryByText('Some content.', { exact: false });
@@ -5,21 +5,12 @@ import { ErrorDisplay } from './error-display';
5
5
  import { ResizeContainer, type Size } from './resize-container';
6
6
  import Headline from '../components/headline';
7
7
 
8
- export const ErrorBoundary: FunctionComponent<{ size?: Size; defaultSize: Size; headline?: string }> = ({
9
- size,
10
- defaultSize,
11
- headline,
12
- children,
13
- }) => {
8
+ export const ErrorBoundary: FunctionComponent<{ size: Size; headline?: string }> = ({ size, headline, children }) => {
14
9
  const [internalError] = useErrorBoundary();
15
10
 
16
- if (internalError) {
17
- console.error(internalError);
18
- }
19
-
20
11
  if (internalError) {
21
12
  return (
22
- <ResizeContainer defaultSize={defaultSize} size={size}>
13
+ <ResizeContainer size={size}>
23
14
  <Headline heading={headline}>
24
15
  <ErrorDisplay error={internalError} />
25
16
  </Headline>
@@ -1,5 +1,5 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
- import { expect, waitFor, within } from '@storybook/test';
2
+ import { expect, userEvent, waitFor, within } from '@storybook/test';
3
3
 
4
4
  import { ErrorDisplay, UserFacingError } from './error-display';
5
5
  import { ResizeContainer } from './resize-container';
@@ -14,7 +14,7 @@ export default meta;
14
14
 
15
15
  export const ErrorStory: StoryObj = {
16
16
  render: () => (
17
- <ResizeContainer defaultSize={{ height: '600px', width: '100%' }}>
17
+ <ResizeContainer size={{ height: '600px', width: '100%' }}>
18
18
  <ErrorDisplay error={new Error('some message')} />
19
19
  </ResizeContainer>
20
20
  ),
@@ -29,15 +29,22 @@ export const ErrorStory: StoryObj = {
29
29
 
30
30
  export const UserFacingErrorStory: StoryObj = {
31
31
  render: () => (
32
- <ResizeContainer defaultSize={{ height: '600px', width: '100%' }}>
33
- <ErrorDisplay error={new UserFacingError('some message')} />
32
+ <ResizeContainer size={{ height: '600px', width: '100%' }}>
33
+ <ErrorDisplay error={new UserFacingError('Error Title', 'some message')} />
34
34
  </ResizeContainer>
35
35
  ),
36
36
 
37
37
  play: async ({ canvasElement }) => {
38
38
  const canvas = within(canvasElement);
39
39
  const error = canvas.getByText('Oops! Something went wrong.', { exact: false });
40
+ const detailMessage = () => canvas.getByText('some message');
40
41
  await waitFor(() => expect(error).toBeInTheDocument());
41
- await waitFor(() => expect(canvas.getByText('some message')).toBeInTheDocument());
42
+ await waitFor(() => {
43
+ expect(detailMessage()).not.toBeVisible();
44
+ });
45
+ await userEvent.click(canvas.getByText('Show details.'));
46
+ await waitFor(() => {
47
+ expect(detailMessage()).toBeVisible();
48
+ });
42
49
  },
43
50
  };
@@ -1,18 +1,52 @@
1
1
  import { type FunctionComponent } from 'preact';
2
+ import { useRef } from 'preact/hooks';
2
3
 
3
4
  export class UserFacingError extends Error {
4
- constructor(message: string) {
5
+ constructor(
6
+ public readonly headline: string,
7
+ message: string,
8
+ ) {
5
9
  super(message);
6
10
  this.name = 'UserFacingError';
7
11
  }
8
12
  }
9
13
 
10
14
  export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) => {
15
+ console.error(error);
16
+
17
+ const ref = useRef<HTMLDialogElement>(null);
18
+
11
19
  return (
12
20
  <div className='h-full w-full rounded-md border-2 border-gray-100 p-2 flex items-center justify-center flex-col'>
13
21
  <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>}
22
+ <div>
23
+ Oops! Something went wrong.
24
+ {error instanceof UserFacingError && (
25
+ <>
26
+ {' '}
27
+ <button
28
+ className='text-sm text-gray-600 hover:text-gray-300'
29
+ onClick={() => ref.current?.showModal()}
30
+ >
31
+ Show details.
32
+ </button>
33
+ <dialog ref={ref} class='modal'>
34
+ <div class='modal-box'>
35
+ <form method='dialog'>
36
+ <button className='btn btn-sm btn-circle btn-ghost absolute right-2 top-2'>
37
+
38
+ </button>
39
+ </form>
40
+ <h1 class='text-lg'>{error.headline}</h1>
41
+ <p class='py-4'>{error.message}</p>
42
+ </div>
43
+ <form method='dialog' class='modal-backdrop'>
44
+ <button>close</button>
45
+ </form>
46
+ </dialog>
47
+ </>
48
+ )}
49
+ </div>
16
50
  </div>
17
51
  );
18
52
  };
@@ -13,7 +13,7 @@ export default meta;
13
13
 
14
14
  export const LoadingStory: StoryObj = {
15
15
  render: () => (
16
- <ResizeContainer defaultSize={{ height: '600px', width: '100%' }}>
16
+ <ResizeContainer size={{ height: '600px', width: '100%' }}>
17
17
  <LoadingDisplay />
18
18
  </ResizeContainer>
19
19
  ),
@@ -1,23 +1,14 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
 
3
3
  export type Size = {
4
- width?: string;
5
- height?: string;
4
+ width: string;
5
+ height: string;
6
6
  };
7
7
 
8
8
  export interface ResizeContainerProps {
9
- size?: Size;
10
- defaultSize: Size;
9
+ size: Size;
11
10
  }
12
11
 
13
- export const ResizeContainer: FunctionComponent<ResizeContainerProps> = ({ children, size, defaultSize }) => {
14
- return <div style={extendByDefault(size, defaultSize)}>{children}</div>;
15
- };
16
-
17
- const extendByDefault = (size: Size | undefined, defaultSize: Size) => {
18
- if (size === undefined) {
19
- return defaultSize;
20
- }
21
-
22
- return { ...defaultSize, ...size };
12
+ export const ResizeContainer: FunctionComponent<ResizeContainerProps> = ({ children, size }) => {
13
+ return <div style={size}>{children}</div>;
23
14
  };
@@ -60,6 +60,7 @@ const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
60
60
  customSelectOptions: [{ label: 'CustomDateRange', dateFrom: '2021-01-01', dateTo: '2021-12-31' }],
61
61
  earliestDate: '1970-01-01',
62
62
  initialValue: PRESET_VALUE_LAST_3_MONTHS,
63
+ dateColumn: 'aDateColumn',
63
64
  width: '100%',
64
65
  },
65
66
  decorators: [withActions],
@@ -75,6 +76,7 @@ export const Primary: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
75
76
  earliestDate={args.earliestDate}
76
77
  initialValue={args.initialValue}
77
78
  width={args.width}
79
+ dateColumn={args.dateColumn}
78
80
  />
79
81
  </LapisUrlContext.Provider>
80
82
  ),