@genspectrum/dashboard-components 0.2.0 → 0.3.1

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 (59) hide show
  1. package/custom-elements.json +330 -179
  2. package/dist/dashboard-components.js +379 -187
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +160 -114
  5. package/dist/style.css +171 -71
  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.stories.tsx +1 -1
  22. package/src/preact/locationFilter/location-filter.tsx +16 -30
  23. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +6 -3
  24. package/src/preact/mutationComparison/mutation-comparison.tsx +10 -13
  25. package/src/preact/mutationComparison/queryMutationData.ts +2 -3
  26. package/src/preact/mutationFilter/mutation-filter.stories.tsx +8 -8
  27. package/src/preact/mutationFilter/mutation-filter.tsx +7 -6
  28. package/src/preact/mutations/mutations.stories.tsx +6 -3
  29. package/src/preact/mutations/mutations.tsx +8 -6
  30. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +14 -7
  31. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +10 -8
  32. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +6 -3
  33. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +9 -7
  34. package/src/preact/textInput/text-input.stories.tsx +26 -0
  35. package/src/preact/textInput/text-input.tsx +4 -5
  36. package/src/query/queryPrevalenceOverTime.ts +4 -10
  37. package/src/types.ts +4 -1
  38. package/src/web-components/ResizeContainer.mdx +13 -0
  39. package/src/web-components/app.ts +3 -1
  40. package/src/web-components/input/gs-date-range-selector.stories.ts +10 -2
  41. package/src/web-components/input/gs-date-range-selector.tsx +26 -16
  42. package/src/web-components/input/gs-location-filter.stories.ts +5 -7
  43. package/src/web-components/input/gs-location-filter.tsx +6 -7
  44. package/src/web-components/input/gs-mutation-filter.stories.ts +11 -8
  45. package/src/web-components/input/gs-mutation-filter.tsx +38 -26
  46. package/src/web-components/input/gs-text-input.stories.ts +3 -3
  47. package/src/web-components/input/gs-text-input.tsx +10 -10
  48. package/src/web-components/input/introduction.mdx +11 -0
  49. package/src/web-components/introduction.mdx +15 -0
  50. package/src/web-components/visualization/gs-aggregate.stories.ts +19 -6
  51. package/src/web-components/visualization/gs-aggregate.tsx +31 -15
  52. package/src/web-components/visualization/gs-mutation-comparison.stories.ts +13 -7
  53. package/src/web-components/visualization/gs-mutation-comparison.tsx +26 -17
  54. package/src/web-components/visualization/gs-mutations.stories.ts +14 -8
  55. package/src/web-components/visualization/gs-mutations.tsx +18 -8
  56. package/src/web-components/visualization/gs-prevalence-over-time.stories.ts +28 -18
  57. package/src/web-components/visualization/gs-prevalence-over-time.tsx +45 -22
  58. package/src/web-components/visualization/gs-relative-growth-advantage.stories.ts +11 -5
  59. 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,16 +1235,14 @@ html {
1262
1235
  --glass-border-opacity: 15%;
1263
1236
  }
1264
1237
 
1265
- .btn-outline.btn-primary:hover {
1266
- --tw-text-opacity: 1;
1267
- color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
1238
+ .btn-ghost:hover {
1239
+ border-color: transparent;
1268
1240
  }
1269
1241
 
1270
- @supports (color: color-mix(in oklab, black, black)) {
1242
+ @supports (color: oklch(0% 0 0)) {
1271
1243
 
1272
- .btn-outline.btn-primary:hover {
1273
- background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black);
1274
- border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black);
1244
+ .btn-ghost:hover {
1245
+ background-color: var(--fallback-bc,oklch(var(--bc)/0.2));
1275
1246
  }
1276
1247
  }
1277
1248
 
@@ -1484,6 +1455,69 @@ html {
1484
1455
  :where(.menu li) .badge {
1485
1456
  justify-self: end;
1486
1457
  }
1458
+ .modal {
1459
+ pointer-events: none;
1460
+ position: fixed;
1461
+ inset: 0px;
1462
+ margin: 0px;
1463
+ display: grid;
1464
+ height: 100%;
1465
+ max-height: none;
1466
+ width: 100%;
1467
+ max-width: none;
1468
+ justify-items: center;
1469
+ padding: 0px;
1470
+ opacity: 0;
1471
+ overscroll-behavior: contain;
1472
+ z-index: 999;
1473
+ background-color: transparent;
1474
+ color: inherit;
1475
+ transition-duration: 200ms;
1476
+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
1477
+ transition-property: transform, opacity, visibility;
1478
+ overflow-y: hidden;
1479
+ }
1480
+ :where(.modal) {
1481
+ align-items: center;
1482
+ }
1483
+ .modal-box {
1484
+ max-height: calc(100vh - 5em);
1485
+ grid-column-start: 1;
1486
+ grid-row-start: 1;
1487
+ width: 91.666667%;
1488
+ max-width: 32rem;
1489
+ --tw-scale-x: .9;
1490
+ --tw-scale-y: .9;
1491
+ 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));
1492
+ border-bottom-right-radius: var(--rounded-box, 1rem);
1493
+ border-bottom-left-radius: var(--rounded-box, 1rem);
1494
+ border-top-left-radius: var(--rounded-box, 1rem);
1495
+ border-top-right-radius: var(--rounded-box, 1rem);
1496
+ --tw-bg-opacity: 1;
1497
+ background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity)));
1498
+ padding: 1.5rem;
1499
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter;
1500
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter;
1501
+ transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter;
1502
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1503
+ transition-timing-function: cubic-bezier(0, 0, 0.2, 1);
1504
+ transition-duration: 200ms;
1505
+ box-shadow: rgba(0, 0, 0, 0.25) 0px 25px 50px -12px;
1506
+ overflow-y: auto;
1507
+ overscroll-behavior: contain;
1508
+ }
1509
+ .modal-open,
1510
+ .modal:target,
1511
+ .modal-toggle:checked + .modal,
1512
+ .modal[open] {
1513
+ pointer-events: auto;
1514
+ visibility: visible;
1515
+ opacity: 1;
1516
+ }
1517
+ :root:has(:is(.modal-open, .modal:target, .modal-toggle:checked + .modal, .modal[open])) {
1518
+ overflow: hidden;
1519
+ scrollbar-gutter: stable;
1520
+ }
1487
1521
  .radio {
1488
1522
  flex-shrink: 0;
1489
1523
  --chkbg: var(--bc);
@@ -1681,34 +1715,12 @@ input.tab:checked + .tab-content,
1681
1715
  background-color: var(--btn-color, var(--fallback-b2));
1682
1716
  border-color: var(--btn-color, var(--fallback-b2));
1683
1717
  }
1684
-
1685
- .btn-primary {
1686
- --btn-color: var(--fallback-p);
1687
- }
1688
- }
1689
- @supports (color: color-mix(in oklab, black, black)) {
1690
-
1691
- .btn-outline.btn-primary.btn-active {
1692
- background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black);
1693
- border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black);
1694
- }
1695
1718
  }
1696
1719
  .btn:focus-visible {
1697
1720
  outline-style: solid;
1698
1721
  outline-width: 2px;
1699
1722
  outline-offset: 2px;
1700
1723
  }
1701
- .btn-primary {
1702
- --tw-text-opacity: 1;
1703
- color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
1704
- outline-color: var(--fallback-p,oklch(var(--p)/1));
1705
- }
1706
- @supports (color: oklch(0% 0 0)) {
1707
-
1708
- .btn-primary {
1709
- --btn-color: var(--p);
1710
- }
1711
- }
1712
1724
  .btn.glass {
1713
1725
  --tw-shadow: 0 0 #0000;
1714
1726
  --tw-shadow-colored: 0 0 #0000;
@@ -1719,13 +1731,19 @@ input.tab:checked + .tab-content,
1719
1731
  --glass-opacity: 25%;
1720
1732
  --glass-border-opacity: 15%;
1721
1733
  }
1722
- .btn-outline.btn-primary {
1723
- --tw-text-opacity: 1;
1724
- color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity)));
1734
+ .btn-ghost {
1735
+ border-width: 1px;
1736
+ border-color: transparent;
1737
+ background-color: transparent;
1738
+ color: currentColor;
1739
+ --tw-shadow: 0 0 #0000;
1740
+ --tw-shadow-colored: 0 0 #0000;
1741
+ box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1742
+ outline-color: currentColor;
1725
1743
  }
1726
- .btn-outline.btn-primary.btn-active {
1727
- --tw-text-opacity: 1;
1728
- color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity)));
1744
+ .btn-ghost.btn-active {
1745
+ border-color: transparent;
1746
+ background-color: var(--fallback-bc,oklch(var(--bc)/0.2));
1729
1747
  }
1730
1748
  .btn.btn-disabled,
1731
1749
  .btn[disabled],
@@ -2040,6 +2058,29 @@ input.tab:checked + .tab-content,
2040
2058
  border-color: currentColor;
2041
2059
  opacity: 0.6;
2042
2060
  }
2061
+ .modal:not(dialog:not(.modal-open)),
2062
+ .modal::backdrop {
2063
+ background-color: #0006;
2064
+ animation: modal-pop 0.2s ease-out;
2065
+ }
2066
+ .modal-backdrop {
2067
+ z-index: -1;
2068
+ grid-column-start: 1;
2069
+ grid-row-start: 1;
2070
+ display: grid;
2071
+ align-self: stretch;
2072
+ justify-self: stretch;
2073
+ color: transparent;
2074
+ }
2075
+ .modal-open .modal-box,
2076
+ .modal-toggle:checked + .modal .modal-box,
2077
+ .modal:target .modal-box,
2078
+ .modal[open] .modal-box {
2079
+ --tw-translate-y: 0px;
2080
+ --tw-scale-x: 1;
2081
+ --tw-scale-y: 1;
2082
+ 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));
2083
+ }
2043
2084
  @keyframes modal-pop {
2044
2085
 
2045
2086
  0% {
@@ -2525,6 +2566,18 @@ input.tab:checked + .tab-content,
2525
2566
  border-radius: 9999px;
2526
2567
  padding: 0px;
2527
2568
  }
2569
+ .btn-circle:where(.btn-md) {
2570
+ height: 3rem;
2571
+ width: 3rem;
2572
+ border-radius: 9999px;
2573
+ padding: 0px;
2574
+ }
2575
+ .btn-circle:where(.btn-lg) {
2576
+ height: 4rem;
2577
+ width: 4rem;
2578
+ border-radius: 9999px;
2579
+ padding: 0px;
2580
+ }
2528
2581
  .join.join-vertical {
2529
2582
  flex-direction: column;
2530
2583
  }
@@ -2639,6 +2692,42 @@ input.tab:checked + .tab-content,
2639
2692
  margin-bottom: 0px;
2640
2693
  margin-inline-start: -1px;
2641
2694
  }
2695
+ .modal-top :where(.modal-box) {
2696
+ width: 100%;
2697
+ max-width: none;
2698
+ --tw-translate-y: -2.5rem;
2699
+ --tw-scale-x: 1;
2700
+ --tw-scale-y: 1;
2701
+ 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));
2702
+ border-bottom-right-radius: var(--rounded-box, 1rem);
2703
+ border-bottom-left-radius: var(--rounded-box, 1rem);
2704
+ border-top-left-radius: 0px;
2705
+ border-top-right-radius: 0px;
2706
+ }
2707
+ .modal-middle :where(.modal-box) {
2708
+ width: 91.666667%;
2709
+ max-width: 32rem;
2710
+ --tw-translate-y: 0px;
2711
+ --tw-scale-x: .9;
2712
+ --tw-scale-y: .9;
2713
+ 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));
2714
+ border-top-left-radius: var(--rounded-box, 1rem);
2715
+ border-top-right-radius: var(--rounded-box, 1rem);
2716
+ border-bottom-right-radius: var(--rounded-box, 1rem);
2717
+ border-bottom-left-radius: var(--rounded-box, 1rem);
2718
+ }
2719
+ .modal-bottom :where(.modal-box) {
2720
+ width: 100%;
2721
+ max-width: none;
2722
+ --tw-translate-y: 2.5rem;
2723
+ --tw-scale-x: 1;
2724
+ --tw-scale-y: 1;
2725
+ 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));
2726
+ border-top-left-radius: var(--rounded-box, 1rem);
2727
+ border-top-right-radius: var(--rounded-box, 1rem);
2728
+ border-bottom-right-radius: 0px;
2729
+ border-bottom-left-radius: 0px;
2730
+ }
2642
2731
  .steps-horizontal .step {
2643
2732
  grid-template-rows: 40px 1fr;
2644
2733
  grid-template-columns: auto;
@@ -2764,9 +2853,15 @@ input.tab:checked + .tab-content,
2764
2853
  .relative {
2765
2854
  position: relative;
2766
2855
  }
2856
+ .right-2 {
2857
+ right: 0.5rem;
2858
+ }
2767
2859
  .right-6 {
2768
2860
  right: 1.5rem;
2769
2861
  }
2862
+ .top-2 {
2863
+ top: 0.5rem;
2864
+ }
2770
2865
  .top-8 {
2771
2866
  top: 2rem;
2772
2867
  }
@@ -2800,9 +2895,6 @@ input.tab:checked + .tab-content,
2800
2895
  .me-1 {
2801
2896
  margin-inline-end: 0.25rem;
2802
2897
  }
2803
- .ml-1 {
2804
- margin-left: 0.25rem;
2805
- }
2806
2898
  .ml-2 {
2807
2899
  margin-left: 0.5rem;
2808
2900
  }
@@ -3003,6 +3095,10 @@ input.tab:checked + .tab-content,
3003
3095
  padding-top: 0.5rem;
3004
3096
  padding-bottom: 0.5rem;
3005
3097
  }
3098
+ .py-4 {
3099
+ padding-top: 1rem;
3100
+ padding-bottom: 1rem;
3101
+ }
3006
3102
  .text-justify {
3007
3103
  text-align: justify;
3008
3104
  }
@@ -3083,6 +3179,10 @@ input.tab:checked + .tab-content,
3083
3179
  --tw-text-opacity: 1;
3084
3180
  color: rgb(30 64 175 / var(--tw-text-opacity));
3085
3181
  }
3182
+ .hover\:text-gray-300:hover {
3183
+ --tw-text-opacity: 1;
3184
+ color: rgb(209 213 219 / var(--tw-text-opacity));
3185
+ }
3086
3186
  .hover\:text-gray-700:hover {
3087
3187
  --tw-text-opacity: 1;
3088
3188
  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.1",
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
  ),