@genspectrum/dashboard-components 0.6.14 → 0.6.16
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/custom-elements.json +12 -6
- package/dist/dashboard-components.js +104 -60
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +27 -9
- package/dist/style.css +2 -5
- package/package.json +3 -3
- package/src/index.ts +8 -0
- package/src/lapisApi/lapisApi.ts +15 -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 +1 -3
- package/src/preact/components/proportion-selector-dropdown.stories.tsx +13 -4
- package/src/preact/components/proportion-selector-dropdown.tsx +7 -4
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +2 -3
- package/src/preact/lineageFilter/lineage-filter.stories.tsx +2 -3
- package/src/preact/locationFilter/fetchAutocompletionList.ts +1 -14
- package/src/preact/locationFilter/location-filter.stories.tsx +2 -3
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +2 -3
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +1 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +1 -1
- 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/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-lineage-filter.stories.ts +2 -3
- package/src/web-components/input/gs-location-filter.stories.ts +2 -3
- package/src/web-components/input/gs-mutation-filter.stories.ts +2 -3
- package/src/web-components/input/gs-text-input.stories.ts +2 -3
- package/standalone-bundle/dashboard-components.js +1724 -1692
- package/standalone-bundle/dashboard-components.js.map +1 -1
|
@@ -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;
|
|
@@ -1004,37 +1022,37 @@ declare global {
|
|
|
1004
1022
|
|
|
1005
1023
|
declare global {
|
|
1006
1024
|
interface HTMLElementTagNameMap {
|
|
1007
|
-
'gs-
|
|
1025
|
+
'gs-date-range-selector': DateRangeSelectorComponent;
|
|
1026
|
+
}
|
|
1027
|
+
interface HTMLElementEventMap {
|
|
1028
|
+
'gs-date-range-changed': CustomEvent<Record<string, string>>;
|
|
1008
1029
|
}
|
|
1009
1030
|
}
|
|
1010
1031
|
|
|
1011
1032
|
|
|
1012
1033
|
declare global {
|
|
1013
1034
|
interface HTMLElementTagNameMap {
|
|
1014
|
-
'gs-
|
|
1015
|
-
}
|
|
1016
|
-
interface HTMLElementEventMap {
|
|
1017
|
-
'gs-date-range-changed': CustomEvent<Record<string, string>>;
|
|
1035
|
+
'gs-mutations-over-time-component': MutationsOverTimeComponent;
|
|
1018
1036
|
}
|
|
1019
1037
|
}
|
|
1020
1038
|
|
|
1021
1039
|
|
|
1022
1040
|
declare global {
|
|
1023
1041
|
interface HTMLElementTagNameMap {
|
|
1024
|
-
'gs-
|
|
1042
|
+
'gs-location-filter': LocationFilterComponent;
|
|
1025
1043
|
}
|
|
1026
1044
|
interface HTMLElementEventMap {
|
|
1027
|
-
'gs-
|
|
1045
|
+
'gs-location-changed': CustomEvent<Record<string, string>>;
|
|
1028
1046
|
}
|
|
1029
1047
|
}
|
|
1030
1048
|
|
|
1031
1049
|
|
|
1032
1050
|
declare global {
|
|
1033
1051
|
interface HTMLElementTagNameMap {
|
|
1034
|
-
'gs-
|
|
1052
|
+
'gs-text-input': TextInputComponent;
|
|
1035
1053
|
}
|
|
1036
1054
|
interface HTMLElementEventMap {
|
|
1037
|
-
'gs-
|
|
1055
|
+
'gs-text-input-changed': CustomEvent<Record<string, string>>;
|
|
1038
1056
|
}
|
|
1039
1057
|
}
|
|
1040
1058
|
|
package/dist/style.css
CHANGED
|
@@ -3036,9 +3036,6 @@ input.tab:checked + .tab-content,
|
|
|
3036
3036
|
.w-32 {
|
|
3037
3037
|
width: 8rem;
|
|
3038
3038
|
}
|
|
3039
|
-
.w-44 {
|
|
3040
|
-
width: 11rem;
|
|
3041
|
-
}
|
|
3042
3039
|
.w-64 {
|
|
3043
3040
|
width: 16rem;
|
|
3044
3041
|
}
|
|
@@ -3372,9 +3369,9 @@ input.tab:checked + .tab-content,
|
|
|
3372
3369
|
--tw-text-opacity: 1;
|
|
3373
3370
|
color: rgb(30 64 175 / var(--tw-text-opacity));
|
|
3374
3371
|
}
|
|
3375
|
-
.hover\:text-gray-
|
|
3372
|
+
.hover\:text-gray-400:hover {
|
|
3376
3373
|
--tw-text-opacity: 1;
|
|
3377
|
-
color: rgb(
|
|
3374
|
+
color: rgb(156 163 175 / var(--tw-text-opacity));
|
|
3378
3375
|
}
|
|
3379
3376
|
.hover\:text-gray-700:hover {
|
|
3380
3377
|
--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.16",
|
|
4
4
|
"description": "GenSpectrum web components for building dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -93,8 +93,8 @@
|
|
|
93
93
|
"@storybook/web-components-vite": "^8.0.9",
|
|
94
94
|
"@types/node": "^22.0.0",
|
|
95
95
|
"@types/object-hash": "^3.0.6",
|
|
96
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
97
|
-
"@typescript-eslint/parser": "^
|
|
96
|
+
"@typescript-eslint/eslint-plugin": "^8.2.0",
|
|
97
|
+
"@typescript-eslint/parser": "^8.2.0",
|
|
98
98
|
"autoprefixer": "^10.4.19",
|
|
99
99
|
"daisyui": "^4.10.2",
|
|
100
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
|
|
|
@@ -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 = () => {
|
|
@@ -17,7 +17,8 @@ export default meta;
|
|
|
17
17
|
const WrapperWithState: FunctionComponent<{
|
|
18
18
|
setMinProportion: (value: number) => void;
|
|
19
19
|
setMaxProportion: (value: number) => void;
|
|
20
|
-
|
|
20
|
+
labelPrefix?: string;
|
|
21
|
+
}> = ({ setMinProportion, setMaxProportion, labelPrefix }) => {
|
|
21
22
|
const [proportionInterval, setProportionInterval] = useState({ min: 0.05, max: 1 });
|
|
22
23
|
|
|
23
24
|
return (
|
|
@@ -31,24 +32,32 @@ const WrapperWithState: FunctionComponent<{
|
|
|
31
32
|
setProportionInterval({ ...proportionInterval, max: value });
|
|
32
33
|
setMaxProportion(value);
|
|
33
34
|
}}
|
|
35
|
+
labelPrefix={labelPrefix}
|
|
34
36
|
/>
|
|
35
37
|
);
|
|
36
38
|
};
|
|
37
39
|
|
|
38
40
|
export const ProportionSelectorStory: StoryObj<ProportionSelectorDropdownProps> = {
|
|
39
41
|
render: (args) => {
|
|
40
|
-
return
|
|
42
|
+
return (
|
|
43
|
+
<WrapperWithState
|
|
44
|
+
setMinProportion={args.setMinProportion}
|
|
45
|
+
setMaxProportion={args.setMaxProportion}
|
|
46
|
+
labelPrefix={args.labelPrefix}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
41
49
|
},
|
|
42
50
|
args: {
|
|
43
51
|
setMinProportion: fn(),
|
|
44
52
|
setMaxProportion: fn(),
|
|
53
|
+
labelPrefix: 'TestPrefix',
|
|
45
54
|
},
|
|
46
55
|
play: async ({ canvasElement, step, args }) => {
|
|
47
56
|
const canvas = within(canvasElement);
|
|
48
57
|
|
|
49
58
|
await step('Expect initial proportion to show on the button', async () => {
|
|
50
59
|
const button = canvas.getByRole('button');
|
|
51
|
-
await expect(button).toHaveTextContent('
|
|
60
|
+
await expect(button).toHaveTextContent('TestPrefix 5.0% - 100.0%');
|
|
52
61
|
});
|
|
53
62
|
|
|
54
63
|
await step('Change min proportion and expect it to show on the button', async () => {
|
|
@@ -59,7 +68,7 @@ export const ProportionSelectorStory: StoryObj<ProportionSelectorDropdownProps>
|
|
|
59
68
|
await userEvent.clear(minInput);
|
|
60
69
|
await userEvent.type(minInput, '10');
|
|
61
70
|
|
|
62
|
-
await waitFor(() => expect(button).toHaveTextContent('
|
|
71
|
+
await waitFor(() => expect(button).toHaveTextContent('TestPrefix 10.0% - 100.0%'));
|
|
63
72
|
await expect(args.setMinProportion).toHaveBeenCalledWith(0.1);
|
|
64
73
|
});
|
|
65
74
|
},
|
|
@@ -3,18 +3,21 @@ import { type FunctionComponent } from 'preact';
|
|
|
3
3
|
import { Dropdown } from './dropdown';
|
|
4
4
|
import { ProportionSelector, type ProportionSelectorProps } from './proportion-selector';
|
|
5
5
|
|
|
6
|
-
export type ProportionSelectorDropdownProps = ProportionSelectorProps;
|
|
6
|
+
export type ProportionSelectorDropdownProps = ProportionSelectorProps & { labelPrefix?: string };
|
|
7
7
|
|
|
8
8
|
export const ProportionSelectorDropdown: FunctionComponent<ProportionSelectorDropdownProps> = ({
|
|
9
9
|
proportionInterval,
|
|
10
10
|
setMinProportion,
|
|
11
11
|
setMaxProportion,
|
|
12
|
+
labelPrefix = 'Proportion',
|
|
12
13
|
}) => {
|
|
13
|
-
const
|
|
14
|
+
const percentLabel = `${(proportionInterval.min * 100).toFixed(1)}% - ${(proportionInterval.max * 100).toFixed(1)}%`;
|
|
15
|
+
|
|
16
|
+
const width = 'w-[calc(1.5 * var(--tw-space-x-reverse) + 1.5 * var(--tw-space-x))]';
|
|
14
17
|
|
|
15
18
|
return (
|
|
16
|
-
<div className=
|
|
17
|
-
<Dropdown buttonTitle={
|
|
19
|
+
<div className={width}>
|
|
20
|
+
<Dropdown buttonTitle={`${labelPrefix} ${percentLabel}`} placement={'bottom-start'}>
|
|
18
21
|
<ProportionSelector
|
|
19
22
|
proportionInterval={proportionInterval}
|
|
20
23
|
setMinProportion={setMinProportion}
|
|
@@ -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;
|
|
@@ -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;
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import { LapisError } from '../../lapisApi/lapisApi';
|
|
2
1
|
import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
|
|
3
|
-
import { UserFacingError } from '../components/error-display';
|
|
4
2
|
|
|
5
3
|
export async function fetchAutocompletionList(fields: string[], lapis: string, signal?: AbortSignal) {
|
|
6
4
|
const toAncestorInHierarchyOverwriteValues = Array(fields.length - 1)
|
|
@@ -10,18 +8,7 @@ export async function fetchAutocompletionList(fields: string[], lapis: string, s
|
|
|
10
8
|
|
|
11
9
|
const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string | null>>({}, fields);
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
|
|
16
|
-
} catch (error) {
|
|
17
|
-
if (error instanceof LapisError) {
|
|
18
|
-
throw new UserFacingError(
|
|
19
|
-
`Failed to fetch autocomplete list from LAPIS: ${error.problemDetail.status} ${error.problemDetail.title ?? ''}`,
|
|
20
|
-
error.problemDetail.detail ?? error.message,
|
|
21
|
-
);
|
|
22
|
-
}
|
|
23
|
-
throw error;
|
|
24
|
-
}
|
|
11
|
+
const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
|
|
25
12
|
|
|
26
13
|
const locationValues = data
|
|
27
14
|
.map((entry) => fields.reduce((acc, field) => ({ ...acc, [field]: entry[field] }), {}))
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { withActions } from '@storybook/addon-actions/decorator';
|
|
2
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
3
2
|
|
|
4
3
|
import data from './__mockData__/aggregated.json';
|
|
5
4
|
import { LocationFilter, type LocationFilterProps } from './location-filter';
|
|
5
|
+
import { previewHandles } from '../../../.storybook/preview';
|
|
6
6
|
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
|
|
7
7
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
8
8
|
|
|
@@ -28,7 +28,7 @@ const meta: Meta<LocationFilterProps> = {
|
|
|
28
28
|
],
|
|
29
29
|
},
|
|
30
30
|
actions: {
|
|
31
|
-
handles: ['gs-location-changed'],
|
|
31
|
+
handles: ['gs-location-changed', ...previewHandles],
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
34
|
args: {
|
|
@@ -59,7 +59,6 @@ const meta: Meta<LocationFilterProps> = {
|
|
|
59
59
|
},
|
|
60
60
|
},
|
|
61
61
|
},
|
|
62
|
-
decorators: [withActions],
|
|
63
62
|
};
|
|
64
63
|
|
|
65
64
|
export default meta;
|