@genspectrum/dashboard-components 0.6.13 → 0.6.15
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.
- package/README.md +29 -0
- package/custom-elements.json +23 -17
- package/dist/dashboard-components.js +8237 -37898
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +26 -8
- package/dist/style.css +3 -3
- package/package.json +9 -8
- package/src/index.ts +8 -0
- package/src/lapisApi/lapisApi.ts +15 -7
- package/src/operator/FetchAggregatedOperator.ts +2 -7
- package/src/preact/components/color-scale-selector-dropdown.stories.tsx +1 -1
- package/src/preact/components/error-boundary.stories.tsx +21 -4
- package/src/preact/components/error-display.stories.tsx +20 -2
- package/src/preact/components/error-display.tsx +64 -10
- package/src/preact/components/info.stories.tsx +5 -5
- package/src/preact/components/info.tsx +2 -4
- package/src/preact/components/percent-intput.tsx +7 -2
- package/src/preact/components/proportion-selector.tsx +12 -2
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +2 -3
- package/src/preact/dateRangeSelector/date-range-selector.tsx +4 -4
- package/src/preact/lineageFilter/lineage-filter.stories.tsx +2 -3
- package/src/preact/lineageFilter/lineage-filter.tsx +2 -2
- package/src/preact/locationFilter/fetchAutocompletionList.ts +1 -14
- package/src/preact/locationFilter/location-filter.stories.tsx +2 -3
- package/src/preact/locationFilter/location-filter.tsx +2 -2
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +2 -3
- package/src/preact/mutationsOverTime/__mockData__/aggregated_2024_01.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_2024_02.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_2024_03.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_2024_04.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_2024_05.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_2024_06.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_2024_07.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_20_01_2024.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_21_01_2024.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_22_01_2024.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_23_01_2024.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_24_01_2024.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_25_01_2024.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_26_01_2024.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_tooManyMutations_total.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_week3_2024.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_week4_2024.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_week5_2024.json +13 -0
- package/src/preact/mutationsOverTime/__mockData__/aggregated_week6_2024.json +13 -0
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +56 -8
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +4 -2
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +135 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +3 -3
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +1 -1
- package/src/preact/shared/floating-ui/hooks.ts +1 -1
- package/src/preact/textInput/text-input.stories.tsx +2 -3
- package/src/preact/textInput/text-input.tsx +2 -2
- package/src/query/queryMutationsOverTime.spec.ts +210 -64
- package/src/query/queryMutationsOverTime.ts +10 -2
- package/src/web-components/app.stories.ts +0 -2
- package/src/web-components/errorHandling.mdx +8 -0
- package/src/web-components/input/gs-date-range-selector.stories.ts +2 -3
- package/src/web-components/input/gs-date-range-selector.tsx +24 -4
- package/src/web-components/input/gs-lineage-filter.stories.ts +2 -3
- package/src/web-components/input/gs-lineage-filter.tsx +15 -1
- package/src/web-components/input/gs-location-filter.stories.ts +2 -3
- package/src/web-components/input/gs-location-filter.tsx +13 -1
- package/src/web-components/input/gs-mutation-filter.stories.ts +2 -3
- package/src/web-components/input/gs-mutation-filter.tsx +1 -0
- package/src/web-components/input/gs-text-input.stories.ts +2 -3
- package/src/web-components/input/gs-text-input.tsx +13 -1
- package/src/web-components/visualization/gs-aggregate.tsx +17 -1
- package/src/web-components/visualization/gs-mutation-comparison.tsx +9 -0
- package/src/web-components/visualization/gs-mutations-over-time.stories.ts +271 -0
- package/src/web-components/visualization/gs-mutations-over-time.tsx +7 -0
- package/src/web-components/visualization/gs-mutations.tsx +11 -5
- package/src/web-components/visualization/gs-number-sequences-over-time.tsx +15 -0
- package/src/web-components/visualization/gs-prevalence-over-time.stories.ts +8 -9
- package/src/web-components/visualization/gs-prevalence-over-time.tsx +26 -8
- package/src/web-components/visualization/gs-relative-growth-advantage.tsx +43 -5
- package/standalone-bundle/dashboard-components.js +30920 -0
- package/standalone-bundle/dashboard-components.js.map +1 -0
|
@@ -178,6 +178,12 @@ export declare class DateRangeSelectorComponent extends PreactLitAdapter {
|
|
|
178
178
|
render(): JSX_2.Element;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
declare class ErrorEvent_2 extends Event {
|
|
182
|
+
readonly error: Error;
|
|
183
|
+
constructor(error: Error);
|
|
184
|
+
}
|
|
185
|
+
export { ErrorEvent_2 as ErrorEvent }
|
|
186
|
+
|
|
181
187
|
declare type LapisFilter = Record<string, string | number | null | boolean>;
|
|
182
188
|
|
|
183
189
|
/**
|
|
@@ -948,11 +954,23 @@ export declare class TextInputComponent extends PreactLitAdapter {
|
|
|
948
954
|
render(): JSX_2.Element;
|
|
949
955
|
}
|
|
950
956
|
|
|
957
|
+
export declare class UserFacingError extends Error {
|
|
958
|
+
readonly headline: string;
|
|
959
|
+
constructor(headline: string, message: string);
|
|
960
|
+
}
|
|
961
|
+
|
|
951
962
|
declare type View = 'table';
|
|
952
963
|
|
|
953
964
|
export { }
|
|
954
965
|
|
|
955
966
|
|
|
967
|
+
declare global {
|
|
968
|
+
interface HTMLElementEventMap {
|
|
969
|
+
'gs-error': ErrorEvent;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
|
|
956
974
|
declare global {
|
|
957
975
|
interface HTMLElementTagNameMap {
|
|
958
976
|
'gs-app': App;
|
|
@@ -990,21 +1008,21 @@ declare global {
|
|
|
990
1008
|
|
|
991
1009
|
declare global {
|
|
992
1010
|
interface HTMLElementTagNameMap {
|
|
993
|
-
'gs-
|
|
1011
|
+
'gs-aggregate-component': AggregateComponent;
|
|
994
1012
|
}
|
|
995
1013
|
}
|
|
996
1014
|
|
|
997
1015
|
|
|
998
1016
|
declare global {
|
|
999
1017
|
interface HTMLElementTagNameMap {
|
|
1000
|
-
'gs-
|
|
1018
|
+
'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
|
|
1001
1019
|
}
|
|
1002
1020
|
}
|
|
1003
1021
|
|
|
1004
1022
|
|
|
1005
1023
|
declare global {
|
|
1006
1024
|
interface HTMLElementTagNameMap {
|
|
1007
|
-
'gs-
|
|
1025
|
+
'gs-mutations-over-time-component': MutationsOverTimeComponent;
|
|
1008
1026
|
}
|
|
1009
1027
|
}
|
|
1010
1028
|
|
|
@@ -1041,21 +1059,21 @@ declare global {
|
|
|
1041
1059
|
|
|
1042
1060
|
declare global {
|
|
1043
1061
|
interface HTMLElementTagNameMap {
|
|
1044
|
-
'gs-
|
|
1062
|
+
'gs-lineage-filter': LineageFilterComponent;
|
|
1045
1063
|
}
|
|
1046
1064
|
interface HTMLElementEventMap {
|
|
1047
|
-
'gs-
|
|
1048
|
-
'gs-mutation-filter-on-blur': CustomEvent<SelectedMutationFilterStrings>;
|
|
1065
|
+
'gs-lineage-filter-changed': CustomEvent<Record<string, string>>;
|
|
1049
1066
|
}
|
|
1050
1067
|
}
|
|
1051
1068
|
|
|
1052
1069
|
|
|
1053
1070
|
declare global {
|
|
1054
1071
|
interface HTMLElementTagNameMap {
|
|
1055
|
-
'gs-
|
|
1072
|
+
'gs-mutation-filter': MutationFilterComponent;
|
|
1056
1073
|
}
|
|
1057
1074
|
interface HTMLElementEventMap {
|
|
1058
|
-
'gs-
|
|
1075
|
+
'gs-mutation-filter-changed': CustomEvent<SelectedMutationFilterStrings>;
|
|
1076
|
+
'gs-mutation-filter-on-blur': CustomEvent<SelectedMutationFilterStrings>;
|
|
1059
1077
|
}
|
|
1060
1078
|
}
|
|
1061
1079
|
|
package/dist/style.css
CHANGED
|
@@ -376,7 +376,7 @@ input[type="range"] {
|
|
|
376
376
|
background-color: #C6C6C6;
|
|
377
377
|
pointer-events: none;
|
|
378
378
|
}/*
|
|
379
|
-
! tailwindcss v3.4.
|
|
379
|
+
! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com
|
|
380
380
|
*//*
|
|
381
381
|
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
|
|
382
382
|
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
|
|
@@ -3372,9 +3372,9 @@ input.tab:checked + .tab-content,
|
|
|
3372
3372
|
--tw-text-opacity: 1;
|
|
3373
3373
|
color: rgb(30 64 175 / var(--tw-text-opacity));
|
|
3374
3374
|
}
|
|
3375
|
-
.hover\:text-gray-
|
|
3375
|
+
.hover\:text-gray-400:hover {
|
|
3376
3376
|
--tw-text-opacity: 1;
|
|
3377
|
-
color: rgb(
|
|
3377
|
+
color: rgb(156 163 175 / var(--tw-text-opacity));
|
|
3378
3378
|
}
|
|
3379
3379
|
.hover\:text-gray-700:hover {
|
|
3380
3380
|
--tw-text-opacity: 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genspectrum/dashboard-components",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.15",
|
|
4
4
|
"description": "GenSpectrum web components for building dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -16,18 +16,19 @@
|
|
|
16
16
|
"require": "./dist/dashboard-components.js",
|
|
17
17
|
"types": "./dist/genspectrum-components.d.ts"
|
|
18
18
|
},
|
|
19
|
-
"./custom-elements.json": "./custom
|
|
19
|
+
"./custom-elements.json": "./custom-elements.json",
|
|
20
20
|
"./package.json": "./package.json",
|
|
21
21
|
"./style.css": "./dist/style.css"
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|
|
25
|
+
"standalone-bundle",
|
|
25
26
|
"src",
|
|
26
27
|
"custom-elements.json",
|
|
27
28
|
"package.json"
|
|
28
29
|
],
|
|
29
30
|
"scripts": {
|
|
30
|
-
"build": "vite --config vite.release.config.ts build && npm run generate-manifest",
|
|
31
|
+
"build": "vite --config vite.release.config.ts build && npm run generate-manifest && vite --config vite.release-standalone.config.ts build",
|
|
31
32
|
"build-and-pack": "npm run build && npm pack | xargs -I {} cp {} genspectrum-dashboard-components.tgz",
|
|
32
33
|
"test": "vitest",
|
|
33
34
|
"lint": "npm run lint:lit-analyzer && npm run lint:eslint",
|
|
@@ -67,9 +68,6 @@
|
|
|
67
68
|
"dayjs": "^1.11.10",
|
|
68
69
|
"flatpickr": "^4.6.13",
|
|
69
70
|
"gridjs": "^6.2.0",
|
|
70
|
-
"@iconify-json/mdi": "^1.1.67",
|
|
71
|
-
"@iconify-json/mdi-light": "^1.1.10",
|
|
72
|
-
"@iconify/tailwind": "^1.1.1",
|
|
73
71
|
"lit": "^3.1.3",
|
|
74
72
|
"object-hash": "^3.0.0",
|
|
75
73
|
"preact": "^10.20.1",
|
|
@@ -77,6 +75,9 @@
|
|
|
77
75
|
},
|
|
78
76
|
"devDependencies": {
|
|
79
77
|
"@custom-elements-manifest/analyzer": "^0.10.2",
|
|
78
|
+
"@iconify-json/mdi": "^1.1.67",
|
|
79
|
+
"@iconify-json/mdi-light": "^1.1.10",
|
|
80
|
+
"@iconify/tailwind": "^1.1.2",
|
|
80
81
|
"@playwright/test": "^1.43.1",
|
|
81
82
|
"@storybook/addon-actions": "^8.0.9",
|
|
82
83
|
"@storybook/addon-essentials": "^8.0.9",
|
|
@@ -92,8 +93,8 @@
|
|
|
92
93
|
"@storybook/web-components-vite": "^8.0.9",
|
|
93
94
|
"@types/node": "^22.0.0",
|
|
94
95
|
"@types/object-hash": "^3.0.6",
|
|
95
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
96
|
-
"@typescript-eslint/parser": "^
|
|
96
|
+
"@typescript-eslint/eslint-plugin": "^8.2.0",
|
|
97
|
+
"@typescript-eslint/parser": "^8.2.0",
|
|
97
98
|
"autoprefixer": "^10.4.19",
|
|
98
99
|
"daisyui": "^4.10.2",
|
|
99
100
|
"depcheck": "^1.4.7",
|
package/src/index.ts
CHANGED
package/src/lapisApi/lapisApi.ts
CHANGED
|
@@ -15,6 +15,7 @@ export class UnknownLapisError extends Error {
|
|
|
15
15
|
constructor(
|
|
16
16
|
message: string,
|
|
17
17
|
public readonly status: number,
|
|
18
|
+
public readonly requestedData: string,
|
|
18
19
|
) {
|
|
19
20
|
super(message);
|
|
20
21
|
this.name = 'UnknownLapisError';
|
|
@@ -26,6 +27,7 @@ export class LapisError extends Error {
|
|
|
26
27
|
message: string,
|
|
27
28
|
public readonly status: number,
|
|
28
29
|
public readonly problemDetail: ProblemDetail,
|
|
30
|
+
public readonly requestedData: string,
|
|
29
31
|
) {
|
|
30
32
|
super(message);
|
|
31
33
|
this.name = 'LapisError';
|
|
@@ -42,7 +44,7 @@ export async function fetchAggregated(lapisUrl: string, body: LapisBaseRequest,
|
|
|
42
44
|
signal,
|
|
43
45
|
});
|
|
44
46
|
|
|
45
|
-
await handleErrors(response);
|
|
47
|
+
await handleErrors(response, 'aggregated data');
|
|
46
48
|
|
|
47
49
|
return aggregatedResponse.parse(await response.json());
|
|
48
50
|
}
|
|
@@ -62,7 +64,7 @@ export async function fetchInsertions(
|
|
|
62
64
|
signal,
|
|
63
65
|
});
|
|
64
66
|
|
|
65
|
-
await handleErrors(response);
|
|
67
|
+
await handleErrors(response, `${sequenceType} insertions`);
|
|
66
68
|
|
|
67
69
|
return insertionsResponse.parse(await response.json());
|
|
68
70
|
}
|
|
@@ -82,7 +84,7 @@ export async function fetchSubstitutionsOrDeletions(
|
|
|
82
84
|
signal,
|
|
83
85
|
});
|
|
84
86
|
|
|
85
|
-
await handleErrors(response);
|
|
87
|
+
await handleErrors(response, `${sequenceType} mutations`);
|
|
86
88
|
|
|
87
89
|
return mutationsResponse.parse(await response.json());
|
|
88
90
|
}
|
|
@@ -96,11 +98,11 @@ export async function fetchReferenceGenome(lapisUrl: string, signal?: AbortSigna
|
|
|
96
98
|
signal,
|
|
97
99
|
});
|
|
98
100
|
|
|
99
|
-
await handleErrors(response);
|
|
101
|
+
await handleErrors(response, 'the reference genomes');
|
|
100
102
|
return referenceGenomeResponse.parse(await response.json());
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
const handleErrors = async (response: Response) => {
|
|
105
|
+
const handleErrors = async (response: Response, requestedData: string) => {
|
|
104
106
|
if (!response.ok) {
|
|
105
107
|
if (response.status >= 400 && response.status < 500) {
|
|
106
108
|
const json = await response.json();
|
|
@@ -111,6 +113,7 @@ const handleErrors = async (response: Response) => {
|
|
|
111
113
|
response.statusText + lapisErrorResult.data.error.detail,
|
|
112
114
|
response.status,
|
|
113
115
|
lapisErrorResult.data.error,
|
|
116
|
+
requestedData,
|
|
114
117
|
);
|
|
115
118
|
}
|
|
116
119
|
|
|
@@ -120,12 +123,17 @@ const handleErrors = async (response: Response) => {
|
|
|
120
123
|
response.statusText + problemDetailResult.data.detail,
|
|
121
124
|
response.status,
|
|
122
125
|
problemDetailResult.data,
|
|
126
|
+
requestedData,
|
|
123
127
|
);
|
|
124
128
|
}
|
|
125
129
|
|
|
126
|
-
throw new UnknownLapisError(
|
|
130
|
+
throw new UnknownLapisError(
|
|
131
|
+
`${response.statusText}: ${JSON.stringify(json)}`,
|
|
132
|
+
response.status,
|
|
133
|
+
requestedData,
|
|
134
|
+
);
|
|
127
135
|
}
|
|
128
|
-
throw new UnknownLapisError(`${response.statusText}: ${response.status}`, response.status);
|
|
136
|
+
throw new UnknownLapisError(`${response.statusText}: ${response.status}`, response.status, requestedData);
|
|
129
137
|
}
|
|
130
138
|
};
|
|
131
139
|
|
|
@@ -5,16 +5,11 @@ import { type AggregatedItem } from '../lapisApi/lapisTypes';
|
|
|
5
5
|
import { type LapisFilter } from '../types';
|
|
6
6
|
|
|
7
7
|
export class FetchAggregatedOperator<Fields extends Record<string, unknown>>
|
|
8
|
-
implements
|
|
9
|
-
Operator<
|
|
10
|
-
Fields & {
|
|
11
|
-
count: number;
|
|
12
|
-
}
|
|
13
|
-
>
|
|
8
|
+
implements Operator<Fields & { count: number }>
|
|
14
9
|
{
|
|
15
10
|
constructor(
|
|
16
11
|
private filter: LapisFilter,
|
|
17
|
-
private fields: string[],
|
|
12
|
+
private fields: string[] = [],
|
|
18
13
|
) {}
|
|
19
14
|
|
|
20
15
|
async evaluate(lapisUrl: string, signal?: AbortSignal): Promise<Dataset<Fields & { count: number }>> {
|
|
@@ -14,7 +14,7 @@ const meta: Meta<ColorScaleSelectorDropdownProps> = {
|
|
|
14
14
|
|
|
15
15
|
export default meta;
|
|
16
16
|
|
|
17
|
-
const WrapperWithState: FunctionComponent
|
|
17
|
+
const WrapperWithState: FunctionComponent = () => {
|
|
18
18
|
const [colorScale, setColorScale] = useState<ColorScale>({ min: 0, max: 1, color: 'indigo' });
|
|
19
19
|
|
|
20
20
|
return <ColorScaleSelectorDropdown colorScale={colorScale} setColorScale={setColorScale} />;
|
|
@@ -2,11 +2,14 @@ import { type Meta, type StoryObj } from '@storybook/preact';
|
|
|
2
2
|
import { expect, waitFor, within } from '@storybook/test';
|
|
3
3
|
|
|
4
4
|
import { ErrorBoundary } from './error-boundary';
|
|
5
|
+
import { UserFacingError } from './error-display';
|
|
5
6
|
|
|
6
7
|
const meta: Meta = {
|
|
7
8
|
title: 'Component/Error boundary',
|
|
8
9
|
component: ErrorBoundary,
|
|
9
|
-
parameters: {
|
|
10
|
+
parameters: {
|
|
11
|
+
fetchMock: {},
|
|
12
|
+
},
|
|
10
13
|
argTypes: {
|
|
11
14
|
size: { control: 'object' },
|
|
12
15
|
defaultSize: { control: 'object' },
|
|
@@ -34,7 +37,7 @@ export const ErrorBoundaryWithoutErrorStory: StoryObj = {
|
|
|
34
37
|
export const ErrorBoundaryWithErrorStory: StoryObj = {
|
|
35
38
|
render: (args) => (
|
|
36
39
|
<ErrorBoundary size={args.size}>
|
|
37
|
-
<ContentThatThrowsError />
|
|
40
|
+
<ContentThatThrowsError error={() => new Error('Some error')} />
|
|
38
41
|
</ErrorBoundary>
|
|
39
42
|
),
|
|
40
43
|
play: async ({ canvasElement }) => {
|
|
@@ -45,6 +48,20 @@ export const ErrorBoundaryWithErrorStory: StoryObj = {
|
|
|
45
48
|
},
|
|
46
49
|
};
|
|
47
50
|
|
|
48
|
-
const
|
|
49
|
-
|
|
51
|
+
export const ErrorBoundaryWithUserFacingErrorStory: StoryObj = {
|
|
52
|
+
render: (args) => (
|
|
53
|
+
<ErrorBoundary size={args.size}>
|
|
54
|
+
<ContentThatThrowsError error={() => new UserFacingError('Error Headline', 'Some error')} />
|
|
55
|
+
</ErrorBoundary>
|
|
56
|
+
),
|
|
57
|
+
play: async ({ canvasElement }) => {
|
|
58
|
+
const canvas = within(canvasElement);
|
|
59
|
+
const content = canvas.queryByText('Some content.', { exact: false });
|
|
60
|
+
await waitFor(() => expect(content).not.toBeInTheDocument());
|
|
61
|
+
await waitFor(() => expect(canvas.getByText('Error - Error Headline')).toBeInTheDocument());
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const ContentThatThrowsError = (props: { error: () => Error }) => {
|
|
66
|
+
throw props.error();
|
|
50
67
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
-
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
|
2
|
+
import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
|
|
3
3
|
|
|
4
4
|
import { ErrorDisplay, UserFacingError } from './error-display';
|
|
5
5
|
import { ResizeContainer } from './resize-container';
|
|
@@ -12,7 +12,7 @@ const meta: Meta = {
|
|
|
12
12
|
|
|
13
13
|
export default meta;
|
|
14
14
|
|
|
15
|
-
export const
|
|
15
|
+
export const GenericErrorStory: StoryObj = {
|
|
16
16
|
render: () => (
|
|
17
17
|
<ResizeContainer size={{ height: '600px', width: '100%' }}>
|
|
18
18
|
<ErrorDisplay error={new Error('some message')} />
|
|
@@ -48,3 +48,21 @@ export const UserFacingErrorStory: StoryObj = {
|
|
|
48
48
|
});
|
|
49
49
|
},
|
|
50
50
|
};
|
|
51
|
+
|
|
52
|
+
export const FiresEvent: StoryObj = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<ResizeContainer size={{ height: '600px', width: '100%' }}>
|
|
55
|
+
<ErrorDisplay error={new UserFacingError('Error Title', 'some message')} />
|
|
56
|
+
</ResizeContainer>
|
|
57
|
+
),
|
|
58
|
+
|
|
59
|
+
play: async ({ canvasElement }) => {
|
|
60
|
+
const listenerMock = fn();
|
|
61
|
+
canvasElement.addEventListener('gs-error', listenerMock);
|
|
62
|
+
|
|
63
|
+
await waitFor(() => {
|
|
64
|
+
expect(listenerMock.mock.calls.at(-1)[0].error.name).toStrictEqual('UserFacingError');
|
|
65
|
+
expect(listenerMock.mock.calls.at(-1)[0].error.message).toStrictEqual('some message');
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
};
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
|
-
import { useRef } from 'preact/hooks';
|
|
2
|
+
import { useEffect, useRef } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import { LapisError, UnknownLapisError } from '../../lapisApi/lapisApi';
|
|
5
|
+
|
|
6
|
+
export const GS_ERROR_EVENT_TYPE = 'gs-error';
|
|
7
|
+
|
|
8
|
+
export class ErrorEvent extends Event {
|
|
9
|
+
constructor(public readonly error: Error) {
|
|
10
|
+
super(GS_ERROR_EVENT_TYPE, {
|
|
11
|
+
bubbles: true,
|
|
12
|
+
composed: true,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
}
|
|
3
16
|
|
|
4
17
|
export class UserFacingError extends Error {
|
|
5
18
|
constructor(
|
|
@@ -14,20 +27,27 @@ export class UserFacingError extends Error {
|
|
|
14
27
|
export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) => {
|
|
15
28
|
console.error(error);
|
|
16
29
|
|
|
30
|
+
const containerRef = useRef<HTMLInputElement>(null);
|
|
17
31
|
const ref = useRef<HTMLDialogElement>(null);
|
|
18
32
|
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
containerRef.current?.dispatchEvent(new ErrorEvent(error));
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const { headline, details } = getDisplayedErrorMessage(error);
|
|
38
|
+
|
|
19
39
|
return (
|
|
20
|
-
<div
|
|
21
|
-
|
|
40
|
+
<div
|
|
41
|
+
ref={containerRef}
|
|
42
|
+
className='h-full w-full rounded-md border-2 border-gray-100 p-2 flex items-center justify-center flex-col'
|
|
43
|
+
>
|
|
44
|
+
<div className='text-red-700 font-bold'>{headline}</div>
|
|
22
45
|
<div>
|
|
23
46
|
Oops! Something went wrong.
|
|
24
|
-
{
|
|
47
|
+
{details !== undefined && (
|
|
25
48
|
<>
|
|
26
49
|
{' '}
|
|
27
|
-
<button
|
|
28
|
-
className='text-sm text-gray-600 hover:text-gray-300'
|
|
29
|
-
onClick={() => ref.current?.showModal()}
|
|
30
|
-
>
|
|
50
|
+
<button className='underline hover:text-gray-400' onClick={() => ref.current?.showModal()}>
|
|
31
51
|
Show details.
|
|
32
52
|
</button>
|
|
33
53
|
<dialog ref={ref} class='modal'>
|
|
@@ -37,8 +57,8 @@ export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) =>
|
|
|
37
57
|
✕
|
|
38
58
|
</button>
|
|
39
59
|
</form>
|
|
40
|
-
<h1 class='text-lg'>{
|
|
41
|
-
<p class='py-4'>{
|
|
60
|
+
<h1 class='text-lg'>{details.headline}</h1>
|
|
61
|
+
<p class='py-4'>{details.message}</p>
|
|
42
62
|
</div>
|
|
43
63
|
<form method='dialog' class='modal-backdrop'>
|
|
44
64
|
<button>close</button>
|
|
@@ -50,3 +70,37 @@ export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) =>
|
|
|
50
70
|
</div>
|
|
51
71
|
);
|
|
52
72
|
};
|
|
73
|
+
|
|
74
|
+
function getDisplayedErrorMessage(error: Error) {
|
|
75
|
+
if (error instanceof UserFacingError) {
|
|
76
|
+
return {
|
|
77
|
+
headline: `Error - ${error.headline}`,
|
|
78
|
+
details: {
|
|
79
|
+
headline: error.headline,
|
|
80
|
+
message: error.message,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (error instanceof LapisError) {
|
|
86
|
+
return {
|
|
87
|
+
headline: `Error - Failed fetching ${error.requestedData} from LAPIS`,
|
|
88
|
+
details: {
|
|
89
|
+
headline: `LAPIS request failed: ${error.requestedData} - ${error.problemDetail.status} ${error.problemDetail.title}`,
|
|
90
|
+
message: error.problemDetail.detail ?? error.message,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (error instanceof UnknownLapisError) {
|
|
96
|
+
return {
|
|
97
|
+
headline: `Error - Failed fetching ${error.requestedData} from LAPIS`,
|
|
98
|
+
details: {
|
|
99
|
+
headline: `LAPIS request failed: An unexpected error occurred while fetching ${error.requestedData}`,
|
|
100
|
+
message: error.message,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { headline: 'Error', details: undefined };
|
|
106
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
2
|
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
|
3
3
|
|
|
4
|
-
import Info
|
|
4
|
+
import Info from './info';
|
|
5
5
|
|
|
6
|
-
const meta: Meta
|
|
6
|
+
const meta: Meta = {
|
|
7
7
|
title: 'Component/Info',
|
|
8
8
|
component: Info,
|
|
9
9
|
parameters: { fetchMock: {} },
|
|
@@ -13,7 +13,7 @@ export default meta;
|
|
|
13
13
|
|
|
14
14
|
const tooltipText = 'This is a tooltip which shows some information.';
|
|
15
15
|
|
|
16
|
-
export const InfoStory: StoryObj
|
|
16
|
+
export const InfoStory: StoryObj = {
|
|
17
17
|
render: (args) => (
|
|
18
18
|
<div class='flex justify-center px-4 py-16'>
|
|
19
19
|
<Info {...args}>{tooltipText}</Info>
|
|
@@ -21,7 +21,7 @@ export const InfoStory: StoryObj<InfoProps> = {
|
|
|
21
21
|
),
|
|
22
22
|
};
|
|
23
23
|
|
|
24
|
-
export const OpenInfo: StoryObj
|
|
24
|
+
export const OpenInfo: StoryObj = {
|
|
25
25
|
...InfoStory,
|
|
26
26
|
play: async ({ canvasElement }) => {
|
|
27
27
|
const canvas = within(canvasElement);
|
|
@@ -30,7 +30,7 @@ export const OpenInfo: StoryObj<InfoProps> = {
|
|
|
30
30
|
},
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
export const ShowsAndClosesInfoOnClick: StoryObj
|
|
33
|
+
export const ShowsAndClosesInfoOnClick: StoryObj = {
|
|
34
34
|
...InfoStory,
|
|
35
35
|
play: async ({ canvasElement, step }) => {
|
|
36
36
|
const canvas = within(canvasElement);
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
import { useRef } from 'preact/hooks';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const Info: FunctionComponent<InfoProps> = ({ children }) => {
|
|
4
|
+
const Info: FunctionComponent = ({ children }) => {
|
|
7
5
|
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
8
6
|
|
|
9
7
|
const toggleHelp = () => {
|
|
@@ -124,7 +122,7 @@ function generateFullExampleCode(componentCode: string, componentName: string) {
|
|
|
124
122
|
const storyBookPath = `/docs/visualization-${componentName}--docs`;
|
|
125
123
|
return `<html>
|
|
126
124
|
<head>
|
|
127
|
-
<script type="module" src="https://unpkg.com/@genspectrum/dashboard-components@latest/
|
|
125
|
+
<script type="module" src="https://unpkg.com/@genspectrum/dashboard-components@latest/standalone-bundle/dashboard-components.js"></script>
|
|
128
126
|
<link rel="stylesheet" href="https://unpkg.com/@genspectrum/dashboard-components@latest/dist/style.css" />
|
|
129
127
|
</head>
|
|
130
128
|
|
|
@@ -4,13 +4,18 @@ import { useEffect, useState } from 'preact/hooks';
|
|
|
4
4
|
export type PercentInputProps = {
|
|
5
5
|
percentage: number;
|
|
6
6
|
setPercentage: (percentage: number) => void;
|
|
7
|
+
indicateError?: boolean;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
10
|
const percentageInRange = (percentage: number) => {
|
|
10
11
|
return percentage <= 100 && percentage >= 0;
|
|
11
12
|
};
|
|
12
13
|
|
|
13
|
-
export const PercentInput: FunctionComponent<PercentInputProps> = ({
|
|
14
|
+
export const PercentInput: FunctionComponent<PercentInputProps> = ({
|
|
15
|
+
percentage,
|
|
16
|
+
setPercentage,
|
|
17
|
+
indicateError = false,
|
|
18
|
+
}) => {
|
|
14
19
|
const [internalPercentage, setInternalPercentage] = useState(percentage);
|
|
15
20
|
|
|
16
21
|
useEffect(() => {
|
|
@@ -33,7 +38,7 @@ export const PercentInput: FunctionComponent<PercentInputProps> = ({ percentage,
|
|
|
33
38
|
setInternalPercentage(value);
|
|
34
39
|
};
|
|
35
40
|
|
|
36
|
-
const isError = !percentageInRange(internalPercentage);
|
|
41
|
+
const isError = indicateError || !percentageInRange(internalPercentage);
|
|
37
42
|
return (
|
|
38
43
|
<label className={`input input-bordered flex items-center gap-2 w-32 ${isError ? 'input-error' : ''}`}>
|
|
39
44
|
<input
|
|
@@ -57,12 +57,22 @@ export const ProportionSelector: FunctionComponent<ProportionSelectorProps> = ({
|
|
|
57
57
|
setInternalMaxProportion(newMaxProportion);
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
+
const indicateError = internalMinProportion > internalMaxProportion;
|
|
61
|
+
|
|
60
62
|
return (
|
|
61
63
|
<div class='flex flex-col w-64 mb-2'>
|
|
62
64
|
<div class='flex items-center '>
|
|
63
|
-
<PercentInput
|
|
65
|
+
<PercentInput
|
|
66
|
+
percentage={internalMinProportion * 100}
|
|
67
|
+
setPercentage={updateMinPercentage}
|
|
68
|
+
indicateError={indicateError}
|
|
69
|
+
/>
|
|
64
70
|
<div class='m-2'>-</div>
|
|
65
|
-
<PercentInput
|
|
71
|
+
<PercentInput
|
|
72
|
+
percentage={internalMaxProportion * 100}
|
|
73
|
+
setPercentage={updateMaxPercentage}
|
|
74
|
+
indicateError={indicateError}
|
|
75
|
+
/>
|
|
66
76
|
</div>
|
|
67
77
|
<div class='my-1'>
|
|
68
78
|
<MinMaxRangeSlider
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { withActions } from '@storybook/addon-actions/decorator';
|
|
2
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
3
2
|
import { expect, waitFor, within } from '@storybook/test';
|
|
4
3
|
import dayjs from 'dayjs/esm';
|
|
@@ -13,6 +12,7 @@ import {
|
|
|
13
12
|
PRESET_VALUE_LAST_6_MONTHS,
|
|
14
13
|
PRESET_VALUE_LAST_MONTH,
|
|
15
14
|
} from './selectableOptions';
|
|
15
|
+
import { previewHandles } from '../../../.storybook/preview';
|
|
16
16
|
import { LAPIS_URL } from '../../constants';
|
|
17
17
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
18
18
|
|
|
@@ -23,7 +23,7 @@ const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
|
23
23
|
component: DateRangeSelector,
|
|
24
24
|
parameters: {
|
|
25
25
|
actions: {
|
|
26
|
-
handles: ['gs-date-range-changed'],
|
|
26
|
+
handles: ['gs-date-range-changed', ...previewHandles],
|
|
27
27
|
},
|
|
28
28
|
fetchMock: {},
|
|
29
29
|
},
|
|
@@ -68,7 +68,6 @@ const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
|
68
68
|
initialDateFrom: '',
|
|
69
69
|
initialDateTo: '',
|
|
70
70
|
},
|
|
71
|
-
decorators: [withActions],
|
|
72
71
|
};
|
|
73
72
|
|
|
74
73
|
export default meta;
|
|
@@ -20,10 +20,10 @@ export interface DateRangeSelectorProps<CustomLabel extends string> extends Date
|
|
|
20
20
|
|
|
21
21
|
export interface DateRangeSelectorPropsInner<CustomLabel extends string> {
|
|
22
22
|
customSelectOptions: CustomSelectOption<CustomLabel>[];
|
|
23
|
-
earliestDate
|
|
24
|
-
initialValue
|
|
25
|
-
initialDateFrom
|
|
26
|
-
initialDateTo
|
|
23
|
+
earliestDate: string;
|
|
24
|
+
initialValue: PresetOptionValues | CustomLabel;
|
|
25
|
+
initialDateFrom: string;
|
|
26
|
+
initialDateTo: string;
|
|
27
27
|
dateColumn: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { withActions } from '@storybook/addon-actions/decorator';
|
|
2
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
3
2
|
|
|
4
3
|
import { LineageFilter, type LineageFilterProps } from './lineage-filter';
|
|
4
|
+
import { previewHandles } from '../../../.storybook/preview';
|
|
5
5
|
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
|
|
6
6
|
import aggregatedData from '../../preact/lineageFilter/__mockData__/aggregated.json';
|
|
7
7
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
@@ -11,7 +11,7 @@ const meta: Meta = {
|
|
|
11
11
|
component: LineageFilter,
|
|
12
12
|
parameters: {
|
|
13
13
|
actions: {
|
|
14
|
-
handles: ['gs-lineage-filter-changed'],
|
|
14
|
+
handles: ['gs-lineage-filter-changed', ...previewHandles],
|
|
15
15
|
},
|
|
16
16
|
fetchMock: {
|
|
17
17
|
mocks: [
|
|
@@ -31,7 +31,6 @@ const meta: Meta = {
|
|
|
31
31
|
],
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
|
-
decorators: [withActions],
|
|
35
34
|
};
|
|
36
35
|
|
|
37
36
|
export default meta;
|