@genspectrum/dashboard-components 0.7.2 → 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 (31) hide show
  1. package/custom-elements.json +1 -24
  2. package/dist/assets/mutationOverTimeWorker-ICjqmm9j.js.map +1 -0
  3. package/dist/dashboard-components.js +143 -90
  4. package/dist/dashboard-components.js.map +1 -1
  5. package/dist/genspectrum-components.d.ts +39 -50
  6. package/dist/style.css +36 -0
  7. package/package.json +1 -1
  8. package/src/lapisApi/lapisApi.ts +59 -34
  9. package/src/preact/aggregatedData/aggregate.stories.tsx +35 -0
  10. package/src/preact/aggregatedData/aggregate.tsx +1 -2
  11. package/src/preact/components/error-boundary.tsx +9 -4
  12. package/src/preact/components/error-display.stories.tsx +23 -3
  13. package/src/preact/components/error-display.tsx +37 -25
  14. package/src/preact/dateRangeSelector/date-range-selector.tsx +1 -1
  15. package/src/preact/lineageFilter/lineage-filter.tsx +2 -3
  16. package/src/preact/locationFilter/location-filter.tsx +2 -3
  17. package/src/preact/mutationComparison/mutation-comparison.tsx +1 -2
  18. package/src/preact/mutationFilter/mutation-filter.stories.tsx +5 -34
  19. package/src/preact/mutationFilter/mutation-filter.tsx +1 -14
  20. package/src/preact/mutations/mutations.tsx +1 -2
  21. package/src/preact/mutationsOverTime/mutations-over-time.tsx +11 -6
  22. package/src/preact/numberSequencesOverTime/number-sequences-over-time.tsx +1 -2
  23. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +1 -2
  24. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +1 -2
  25. package/src/preact/textInput/text-input.tsx +2 -3
  26. package/src/preact/webWorkers/useWebWorker.ts +4 -8
  27. package/src/web-components/input/gs-mutation-filter.stories.ts +1 -27
  28. package/src/web-components/input/gs-mutation-filter.tsx +0 -11
  29. package/standalone-bundle/dashboard-components.js +4152 -4068
  30. package/standalone-bundle/dashboard-components.js.map +1 -1
  31. package/dist/assets/mutationOverTimeWorker-BOCXtKzd.js.map +0 -1
@@ -450,16 +450,6 @@ export declare class MutationComparisonComponent extends PreactLitAdapterWithGri
450
450
  * Fired when:
451
451
  * - The user has submitted a valid mutation or insertion
452
452
  * - The user has removed a mutation or insertion
453
- *
454
- * @fires {CustomEvent<{
455
- * nucleotideMutations: string[],
456
- * aminoAcidMutations: string[],
457
- * nucleotideInsertions: string[],
458
- * aminoAcidInsertions: string[]
459
- * }>} gs-mutation-filter-on-blur
460
- * Fired when:
461
- * - the mutation filter has lost focus
462
- * Contains the selected mutations in the format
463
453
  */
464
454
  export declare class MutationFilterComponent extends PreactLitAdapter {
465
455
  /**
@@ -1039,7 +1029,10 @@ declare global {
1039
1029
 
1040
1030
  declare global {
1041
1031
  interface HTMLElementTagNameMap {
1042
- 'gs-mutation-comparison-component': MutationComparisonComponent;
1032
+ 'gs-location-filter': LocationFilterComponent;
1033
+ }
1034
+ interface HTMLElementEventMap {
1035
+ 'gs-location-changed': CustomEvent<Record<string, string>>;
1043
1036
  }
1044
1037
  }
1045
1038
 
@@ -1047,7 +1040,7 @@ declare global {
1047
1040
  declare global {
1048
1041
  namespace JSX {
1049
1042
  interface IntrinsicElements {
1050
- 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1043
+ 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1051
1044
  }
1052
1045
  }
1053
1046
  }
@@ -1055,7 +1048,10 @@ declare global {
1055
1048
 
1056
1049
  declare global {
1057
1050
  interface HTMLElementTagNameMap {
1058
- 'gs-mutations-component': MutationsComponent;
1051
+ 'gs-date-range-selector': DateRangeSelectorComponent;
1052
+ }
1053
+ interface HTMLElementEventMap {
1054
+ 'gs-date-range-changed': CustomEvent<Record<string, string>>;
1059
1055
  }
1060
1056
  }
1061
1057
 
@@ -1063,7 +1059,7 @@ declare global {
1063
1059
  declare global {
1064
1060
  namespace JSX {
1065
1061
  interface IntrinsicElements {
1066
- 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1062
+ 'gs-date-range-selector': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1067
1063
  }
1068
1064
  }
1069
1065
  }
@@ -1071,7 +1067,10 @@ declare global {
1071
1067
 
1072
1068
  declare global {
1073
1069
  interface HTMLElementTagNameMap {
1074
- 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
1070
+ 'gs-text-input': TextInputComponent;
1071
+ }
1072
+ interface HTMLElementEventMap {
1073
+ 'gs-text-input-changed': CustomEvent<Record<string, string>>;
1075
1074
  }
1076
1075
  }
1077
1076
 
@@ -1079,7 +1078,7 @@ declare global {
1079
1078
  declare global {
1080
1079
  namespace JSX {
1081
1080
  interface IntrinsicElements {
1082
- 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1081
+ 'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1083
1082
  }
1084
1083
  }
1085
1084
  }
@@ -1087,7 +1086,10 @@ declare global {
1087
1086
 
1088
1087
  declare global {
1089
1088
  interface HTMLElementTagNameMap {
1090
- 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
1089
+ 'gs-mutation-filter': MutationFilterComponent;
1090
+ }
1091
+ interface HTMLElementEventMap {
1092
+ 'gs-mutation-filter-changed': CustomEvent<SelectedMutationFilterStrings>;
1091
1093
  }
1092
1094
  }
1093
1095
 
@@ -1095,7 +1097,7 @@ declare global {
1095
1097
  declare global {
1096
1098
  namespace JSX {
1097
1099
  interface IntrinsicElements {
1098
- 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1100
+ 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1099
1101
  }
1100
1102
  }
1101
1103
  }
@@ -1103,7 +1105,10 @@ declare global {
1103
1105
 
1104
1106
  declare global {
1105
1107
  interface HTMLElementTagNameMap {
1106
- 'gs-aggregate-component': AggregateComponent;
1108
+ 'gs-lineage-filter': LineageFilterComponent;
1109
+ }
1110
+ interface HTMLElementEventMap {
1111
+ 'gs-lineage-filter-changed': CustomEvent<Record<string, string>>;
1107
1112
  }
1108
1113
  }
1109
1114
 
@@ -1111,7 +1116,7 @@ declare global {
1111
1116
  declare global {
1112
1117
  namespace JSX {
1113
1118
  interface IntrinsicElements {
1114
- 'gs-aggregate-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1119
+ 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1115
1120
  }
1116
1121
  }
1117
1122
  }
@@ -1119,7 +1124,7 @@ declare global {
1119
1124
 
1120
1125
  declare global {
1121
1126
  interface HTMLElementTagNameMap {
1122
- 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1127
+ 'gs-mutation-comparison-component': MutationComparisonComponent;
1123
1128
  }
1124
1129
  }
1125
1130
 
@@ -1127,7 +1132,7 @@ declare global {
1127
1132
  declare global {
1128
1133
  namespace JSX {
1129
1134
  interface IntrinsicElements {
1130
- 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1135
+ 'gs-mutation-comparison-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1131
1136
  }
1132
1137
  }
1133
1138
  }
@@ -1135,7 +1140,7 @@ declare global {
1135
1140
 
1136
1141
  declare global {
1137
1142
  interface HTMLElementTagNameMap {
1138
- 'gs-mutations-over-time': MutationsOverTimeComponent;
1143
+ 'gs-prevalence-over-time': PrevalenceOverTimeComponent;
1139
1144
  }
1140
1145
  }
1141
1146
 
@@ -1143,7 +1148,7 @@ declare global {
1143
1148
  declare global {
1144
1149
  namespace JSX {
1145
1150
  interface IntrinsicElements {
1146
- 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1151
+ 'gs-prevalence-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1147
1152
  }
1148
1153
  }
1149
1154
  }
@@ -1151,10 +1156,7 @@ declare global {
1151
1156
 
1152
1157
  declare global {
1153
1158
  interface HTMLElementTagNameMap {
1154
- 'gs-date-range-selector': DateRangeSelectorComponent;
1155
- }
1156
- interface HTMLElementEventMap {
1157
- 'gs-date-range-changed': CustomEvent<Record<string, string>>;
1159
+ 'gs-mutations-component': MutationsComponent;
1158
1160
  }
1159
1161
  }
1160
1162
 
@@ -1162,7 +1164,7 @@ declare global {
1162
1164
  declare global {
1163
1165
  namespace JSX {
1164
1166
  interface IntrinsicElements {
1165
- 'gs-date-range-selector': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1167
+ 'gs-mutations-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1166
1168
  }
1167
1169
  }
1168
1170
  }
@@ -1170,10 +1172,7 @@ declare global {
1170
1172
 
1171
1173
  declare global {
1172
1174
  interface HTMLElementTagNameMap {
1173
- 'gs-location-filter': LocationFilterComponent;
1174
- }
1175
- interface HTMLElementEventMap {
1176
- 'gs-location-changed': CustomEvent<Record<string, string>>;
1175
+ 'gs-relative-growth-advantage': RelativeGrowthAdvantageComponent;
1177
1176
  }
1178
1177
  }
1179
1178
 
@@ -1181,7 +1180,7 @@ declare global {
1181
1180
  declare global {
1182
1181
  namespace JSX {
1183
1182
  interface IntrinsicElements {
1184
- 'gs-location-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1183
+ 'gs-relative-growth-advantage': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1185
1184
  }
1186
1185
  }
1187
1186
  }
@@ -1189,10 +1188,7 @@ declare global {
1189
1188
 
1190
1189
  declare global {
1191
1190
  interface HTMLElementTagNameMap {
1192
- 'gs-text-input': TextInputComponent;
1193
- }
1194
- interface HTMLElementEventMap {
1195
- 'gs-text-input-changed': CustomEvent<Record<string, string>>;
1191
+ 'gs-aggregate-component': AggregateComponent;
1196
1192
  }
1197
1193
  }
1198
1194
 
@@ -1200,7 +1196,7 @@ declare global {
1200
1196
  declare global {
1201
1197
  namespace JSX {
1202
1198
  interface IntrinsicElements {
1203
- 'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1199
+ 'gs-aggregate-component': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1204
1200
  }
1205
1201
  }
1206
1202
  }
@@ -1208,11 +1204,7 @@ declare global {
1208
1204
 
1209
1205
  declare global {
1210
1206
  interface HTMLElementTagNameMap {
1211
- 'gs-mutation-filter': MutationFilterComponent;
1212
- }
1213
- interface HTMLElementEventMap {
1214
- 'gs-mutation-filter-changed': CustomEvent<SelectedMutationFilterStrings>;
1215
- 'gs-mutation-filter-on-blur': CustomEvent<SelectedMutationFilterStrings>;
1207
+ 'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
1216
1208
  }
1217
1209
  }
1218
1210
 
@@ -1220,7 +1212,7 @@ declare global {
1220
1212
  declare global {
1221
1213
  namespace JSX {
1222
1214
  interface IntrinsicElements {
1223
- 'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1215
+ 'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1224
1216
  }
1225
1217
  }
1226
1218
  }
@@ -1228,10 +1220,7 @@ declare global {
1228
1220
 
1229
1221
  declare global {
1230
1222
  interface HTMLElementTagNameMap {
1231
- 'gs-lineage-filter': LineageFilterComponent;
1232
- }
1233
- interface HTMLElementEventMap {
1234
- 'gs-lineage-filter-changed': CustomEvent<Record<string, string>>;
1223
+ 'gs-mutations-over-time': MutationsOverTimeComponent;
1235
1224
  }
1236
1225
  }
1237
1226
 
@@ -1239,7 +1228,7 @@ declare global {
1239
1228
  declare global {
1240
1229
  namespace JSX {
1241
1230
  interface IntrinsicElements {
1242
- 'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1231
+ 'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
1243
1232
  }
1244
1233
  }
1245
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.7.2",
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>