@genspectrum/dashboard-components 0.1.5 → 0.3.0
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 +1161 -928
- package/dist/dashboard-components.js +663 -237
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +177 -140
- package/dist/style.css +247 -50
- package/package.json +2 -3
- package/src/constants.ts +1 -1
- package/src/lapisApi/lapisApi.ts +46 -2
- package/src/lapisApi/lapisTypes.ts +14 -0
- package/src/preact/aggregatedData/aggregate.stories.tsx +4 -2
- package/src/preact/aggregatedData/aggregate.tsx +31 -29
- package/src/preact/components/error-boundary.stories.tsx +54 -0
- package/src/preact/components/error-boundary.tsx +22 -0
- package/src/preact/components/error-display.stories.tsx +32 -4
- package/src/preact/components/error-display.tsx +48 -1
- package/src/preact/components/loading-display.stories.tsx +6 -6
- package/src/preact/components/loading-display.tsx +1 -1
- package/src/preact/components/no-data-display.tsx +5 -1
- package/src/preact/components/resize-container.tsx +5 -14
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +19 -0
- package/src/preact/dateRangeSelector/date-range-selector.tsx +38 -7
- package/src/preact/locationFilter/fetchAutocompletionList.ts +15 -1
- package/src/preact/locationFilter/location-filter.stories.tsx +23 -6
- package/src/preact/locationFilter/location-filter.tsx +28 -18
- package/src/preact/mutationComparison/mutation-comparison.stories.tsx +6 -3
- package/src/preact/mutationComparison/mutation-comparison.tsx +33 -32
- package/src/preact/mutationComparison/queryMutationData.ts +2 -3
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +18 -3
- package/src/preact/mutationFilter/mutation-filter.tsx +26 -7
- package/src/preact/mutations/mutations.stories.tsx +6 -3
- package/src/preact/mutations/mutations.tsx +28 -26
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +14 -7
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +50 -32
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +6 -3
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +46 -32
- package/src/preact/textInput/text-input.stories.tsx +26 -0
- package/src/preact/textInput/text-input.tsx +25 -3
- package/src/query/queryPrevalenceOverTime.ts +4 -10
- package/src/types.ts +4 -1
- package/src/web-components/ResizeContainer.mdx +13 -0
- package/src/web-components/app.stories.ts +1 -2
- package/src/web-components/app.ts +7 -3
- package/src/web-components/index.ts +1 -1
- package/src/web-components/input/{date-range-selector-component.stories.ts → gs-date-range-selector.stories.ts} +29 -4
- package/src/web-components/input/{date-range-selector-component.tsx → gs-date-range-selector.tsx} +32 -10
- package/src/web-components/input/{location-filter-component.stories.ts → gs-location-filter.stories.ts} +32 -5
- package/src/web-components/input/{location-filter-component.tsx → gs-location-filter.tsx} +11 -1
- package/src/web-components/input/{mutation-filter-component.stories.ts → gs-mutation-filter.stories.ts} +23 -4
- package/src/web-components/input/gs-mutation-filter.tsx +126 -0
- package/src/web-components/input/{text-input-component.stories.ts → gs-text-input.stories.ts} +34 -6
- package/src/web-components/input/{text-input-component.tsx → gs-text-input.tsx} +16 -4
- package/src/web-components/input/index.ts +4 -4
- package/src/web-components/input/introduction.mdx +11 -0
- package/src/web-components/introduction.mdx +15 -0
- package/src/web-components/visualization/data_visualization_statistical_analysis.mdx +26 -0
- package/src/web-components/{display/aggregate-component.stories.ts → visualization/gs-aggregate.stories.ts} +23 -11
- package/src/web-components/visualization/gs-aggregate.tsx +88 -0
- package/src/web-components/{display/mutation-comparison-component.stories.ts → visualization/gs-mutation-comparison.stories.ts} +21 -16
- package/src/web-components/{display/mutation-comparison-component.tsx → visualization/gs-mutation-comparison.tsx} +27 -18
- package/src/web-components/{display/mutations-component.stories.ts → visualization/gs-mutations.stories.ts} +20 -15
- package/src/web-components/{display/mutations-component.tsx → visualization/gs-mutations.tsx} +20 -10
- package/src/web-components/{display/prevalence-over-time-component.stories.ts → visualization/gs-prevalence-over-time.stories.ts} +29 -20
- package/src/web-components/{display/prevalence-over-time-component.tsx → visualization/gs-prevalence-over-time.tsx} +47 -22
- package/src/web-components/{display/relative-growth-advantage-component.stories.ts → visualization/gs-relative-growth-advantage.stories.ts} +12 -7
- package/src/web-components/{display/relative-growth-advantage-component.tsx → visualization/gs-relative-growth-advantage.tsx} +21 -9
- package/src/web-components/visualization/index.ts +5 -0
- package/src/web-components/display/aggregate-component.tsx +0 -72
- package/src/web-components/display/index.ts +0 -5
- package/src/web-components/input/mutation-filter-component.tsx +0 -83
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
+
import { expect, waitFor, within } from '@storybook/test';
|
|
3
|
+
|
|
4
|
+
import { ErrorBoundary } from './error-boundary';
|
|
5
|
+
|
|
6
|
+
const meta: Meta = {
|
|
7
|
+
title: 'Component/Error boundary',
|
|
8
|
+
component: ErrorBoundary,
|
|
9
|
+
parameters: { fetchMock: {} },
|
|
10
|
+
argTypes: {
|
|
11
|
+
size: { control: 'object' },
|
|
12
|
+
defaultSize: { control: 'object' },
|
|
13
|
+
headline: { control: 'text' },
|
|
14
|
+
},
|
|
15
|
+
args: {
|
|
16
|
+
size: { height: '600px', width: '100%' },
|
|
17
|
+
headline: 'Some headline',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
|
|
23
|
+
export const ErrorBoundaryWithoutErrorStory: StoryObj = {
|
|
24
|
+
render: (args) => (
|
|
25
|
+
<ErrorBoundary size={args.size} headline={args.headline}>
|
|
26
|
+
<div>Some content</div>
|
|
27
|
+
</ErrorBoundary>
|
|
28
|
+
),
|
|
29
|
+
play: async ({ canvasElement }) => {
|
|
30
|
+
const canvas = within(canvasElement);
|
|
31
|
+
const content = canvas.getByText('Some content', { exact: false });
|
|
32
|
+
await waitFor(() => expect(content).toBeInTheDocument());
|
|
33
|
+
await waitFor(() => expect(canvas.queryByText('Some headline')).not.toBeInTheDocument());
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const ErrorBoundaryWithErrorStory: StoryObj = {
|
|
38
|
+
render: (args) => (
|
|
39
|
+
<ErrorBoundary size={args.size} headline={args.headline}>
|
|
40
|
+
<ContentThatThrowsError />
|
|
41
|
+
</ErrorBoundary>
|
|
42
|
+
),
|
|
43
|
+
play: async ({ canvasElement }) => {
|
|
44
|
+
const canvas = within(canvasElement);
|
|
45
|
+
const content = canvas.queryByText('Some content.', { exact: false });
|
|
46
|
+
await waitFor(() => expect(content).not.toBeInTheDocument());
|
|
47
|
+
await waitFor(() => expect(canvas.getByText('Some headline')).toBeInTheDocument());
|
|
48
|
+
await waitFor(() => expect(canvas.getByText('Error')).toBeInTheDocument());
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const ContentThatThrowsError = () => {
|
|
53
|
+
throw new Error('Some error');
|
|
54
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FunctionComponent } from 'preact';
|
|
2
|
+
import { useErrorBoundary } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import { ErrorDisplay } from './error-display';
|
|
5
|
+
import { ResizeContainer, type Size } from './resize-container';
|
|
6
|
+
import Headline from '../components/headline';
|
|
7
|
+
|
|
8
|
+
export const ErrorBoundary: FunctionComponent<{ size: Size; headline?: string }> = ({ size, headline, children }) => {
|
|
9
|
+
const [internalError] = useErrorBoundary();
|
|
10
|
+
|
|
11
|
+
if (internalError) {
|
|
12
|
+
return (
|
|
13
|
+
<ResizeContainer size={size}>
|
|
14
|
+
<Headline heading={headline}>
|
|
15
|
+
<ErrorDisplay error={internalError} />
|
|
16
|
+
</Headline>
|
|
17
|
+
</ResizeContainer>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return <>{children}</>;
|
|
22
|
+
};
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
-
import { expect, waitFor, within } from '@storybook/test';
|
|
2
|
+
import { expect, userEvent, waitFor, within } from '@storybook/test';
|
|
3
3
|
|
|
4
|
-
import { ErrorDisplay } from './error-display';
|
|
4
|
+
import { ErrorDisplay, UserFacingError } from './error-display';
|
|
5
|
+
import { ResizeContainer } from './resize-container';
|
|
5
6
|
|
|
6
7
|
const meta: Meta = {
|
|
7
8
|
title: 'Component/Error',
|
|
@@ -12,11 +13,38 @@ const meta: Meta = {
|
|
|
12
13
|
export default meta;
|
|
13
14
|
|
|
14
15
|
export const ErrorStory: StoryObj = {
|
|
15
|
-
render: () =>
|
|
16
|
+
render: () => (
|
|
17
|
+
<ResizeContainer size={{ height: '600px', width: '100%' }}>
|
|
18
|
+
<ErrorDisplay error={new Error('some message')} />
|
|
19
|
+
</ResizeContainer>
|
|
20
|
+
),
|
|
16
21
|
|
|
17
22
|
play: async ({ canvasElement }) => {
|
|
18
23
|
const canvas = within(canvasElement);
|
|
19
|
-
const error = canvas.getByText('
|
|
24
|
+
const error = canvas.getByText('Oops! Something went wrong.', { exact: false });
|
|
20
25
|
await waitFor(() => expect(error).toBeInTheDocument());
|
|
26
|
+
await waitFor(() => expect(canvas.queryByText('some message')).not.toBeInTheDocument());
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const UserFacingErrorStory: StoryObj = {
|
|
31
|
+
render: () => (
|
|
32
|
+
<ResizeContainer size={{ height: '600px', width: '100%' }}>
|
|
33
|
+
<ErrorDisplay error={new UserFacingError('Error Title', 'some message')} />
|
|
34
|
+
</ResizeContainer>
|
|
35
|
+
),
|
|
36
|
+
|
|
37
|
+
play: async ({ canvasElement }) => {
|
|
38
|
+
const canvas = within(canvasElement);
|
|
39
|
+
const error = canvas.getByText('Oops! Something went wrong.', { exact: false });
|
|
40
|
+
const detailMessage = () => canvas.getByText('some message');
|
|
41
|
+
await waitFor(() => expect(error).toBeInTheDocument());
|
|
42
|
+
await waitFor(() => {
|
|
43
|
+
expect(detailMessage()).not.toBeVisible();
|
|
44
|
+
});
|
|
45
|
+
await userEvent.click(canvas.getByText('Show details.'));
|
|
46
|
+
await waitFor(() => {
|
|
47
|
+
expect(detailMessage()).toBeVisible();
|
|
48
|
+
});
|
|
21
49
|
},
|
|
22
50
|
};
|
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { useRef } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
export class UserFacingError extends Error {
|
|
5
|
+
constructor(
|
|
6
|
+
public readonly headline: string,
|
|
7
|
+
message: string,
|
|
8
|
+
) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'UserFacingError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
2
13
|
|
|
3
14
|
export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) => {
|
|
4
|
-
|
|
15
|
+
console.error(error);
|
|
16
|
+
|
|
17
|
+
const ref = useRef<HTMLDialogElement>(null);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className='h-full w-full rounded-md border-2 border-gray-100 p-2 flex items-center justify-center flex-col'>
|
|
21
|
+
<div className='text-red-700 font-bold'>Error</div>
|
|
22
|
+
<div>
|
|
23
|
+
Oops! Something went wrong.
|
|
24
|
+
{error instanceof UserFacingError && (
|
|
25
|
+
<>
|
|
26
|
+
{' '}
|
|
27
|
+
<button
|
|
28
|
+
className='text-sm text-gray-600 hover:text-gray-300'
|
|
29
|
+
onClick={() => ref.current?.showModal()}
|
|
30
|
+
>
|
|
31
|
+
Show details.
|
|
32
|
+
</button>
|
|
33
|
+
<dialog ref={ref} class='modal'>
|
|
34
|
+
<div class='modal-box'>
|
|
35
|
+
<form method='dialog'>
|
|
36
|
+
<button className='btn btn-sm btn-circle btn-ghost absolute right-2 top-2'>
|
|
37
|
+
✕
|
|
38
|
+
</button>
|
|
39
|
+
</form>
|
|
40
|
+
<h1 class='text-lg'>{error.headline}</h1>
|
|
41
|
+
<p class='py-4'>{error.message}</p>
|
|
42
|
+
</div>
|
|
43
|
+
<form method='dialog' class='modal-backdrop'>
|
|
44
|
+
<button>close</button>
|
|
45
|
+
</form>
|
|
46
|
+
</dialog>
|
|
47
|
+
</>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
5
52
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
-
import { expect, waitFor, within } from '@storybook/test';
|
|
3
2
|
|
|
4
3
|
import { LoadingDisplay } from './loading-display';
|
|
4
|
+
import { ResizeContainer } from './resize-container';
|
|
5
5
|
|
|
6
6
|
const meta: Meta = {
|
|
7
7
|
title: 'Component/Loading',
|
|
@@ -12,9 +12,9 @@ const meta: Meta = {
|
|
|
12
12
|
export default meta;
|
|
13
13
|
|
|
14
14
|
export const LoadingStory: StoryObj = {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
render: () => (
|
|
16
|
+
<ResizeContainer size={{ height: '600px', width: '100%' }}>
|
|
17
|
+
<LoadingDisplay />
|
|
18
|
+
</ResizeContainer>
|
|
19
|
+
),
|
|
20
20
|
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
|
|
3
3
|
export const NoDataDisplay: FunctionComponent = () => {
|
|
4
|
-
return
|
|
4
|
+
return (
|
|
5
|
+
<div className='h-full w-full rounded-md border-2 border-gray-100 p-2 flex items-center justify-center'>
|
|
6
|
+
<div>No data available.</div>
|
|
7
|
+
</div>
|
|
8
|
+
);
|
|
5
9
|
};
|
|
@@ -1,23 +1,14 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
|
|
3
3
|
export type Size = {
|
|
4
|
-
width
|
|
5
|
-
height
|
|
4
|
+
width: string;
|
|
5
|
+
height: string;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
export interface ResizeContainerProps {
|
|
9
|
-
size
|
|
10
|
-
defaultSize: Size;
|
|
9
|
+
size: Size;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
|
-
export const ResizeContainer: FunctionComponent<ResizeContainerProps> = ({ children, size
|
|
14
|
-
return <div style={
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const extendByDefault = (size: Size | undefined, defaultSize: Size) => {
|
|
18
|
-
if (size === undefined) {
|
|
19
|
-
return defaultSize;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return { ...defaultSize, ...size };
|
|
12
|
+
export const ResizeContainer: FunctionComponent<ResizeContainerProps> = ({ children, size }) => {
|
|
13
|
+
return <div style={size}>{children}</div>;
|
|
23
14
|
};
|
|
@@ -40,11 +40,28 @@ const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
|
40
40
|
'CustomDateRange',
|
|
41
41
|
],
|
|
42
42
|
},
|
|
43
|
+
customSelectOptions: {
|
|
44
|
+
control: {
|
|
45
|
+
type: 'object',
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
earliestDate: {
|
|
49
|
+
control: {
|
|
50
|
+
type: 'text',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
width: {
|
|
54
|
+
control: {
|
|
55
|
+
type: 'text',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
43
58
|
},
|
|
44
59
|
args: {
|
|
45
60
|
customSelectOptions: [{ label: 'CustomDateRange', dateFrom: '2021-01-01', dateTo: '2021-12-31' }],
|
|
46
61
|
earliestDate: '1970-01-01',
|
|
47
62
|
initialValue: PRESET_VALUE_LAST_3_MONTHS,
|
|
63
|
+
dateColumn: 'aDateColumn',
|
|
64
|
+
width: '100%',
|
|
48
65
|
},
|
|
49
66
|
decorators: [withActions],
|
|
50
67
|
};
|
|
@@ -58,6 +75,8 @@ export const Primary: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
|
58
75
|
customSelectOptions={args.customSelectOptions}
|
|
59
76
|
earliestDate={args.earliestDate}
|
|
60
77
|
initialValue={args.initialValue}
|
|
78
|
+
width={args.width}
|
|
79
|
+
dateColumn={args.dateColumn}
|
|
61
80
|
/>
|
|
62
81
|
</LapisUrlContext.Provider>
|
|
63
82
|
),
|
|
@@ -3,15 +3,22 @@ import 'flatpickr/dist/flatpickr.min.css';
|
|
|
3
3
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
4
4
|
|
|
5
5
|
import { toYYYYMMDD } from './dateConversion';
|
|
6
|
+
import { ErrorBoundary } from '../components/error-boundary';
|
|
7
|
+
import { ResizeContainer } from '../components/resize-container';
|
|
6
8
|
import { Select } from '../components/select';
|
|
7
9
|
import type { ScaleType } from '../shared/charts/getYAxisScale';
|
|
8
10
|
|
|
9
11
|
export type CustomSelectOption<CustomLabel extends string> = { label: CustomLabel; dateFrom: string; dateTo: string };
|
|
10
12
|
|
|
11
|
-
export interface DateRangeSelectorProps<CustomLabel extends string> {
|
|
13
|
+
export interface DateRangeSelectorProps<CustomLabel extends string> extends DateRangeSelectorPropsInner<CustomLabel> {
|
|
14
|
+
width: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DateRangeSelectorPropsInner<CustomLabel extends string> {
|
|
12
18
|
customSelectOptions: CustomSelectOption<CustomLabel>[];
|
|
13
19
|
earliestDate?: string;
|
|
14
20
|
initialValue?: PresetOptionValues | CustomLabel;
|
|
21
|
+
dateColumn: string;
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
export const PRESET_VALUE_CUSTOM = 'custom';
|
|
@@ -38,7 +45,31 @@ export const DateRangeSelector = <CustomLabel extends string>({
|
|
|
38
45
|
customSelectOptions,
|
|
39
46
|
earliestDate = '1900-01-01',
|
|
40
47
|
initialValue,
|
|
48
|
+
width,
|
|
49
|
+
dateColumn,
|
|
41
50
|
}: DateRangeSelectorProps<CustomLabel>) => {
|
|
51
|
+
const size = { width, height: '3rem' };
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<ErrorBoundary size={size}>
|
|
55
|
+
<ResizeContainer size={size}>
|
|
56
|
+
<DateRangeSelectorInner
|
|
57
|
+
customSelectOptions={customSelectOptions}
|
|
58
|
+
earliestDate={earliestDate}
|
|
59
|
+
initialValue={initialValue}
|
|
60
|
+
dateColumn={dateColumn}
|
|
61
|
+
/>
|
|
62
|
+
</ResizeContainer>
|
|
63
|
+
</ErrorBoundary>
|
|
64
|
+
);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const DateRangeSelectorInner = <CustomLabel extends string>({
|
|
68
|
+
customSelectOptions,
|
|
69
|
+
earliestDate = '1900-01-01',
|
|
70
|
+
initialValue,
|
|
71
|
+
dateColumn,
|
|
72
|
+
}: DateRangeSelectorPropsInner<CustomLabel>) => {
|
|
42
73
|
const fromDatePickerRef = useRef<HTMLInputElement>(null);
|
|
43
74
|
const toDatePickerRef = useRef<HTMLInputElement>(null);
|
|
44
75
|
const divRef = useRef<HTMLDivElement>(null);
|
|
@@ -137,8 +168,8 @@ export const DateRangeSelector = <CustomLabel extends string>({
|
|
|
137
168
|
const dateTo = toYYYYMMDD(dateToPicker?.selectedDates[0]);
|
|
138
169
|
|
|
139
170
|
const detail = {
|
|
140
|
-
...(dateFrom !== undefined && { dateFrom }),
|
|
141
|
-
...(dateTo !== undefined && { dateTo }),
|
|
171
|
+
...(dateFrom !== undefined && { [`${dateColumn}From`]: dateFrom }),
|
|
172
|
+
...(dateTo !== undefined && { [`${dateColumn}To`]: dateTo }),
|
|
142
173
|
};
|
|
143
174
|
|
|
144
175
|
divRef.current?.dispatchEvent(
|
|
@@ -151,11 +182,11 @@ export const DateRangeSelector = <CustomLabel extends string>({
|
|
|
151
182
|
};
|
|
152
183
|
|
|
153
184
|
return (
|
|
154
|
-
<div class='join' ref={divRef}>
|
|
185
|
+
<div class='join w-full' ref={divRef}>
|
|
155
186
|
<Select
|
|
156
187
|
items={selectableOptions}
|
|
157
188
|
selected={selectedDateRange}
|
|
158
|
-
selectStyle='select-bordered rounded-none join-item'
|
|
189
|
+
selectStyle='select-bordered rounded-none join-item grow'
|
|
159
190
|
onChange={(event: Event) => {
|
|
160
191
|
event.preventDefault();
|
|
161
192
|
const select = event.target as HTMLSelectElement;
|
|
@@ -164,7 +195,7 @@ export const DateRangeSelector = <CustomLabel extends string>({
|
|
|
164
195
|
}}
|
|
165
196
|
/>
|
|
166
197
|
<input
|
|
167
|
-
class='input input-bordered rounded-none join-item'
|
|
198
|
+
class='input input-bordered rounded-none join-item grow'
|
|
168
199
|
type='text'
|
|
169
200
|
placeholder='Date from'
|
|
170
201
|
ref={fromDatePickerRef}
|
|
@@ -172,7 +203,7 @@ export const DateRangeSelector = <CustomLabel extends string>({
|
|
|
172
203
|
onBlur={onChangeDateFrom}
|
|
173
204
|
/>
|
|
174
205
|
<input
|
|
175
|
-
class='input input-bordered rounded-none join-item'
|
|
206
|
+
class='input input-bordered rounded-none join-item grow'
|
|
176
207
|
type='text'
|
|
177
208
|
placeholder='Date to'
|
|
178
209
|
ref={toDatePickerRef}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { LapisError } from '../../lapisApi/lapisApi';
|
|
1
2
|
import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
|
|
3
|
+
import { UserFacingError } from '../components/error-display';
|
|
2
4
|
|
|
3
5
|
export async function fetchAutocompletionList(fields: string[], lapis: string, signal?: AbortSignal) {
|
|
4
6
|
const toAncestorInHierarchyOverwriteValues = Array(fields.length - 1)
|
|
@@ -8,7 +10,19 @@ export async function fetchAutocompletionList(fields: string[], lapis: string, s
|
|
|
8
10
|
|
|
9
11
|
const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string | null>>({}, fields);
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
let data;
|
|
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
|
+
}
|
|
25
|
+
|
|
12
26
|
const locationValues = data
|
|
13
27
|
.map((entry) => fields.reduce((acc, field) => ({ ...acc, [field]: entry[field] }), {}))
|
|
14
28
|
.reduce<Set<string>>((setOfAllHierarchies, entry) => {
|
|
@@ -6,7 +6,7 @@ import { LocationFilter, type LocationFilterProps } from './location-filter';
|
|
|
6
6
|
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
|
|
7
7
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
8
8
|
|
|
9
|
-
const meta: Meta<
|
|
9
|
+
const meta: Meta<LocationFilterProps> = {
|
|
10
10
|
title: 'Input/LocationFilter',
|
|
11
11
|
component: LocationFilter,
|
|
12
12
|
parameters: {
|
|
@@ -32,7 +32,26 @@ const meta: Meta<typeof LocationFilter> = {
|
|
|
32
32
|
},
|
|
33
33
|
},
|
|
34
34
|
args: {
|
|
35
|
+
width: '100%',
|
|
35
36
|
fields: ['region', 'country', 'division', 'location'],
|
|
37
|
+
initialValue: 'United States',
|
|
38
|
+
},
|
|
39
|
+
argTypes: {
|
|
40
|
+
fields: {
|
|
41
|
+
control: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
initialValue: {
|
|
46
|
+
control: {
|
|
47
|
+
type: 'text',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
width: {
|
|
51
|
+
control: {
|
|
52
|
+
type: 'text',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
36
55
|
},
|
|
37
56
|
decorators: [withActions],
|
|
38
57
|
};
|
|
@@ -41,10 +60,8 @@ export default meta;
|
|
|
41
60
|
|
|
42
61
|
export const Primary: StoryObj<LocationFilterProps> = {
|
|
43
62
|
render: (args) => (
|
|
44
|
-
<
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
</LapisUrlContext.Provider>
|
|
48
|
-
</div>
|
|
63
|
+
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
64
|
+
<LocationFilter fields={args.fields} initialValue={args.initialValue} width={args.width} />
|
|
65
|
+
</LapisUrlContext.Provider>
|
|
49
66
|
),
|
|
50
67
|
};
|
|
@@ -1,15 +1,36 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'preact';
|
|
1
2
|
import { useContext, useRef, useState } from 'preact/hooks';
|
|
2
3
|
|
|
3
4
|
import { fetchAutocompletionList } from './fetchAutocompletionList';
|
|
4
5
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
6
|
+
import { ErrorBoundary } from '../components/error-boundary';
|
|
7
|
+
import { ErrorDisplay } from '../components/error-display';
|
|
8
|
+
import { LoadingDisplay } from '../components/loading-display';
|
|
9
|
+
import { ResizeContainer } from '../components/resize-container';
|
|
5
10
|
import { useQuery } from '../useQuery';
|
|
6
11
|
|
|
7
|
-
export
|
|
12
|
+
export interface LocationFilterInnerProps {
|
|
8
13
|
initialValue?: string;
|
|
9
14
|
fields: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LocationFilterProps extends LocationFilterInnerProps {
|
|
18
|
+
width: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const LocationFilter: FunctionComponent<LocationFilterProps> = ({ width, initialValue, fields }) => {
|
|
22
|
+
const size = { width, height: '3rem' };
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<ErrorBoundary size={size}>
|
|
26
|
+
<ResizeContainer size={size}>
|
|
27
|
+
<LocationFilterInner initialValue={initialValue} fields={fields} />
|
|
28
|
+
</ResizeContainer>
|
|
29
|
+
</ErrorBoundary>
|
|
30
|
+
);
|
|
10
31
|
};
|
|
11
32
|
|
|
12
|
-
export const
|
|
33
|
+
export const LocationFilterInner = ({ initialValue, fields }: LocationFilterInnerProps) => {
|
|
13
34
|
const lapis = useContext(LapisUrlContext);
|
|
14
35
|
|
|
15
36
|
const [value, setValue] = useState(initialValue ?? '');
|
|
@@ -19,22 +40,11 @@ export const LocationFilter = ({ initialValue, fields }: LocationFilterProps) =>
|
|
|
19
40
|
|
|
20
41
|
const { data, error, isLoading } = useQuery(() => fetchAutocompletionList(fields, lapis), [fields, lapis]);
|
|
21
42
|
|
|
22
|
-
if (isLoading)
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
<input type='text' class='input input-bordered grow' value={value} disabled />
|
|
26
|
-
<button class='btn ml-1' disabled type='submit'>
|
|
27
|
-
Loading...
|
|
28
|
-
</button>
|
|
29
|
-
</form>
|
|
30
|
-
);
|
|
31
|
-
|
|
43
|
+
if (isLoading) {
|
|
44
|
+
return <LoadingDisplay />;
|
|
45
|
+
}
|
|
32
46
|
if (error) {
|
|
33
|
-
return
|
|
34
|
-
<p>
|
|
35
|
-
Error: {error.name} {error.message} {error.stack}
|
|
36
|
-
</p>
|
|
37
|
-
);
|
|
47
|
+
return <ErrorDisplay error={error} />;
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
const onInput = (event: InputEvent) => {
|
|
@@ -70,7 +80,7 @@ export const LocationFilter = ({ initialValue, fields }: LocationFilterProps) =>
|
|
|
70
80
|
};
|
|
71
81
|
|
|
72
82
|
return (
|
|
73
|
-
<form class='flex' onSubmit={submit} ref={formRef}>
|
|
83
|
+
<form class='flex w-full' onSubmit={submit} ref={formRef}>
|
|
74
84
|
<input
|
|
75
85
|
type='text'
|
|
76
86
|
class={`input input-bordered grow ${unknownLocation ? 'border-2 border-error' : ''}`}
|
|
@@ -27,7 +27,8 @@ const meta: Meta<MutationComparisonProps> = {
|
|
|
27
27
|
options: ['table', 'venn'],
|
|
28
28
|
control: { type: 'check' },
|
|
29
29
|
},
|
|
30
|
-
|
|
30
|
+
width: { control: 'text' },
|
|
31
|
+
height: { control: 'text' },
|
|
31
32
|
headline: { control: 'text' },
|
|
32
33
|
},
|
|
33
34
|
parameters: {
|
|
@@ -81,7 +82,8 @@ const Template: StoryObj<MutationComparisonProps> = {
|
|
|
81
82
|
variants={args.variants}
|
|
82
83
|
sequenceType={args.sequenceType}
|
|
83
84
|
views={args.views}
|
|
84
|
-
|
|
85
|
+
width={args.width}
|
|
86
|
+
height={args.height}
|
|
85
87
|
headline={args.headline}
|
|
86
88
|
/>
|
|
87
89
|
</ReferenceGenomeContext.Provider>
|
|
@@ -109,7 +111,8 @@ export const TwoVariants: StoryObj<MutationComparisonProps> = {
|
|
|
109
111
|
],
|
|
110
112
|
sequenceType: 'nucleotide',
|
|
111
113
|
views: ['table', 'venn'],
|
|
112
|
-
|
|
114
|
+
width: '100%',
|
|
115
|
+
height: '700px',
|
|
113
116
|
headline: 'Mutation comparison',
|
|
114
117
|
},
|
|
115
118
|
};
|