@genspectrum/dashboard-components 0.8.0 → 0.8.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 (26) hide show
  1. package/dist/assets/mutationOverTimeWorker-ICjqmm9j.js.map +1 -0
  2. package/dist/dashboard-components.js +131 -67
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +39 -39
  5. package/dist/style.css +36 -0
  6. package/package.json +1 -1
  7. package/src/lapisApi/lapisApi.ts +59 -34
  8. package/src/preact/aggregatedData/aggregate.stories.tsx +35 -0
  9. package/src/preact/aggregatedData/aggregate.tsx +1 -2
  10. package/src/preact/components/error-boundary.tsx +9 -4
  11. package/src/preact/components/error-display.stories.tsx +23 -3
  12. package/src/preact/components/error-display.tsx +37 -25
  13. package/src/preact/dateRangeSelector/date-range-selector.tsx +1 -1
  14. package/src/preact/lineageFilter/lineage-filter.tsx +2 -3
  15. package/src/preact/locationFilter/location-filter.tsx +2 -3
  16. package/src/preact/mutationComparison/mutation-comparison.tsx +1 -2
  17. package/src/preact/mutationFilter/mutation-filter.tsx +1 -1
  18. package/src/preact/mutations/mutations.tsx +1 -2
  19. package/src/preact/mutationsOverTime/mutations-over-time.tsx +1 -2
  20. package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +1 -2
  21. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +1 -2
  22. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +1 -2
  23. package/src/preact/textInput/text-input.tsx +2 -3
  24. package/standalone-bundle/dashboard-components.js +4184 -4089
  25. package/standalone-bundle/dashboard-components.js.map +1 -1
  26. package/dist/assets/mutationOverTimeWorker-BOCXtKzd.js.map +0 -1
@@ -1029,7 +1029,10 @@ declare global {
1029
1029
 
1030
1030
  declare global {
1031
1031
  interface HTMLElementTagNameMap {
1032
- 'gs-mutation-comparison-component': MutationComparisonComponent;
1032
+ 'gs-location-filter': LocationFilterComponent;
1033
+ }
1034
+ interface HTMLElementEventMap {
1035
+ 'gs-location-changed': CustomEvent<Record<string, string>>;
1033
1036
  }
1034
1037
  }
1035
1038
 
@@ -1037,7 +1040,7 @@ declare global {
1037
1040
  declare global {
1038
1041
  namespace JSX {
1039
1042
  interface IntrinsicElements {
1040
- 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1043
+ 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1041
1044
  }
1042
1045
  }
1043
1046
  }
@@ -1045,7 +1048,10 @@ declare global {
1045
1048
 
1046
1049
  declare global {
1047
1050
  interface HTMLElementTagNameMap {
1048
- 'gs-mutations-component': MutationsComponent;
1051
+ 'gs-date-range-selector': DateRangeSelectorComponent;
1052
+ }
1053
+ interface HTMLElementEventMap {
1054
+ 'gs-date-range-changed': CustomEvent<Record<string, string>>;
1049
1055
  }
1050
1056
  }
1051
1057
 
@@ -1053,7 +1059,7 @@ declare global {
1053
1059
  declare global {
1054
1060
  namespace JSX {
1055
1061
  interface IntrinsicElements {
1056
- 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1062
+ 'gs-date-range-selector': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1057
1063
  }
1058
1064
  }
1059
1065
  }
@@ -1061,7 +1067,10 @@ declare global {
1061
1067
 
1062
1068
  declare global {
1063
1069
  interface HTMLElementTagNameMap {
1064
- 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
1070
+ 'gs-text-input': TextInputComponent;
1071
+ }
1072
+ interface HTMLElementEventMap {
1073
+ 'gs-text-input-changed': CustomEvent<Record<string, string>>;
1065
1074
  }
1066
1075
  }
1067
1076
 
@@ -1069,7 +1078,7 @@ declare global {
1069
1078
  declare global {
1070
1079
  namespace JSX {
1071
1080
  interface IntrinsicElements {
1072
- 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1081
+ 'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1073
1082
  }
1074
1083
  }
1075
1084
  }
@@ -1077,7 +1086,10 @@ declare global {
1077
1086
 
1078
1087
  declare global {
1079
1088
  interface HTMLElementTagNameMap {
1080
- 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
1089
+ 'gs-mutation-filter': MutationFilterComponent;
1090
+ }
1091
+ interface HTMLElementEventMap {
1092
+ 'gs-mutation-filter-changed': CustomEvent<SelectedMutationFilterStrings>;
1081
1093
  }
1082
1094
  }
1083
1095
 
@@ -1085,7 +1097,7 @@ declare global {
1085
1097
  declare global {
1086
1098
  namespace JSX {
1087
1099
  interface IntrinsicElements {
1088
- 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1100
+ 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1089
1101
  }
1090
1102
  }
1091
1103
  }
@@ -1093,7 +1105,10 @@ declare global {
1093
1105
 
1094
1106
  declare global {
1095
1107
  interface HTMLElementTagNameMap {
1096
- 'gs-aggregate-component': AggregateComponent;
1108
+ 'gs-lineage-filter': LineageFilterComponent;
1109
+ }
1110
+ interface HTMLElementEventMap {
1111
+ 'gs-lineage-filter-changed': CustomEvent<Record<string, string>>;
1097
1112
  }
1098
1113
  }
1099
1114
 
@@ -1101,7 +1116,7 @@ declare global {
1101
1116
  declare global {
1102
1117
  namespace JSX {
1103
1118
  interface IntrinsicElements {
1104
- 'gs-aggregate-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1119
+ 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1105
1120
  }
1106
1121
  }
1107
1122
  }
@@ -1109,7 +1124,7 @@ declare global {
1109
1124
 
1110
1125
  declare global {
1111
1126
  interface HTMLElementTagNameMap {
1112
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1127
+ 'gs-mutation-comparison-component': MutationComparisonComponent;
1113
1128
  }
1114
1129
  }
1115
1130
 
@@ -1117,7 +1132,7 @@ declare global {
1117
1132
  declare global {
1118
1133
  namespace JSX {
1119
1134
  interface IntrinsicElements {
1120
- 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1135
+ 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1121
1136
  }
1122
1137
  }
1123
1138
  }
@@ -1125,7 +1140,7 @@ declare global {
1125
1140
 
1126
1141
  declare global {
1127
1142
  interface HTMLElementTagNameMap {
1128
- 'gs-mutations-over-time': MutationsOverTimeComponent;
1143
+ 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
1129
1144
  }
1130
1145
  }
1131
1146
 
@@ -1133,7 +1148,7 @@ declare global {
1133
1148
  declare global {
1134
1149
  namespace JSX {
1135
1150
  interface IntrinsicElements {
1136
- 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1151
+ 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1137
1152
  }
1138
1153
  }
1139
1154
  }
@@ -1141,10 +1156,7 @@ declare global {
1141
1156
 
1142
1157
  declare global {
1143
1158
  interface HTMLElementTagNameMap {
1144
- 'gs-date-range-selector': DateRangeSelectorComponent;
1145
- }
1146
- interface HTMLElementEventMap {
1147
- 'gs-date-range-changed': CustomEvent<Record<string, string>>;
1159
+ 'gs-mutations-component': MutationsComponent;
1148
1160
  }
1149
1161
  }
1150
1162
 
@@ -1152,7 +1164,7 @@ declare global {
1152
1164
  declare global {
1153
1165
  namespace JSX {
1154
1166
  interface IntrinsicElements {
1155
- 'gs-date-range-selector': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1167
+ 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1156
1168
  }
1157
1169
  }
1158
1170
  }
@@ -1160,10 +1172,7 @@ declare global {
1160
1172
 
1161
1173
  declare global {
1162
1174
  interface HTMLElementTagNameMap {
1163
- 'gs-location-filter': LocationFilterComponent;
1164
- }
1165
- interface HTMLElementEventMap {
1166
- 'gs-location-changed': CustomEvent<Record<string, string>>;
1175
+ 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
1167
1176
  }
1168
1177
  }
1169
1178
 
@@ -1171,7 +1180,7 @@ declare global {
1171
1180
  declare global {
1172
1181
  namespace JSX {
1173
1182
  interface IntrinsicElements {
1174
- 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1183
+ 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1175
1184
  }
1176
1185
  }
1177
1186
  }
@@ -1179,10 +1188,7 @@ declare global {
1179
1188
 
1180
1189
  declare global {
1181
1190
  interface HTMLElementTagNameMap {
1182
- 'gs-text-input': TextInputComponent;
1183
- }
1184
- interface HTMLElementEventMap {
1185
- 'gs-text-input-changed': CustomEvent<Record<string, string>>;
1191
+ 'gs-aggregate-component': AggregateComponent;
1186
1192
  }
1187
1193
  }
1188
1194
 
@@ -1190,7 +1196,7 @@ declare global {
1190
1196
  declare global {
1191
1197
  namespace JSX {
1192
1198
  interface IntrinsicElements {
1193
- 'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1199
+ 'gs-aggregate-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1194
1200
  }
1195
1201
  }
1196
1202
  }
@@ -1198,10 +1204,7 @@ declare global {
1198
1204
 
1199
1205
  declare global {
1200
1206
  interface HTMLElementTagNameMap {
1201
- 'gs-mutation-filter': MutationFilterComponent;
1202
- }
1203
- interface HTMLElementEventMap {
1204
- 'gs-mutation-filter-changed': CustomEvent<SelectedMutationFilterStrings>;
1207
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1205
1208
  }
1206
1209
  }
1207
1210
 
@@ -1209,7 +1212,7 @@ declare global {
1209
1212
  declare global {
1210
1213
  namespace JSX {
1211
1214
  interface IntrinsicElements {
1212
- 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1215
+ 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1213
1216
  }
1214
1217
  }
1215
1218
  }
@@ -1217,10 +1220,7 @@ declare global {
1217
1220
 
1218
1221
  declare global {
1219
1222
  interface HTMLElementTagNameMap {
1220
- 'gs-lineage-filter': LineageFilterComponent;
1221
- }
1222
- interface HTMLElementEventMap {
1223
- 'gs-lineage-filter-changed': CustomEvent<Record<string, string>>;
1223
+ 'gs-mutations-over-time': MutationsOverTimeComponent;
1224
1224
  }
1225
1225
  }
1226
1226
 
@@ -1228,7 +1228,7 @@ declare global {
1228
1228
  declare global {
1229
1229
  namespace JSX {
1230
1230
  interface IntrinsicElements {
1231
- 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1231
+ 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1232
1232
  }
1233
1233
  }
1234
1234
  }
package/dist/style.css CHANGED
@@ -2525,6 +2525,36 @@ input.tab:checked + .tab-content,
2525
2525
  --togglehandleborder: 0 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset,
2526
2526
  var(--handleoffsetcalculator) 0 0 3px var(--fallback-bc,oklch(var(--bc)/1)) inset;
2527
2527
  }
2528
+ .artboard.phone-1.horizontal,
2529
+ .artboard.phone-1.artboard-horizontal {
2530
+ width: 568px;
2531
+ height: 320px;
2532
+ }
2533
+ .artboard.phone-2.horizontal,
2534
+ .artboard.phone-2.artboard-horizontal {
2535
+ width: 667px;
2536
+ height: 375px;
2537
+ }
2538
+ .artboard.phone-3.horizontal,
2539
+ .artboard.phone-3.artboard-horizontal {
2540
+ width: 736px;
2541
+ height: 414px;
2542
+ }
2543
+ .artboard.phone-4.horizontal,
2544
+ .artboard.phone-4.artboard-horizontal {
2545
+ width: 812px;
2546
+ height: 375px;
2547
+ }
2548
+ .artboard.phone-5.horizontal,
2549
+ .artboard.phone-5.artboard-horizontal {
2550
+ width: 896px;
2551
+ height: 414px;
2552
+ }
2553
+ .artboard.phone-6.horizontal,
2554
+ .artboard.phone-6.artboard-horizontal {
2555
+ width: 1024px;
2556
+ height: 320px;
2557
+ }
2528
2558
  .btm-nav-xs > *:where(.active) {
2529
2559
  border-top-width: 1px;
2530
2560
  }
@@ -2937,6 +2967,9 @@ input.tab:checked + .tab-content,
2937
2967
  .m-2 {
2938
2968
  margin: 0.5rem;
2939
2969
  }
2970
+ .m-4 {
2971
+ margin: 1rem;
2972
+ }
2940
2973
  .mx-1 {
2941
2974
  margin-left: 0.25rem;
2942
2975
  margin-right: 0.25rem;
@@ -3320,6 +3353,9 @@ input.tab:checked + .tab-content,
3320
3353
  .mdi--fullscreen-exit {
3321
3354
  --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M14 14h5v2h-3v3h-2zm-9 0h5v5H8v-3H5zm3-9h2v5H5V8h3zm11 3v2h-5V5h2v3z'/%3E%3C/svg%3E");
3322
3355
  }
3356
+ .mdi--reload {
3357
+ --svg: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='black' d='M2 12a9 9 0 0 0 9 9c2.39 0 4.68-.94 6.4-2.6l-1.5-1.5A6.7 6.7 0 0 1 11 19c-6.24 0-9.36-7.54-4.95-11.95S18 5.77 18 12h-3l4 4h.1l3.9-4h-3a9 9 0 0 0-18 0'/%3E%3C/svg%3E");
3358
+ }
3323
3359
  @media (min-width: 640px) {
3324
3360
 
3325
3361
  .sm\:modal-middle {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genspectrum/dashboard-components",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "GenSpectrum web components for building dashboards",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -35,16 +35,18 @@ export class LapisError extends Error {
35
35
  }
36
36
 
37
37
  export async function fetchAggregated(lapisUrl: string, body: LapisBaseRequest, signal?: AbortSignal) {
38
- const response = await fetch(aggregatedEndpoint(lapisUrl), {
39
- method: 'POST',
40
- headers: {
41
- 'Content-Type': 'application/json',
38
+ const response = await callLapis(
39
+ aggregatedEndpoint(lapisUrl),
40
+ {
41
+ method: 'POST',
42
+ headers: {
43
+ 'Content-Type': 'application/json',
44
+ },
45
+ body: JSON.stringify(body),
46
+ signal,
42
47
  },
43
- body: JSON.stringify(body),
44
- signal,
45
- });
46
-
47
- await handleErrors(response, 'aggregated data');
48
+ 'aggregated data',
49
+ );
48
50
 
49
51
  return aggregatedResponse.parse(await response.json());
50
52
  }
@@ -55,16 +57,18 @@ export async function fetchInsertions(
55
57
  sequenceType: SequenceType,
56
58
  signal?: AbortSignal,
57
59
  ) {
58
- const response = await fetch(insertionsEndpoint(lapisUrl, sequenceType), {
59
- method: 'POST',
60
- headers: {
61
- 'Content-Type': 'application/json',
60
+ const response = await callLapis(
61
+ insertionsEndpoint(lapisUrl, sequenceType),
62
+ {
63
+ method: 'POST',
64
+ headers: {
65
+ 'Content-Type': 'application/json',
66
+ },
67
+ body: JSON.stringify(body),
68
+ signal,
62
69
  },
63
- body: JSON.stringify(body),
64
- signal,
65
- });
66
-
67
- await handleErrors(response, `${sequenceType} insertions`);
70
+ `${sequenceType} insertions`,
71
+ );
68
72
 
69
73
  return insertionsResponse.parse(await response.json());
70
74
  }
@@ -75,33 +79,54 @@ export async function fetchSubstitutionsOrDeletions(
75
79
  sequenceType: SequenceType,
76
80
  signal?: AbortSignal,
77
81
  ) {
78
- const response = await fetch(substitutionsOrDeletionsEndpoint(lapisUrl, sequenceType), {
79
- method: 'POST',
80
- headers: {
81
- 'Content-Type': 'application/json',
82
+ const response = await callLapis(
83
+ substitutionsOrDeletionsEndpoint(lapisUrl, sequenceType),
84
+ {
85
+ method: 'POST',
86
+ headers: {
87
+ 'Content-Type': 'application/json',
88
+ },
89
+ body: JSON.stringify(body),
90
+ signal,
82
91
  },
83
- body: JSON.stringify(body),
84
- signal,
85
- });
86
-
87
- await handleErrors(response, `${sequenceType} mutations`);
92
+ `${sequenceType} mutations`,
93
+ );
88
94
 
89
95
  return mutationsResponse.parse(await response.json());
90
96
  }
91
97
 
92
98
  export async function fetchReferenceGenome(lapisUrl: string, signal?: AbortSignal) {
93
- const response = await fetch(referenceGenomeEndpoint(lapisUrl), {
94
- method: 'GET',
95
- headers: {
96
- 'Content-Type': 'application/json',
99
+ const response = await callLapis(
100
+ referenceGenomeEndpoint(lapisUrl),
101
+ {
102
+ method: 'GET',
103
+ headers: {
104
+ 'Content-Type': 'application/json',
105
+ },
106
+ signal,
97
107
  },
98
- signal,
99
- });
108
+ 'the reference genomes',
109
+ );
100
110
 
101
- await handleErrors(response, 'the reference genomes');
102
111
  return referenceGenomeResponse.parse(await response.json());
103
112
  }
104
113
 
114
+ async function callLapis(
115
+ input: Parameters<typeof fetch>[0],
116
+ init: Parameters<typeof fetch>[1],
117
+ requestedDataName: string,
118
+ ) {
119
+ try {
120
+ const response = await fetch(input, init);
121
+
122
+ await handleErrors(response, requestedDataName);
123
+ return response;
124
+ } catch (error) {
125
+ const message = error instanceof Error ? error.message : `${error}`;
126
+ throw new UnknownLapisError(`Failed to connect to LAPIS: ${message}`, 500, requestedDataName);
127
+ }
128
+ }
129
+
105
130
  const handleErrors = async (response: Response, requestedData: string) => {
106
131
  if (!response.ok) {
107
132
  if (response.status >= 400 && response.status < 500) {
@@ -1,4 +1,5 @@
1
1
  import { type Meta, type StoryObj } from '@storybook/preact';
2
+ import { expect, waitFor, within } from '@storybook/test';
2
3
 
3
4
  import aggregatedData from './__mockData__/aggregated.json';
4
5
  import { Aggregate, type AggregateProps } from './aggregate';
@@ -59,3 +60,37 @@ export const Default: StoryObj<AggregateProps> = {
59
60
  pageSize: 10,
60
61
  },
61
62
  };
63
+
64
+ export const FailsLoadingData: StoryObj<AggregateProps> = {
65
+ ...Default,
66
+ parameters: {
67
+ fetchMock: {
68
+ mocks: [
69
+ {
70
+ matcher: {
71
+ name: 'aggregatedData',
72
+ url: AGGREGATED_ENDPOINT,
73
+ },
74
+ response: {
75
+ status: 400,
76
+ body: {
77
+ error: {
78
+ title: 'Bad Request',
79
+ detail: 'Test error',
80
+ status: 400,
81
+ },
82
+ },
83
+ },
84
+ },
85
+ ],
86
+ },
87
+ },
88
+ play: async ({ canvasElement }) => {
89
+ const canvas = within(canvasElement);
90
+
91
+ await waitFor(async () => {
92
+ await expect(canvas.getByText('Error - Failed fetching aggregated data from LAPIS')).toBeInTheDocument();
93
+ await expect(canvas.getByRole('button', { name: 'Try again' })).toBeInTheDocument();
94
+ });
95
+ },
96
+ };
@@ -7,7 +7,6 @@ import { type LapisFilter } from '../../types';
7
7
  import { LapisUrlContext } from '../LapisUrlContext';
8
8
  import { CsvDownloadButton } from '../components/csv-download-button';
9
9
  import { ErrorBoundary } from '../components/error-boundary';
10
- import { ErrorDisplay } from '../components/error-display';
11
10
  import { Fullscreen } from '../components/fullscreen';
12
11
  import Info, { InfoComponentCode, InfoHeadline1, InfoParagraph } from '../components/info';
13
12
  import { LoadingDisplay } from '../components/loading-display';
@@ -56,7 +55,7 @@ export const AggregateInner: FunctionComponent<AggregateProps> = (componentProps
56
55
  }
57
56
 
58
57
  if (error !== null) {
59
- return <ErrorDisplay error={error} />;
58
+ throw error;
60
59
  }
61
60
 
62
61
  if (data === null) {
@@ -1,16 +1,21 @@
1
1
  import type { FunctionComponent } from 'preact';
2
2
  import { useErrorBoundary } from 'preact/hooks';
3
3
 
4
- import { ErrorDisplay } from './error-display';
4
+ import { ErrorDisplay, type ErrorDisplayProps } from './error-display';
5
5
  import { ResizeContainer, type Size } from './resize-container';
6
6
 
7
- export const ErrorBoundary: FunctionComponent<{ size: Size }> = ({ size, children }) => {
8
- const [internalError] = useErrorBoundary();
7
+ type ErrorBoundaryProps = {
8
+ size: Size;
9
+ layout?: ErrorDisplayProps['layout'];
10
+ };
11
+
12
+ export const ErrorBoundary: FunctionComponent<ErrorBoundaryProps> = ({ size, layout, children }) => {
13
+ const [internalError, resetError] = useErrorBoundary();
9
14
 
10
15
  if (internalError) {
11
16
  return (
12
17
  <ResizeContainer size={size}>
13
- <ErrorDisplay error={internalError} />
18
+ <ErrorDisplay error={internalError} resetError={resetError} layout={layout} />
14
19
  </ResizeContainer>
15
20
  );
16
21
  }
@@ -15,7 +15,7 @@ export default meta;
15
15
  export const GenericErrorStory: StoryObj = {
16
16
  render: () => (
17
17
  <ResizeContainer size={{ height: '600px', width: '100%' }}>
18
- <ErrorDisplay error={new Error('some message')} />
18
+ <ErrorDisplay error={new Error('some message')} resetError={() => {}} />
19
19
  </ResizeContainer>
20
20
  ),
21
21
 
@@ -30,7 +30,7 @@ export const GenericErrorStory: StoryObj = {
30
30
  export const UserFacingErrorStory: StoryObj = {
31
31
  render: () => (
32
32
  <ResizeContainer size={{ height: '600px', width: '100%' }}>
33
- <ErrorDisplay error={new UserFacingError('Error Title', 'some message')} />
33
+ <ErrorDisplay error={new UserFacingError('Error Title', 'some message')} resetError={() => {}} />
34
34
  </ResizeContainer>
35
35
  ),
36
36
 
@@ -52,7 +52,7 @@ export const UserFacingErrorStory: StoryObj = {
52
52
  export const FiresEvent: StoryObj = {
53
53
  render: () => (
54
54
  <ResizeContainer size={{ height: '600px', width: '100%' }}>
55
- <ErrorDisplay error={new UserFacingError('Error Title', 'some message')} />
55
+ <ErrorDisplay error={new UserFacingError('Error Title', 'some message')} resetError={() => {}} />
56
56
  </ResizeContainer>
57
57
  ),
58
58
 
@@ -66,3 +66,23 @@ export const FiresEvent: StoryObj = {
66
66
  });
67
67
  },
68
68
  };
69
+
70
+ const resetErrorMock = fn();
71
+
72
+ export const TriggersResetErrorOnReloadButton: StoryObj = {
73
+ render: () => (
74
+ <ResizeContainer size={{ height: '600px', width: '100%' }}>
75
+ <ErrorDisplay error={new UserFacingError('Error Title', 'some message')} resetError={resetErrorMock} />
76
+ </ResizeContainer>
77
+ ),
78
+
79
+ play: async ({ canvasElement }) => {
80
+ const canvas = within(canvasElement);
81
+
82
+ await userEvent.click(canvas.getByText('Try again'));
83
+
84
+ await waitFor(() => {
85
+ expect(resetErrorMock).toHaveBeenCalled();
86
+ });
87
+ },
88
+ };
@@ -24,7 +24,13 @@ export class UserFacingError extends Error {
24
24
  }
25
25
  }
26
26
 
27
- export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) => {
27
+ export type ErrorDisplayProps = {
28
+ error: Error;
29
+ resetError: () => void;
30
+ layout?: 'horizontal' | 'vertical';
31
+ };
32
+
33
+ export const ErrorDisplay: FunctionComponent<ErrorDisplayProps> = ({ error, resetError, layout }) => {
28
34
  // eslint-disable-next-line no-console -- Currently we use the following statement for our error handling
29
35
  console.error(error);
30
36
 
@@ -40,34 +46,40 @@ export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) =>
40
46
  return (
41
47
  <div
42
48
  ref={containerRef}
43
- className='h-full w-full rounded-md border-2 border-gray-100 p-2 flex items-center justify-center flex-col'
49
+ className={`h-full w-full rounded-md border-2 border-gray-100 p-2 flex items-center justify-center ${layout === 'horizontal' ? 'flex-row' : 'flex-col'}`}
44
50
  >
45
- <div className='text-red-700 font-bold'>{headline}</div>
46
51
  <div>
47
- Oops! Something went wrong.
48
- {details !== undefined && (
49
- <>
50
- {' '}
51
- <button className='underline hover:text-gray-400' onClick={() => ref.current?.showModal()}>
52
- Show details.
53
- </button>
54
- <dialog ref={ref} class='modal'>
55
- <div class='modal-box'>
56
- <form method='dialog'>
57
- <button className='btn btn-sm btn-circle btn-ghost absolute right-2 top-2'>
58
-
59
- </button>
52
+ <div className='text-red-700 font-bold'>{headline}</div>
53
+ <div>
54
+ Oops! Something went wrong.
55
+ {details !== undefined && (
56
+ <>
57
+ {' '}
58
+ <button className='underline hover:text-gray-400' onClick={() => ref.current?.showModal()}>
59
+ Show details.
60
+ </button>
61
+ <dialog ref={ref} class='modal'>
62
+ <div class='modal-box'>
63
+ <form method='dialog'>
64
+ <button className='btn btn-sm btn-circle btn-ghost absolute right-2 top-2'>
65
+
66
+ </button>
67
+ </form>
68
+ <h1 class='text-lg'>{details.headline}</h1>
69
+ <p class='py-4'>{details.message}</p>
70
+ </div>
71
+ <form method='dialog' class='modal-backdrop'>
72
+ <button>close</button>
60
73
  </form>
61
- <h1 class='text-lg'>{details.headline}</h1>
62
- <p class='py-4'>{details.message}</p>
63
- </div>
64
- <form method='dialog' class='modal-backdrop'>
65
- <button>close</button>
66
- </form>
67
- </dialog>
68
- </>
69
- )}
74
+ </dialog>
75
+ </>
76
+ )}
77
+ </div>
70
78
  </div>
79
+ <button onClick={resetError} className='btn btn-sm flex items-center m-4'>
80
+ <span className='iconify mdi--reload text-lg' />
81
+ Try again
82
+ </button>
71
83
  </div>
72
84
  );
73
85
  };
@@ -29,7 +29,7 @@ export const DateRangeSelector = ({ width, ...innerProps }: DateRangeSelectorPro
29
29
  const size = { width, height: '3rem' };
30
30
 
31
31
  return (
32
- <ErrorBoundary size={size}>
32
+ <ErrorBoundary size={size} layout='horizontal'>
33
33
  <div style={{ width }}>
34
34
  <DateRangeSelectorInner {...innerProps} />
35
35
  </div>