@genspectrum/dashboard-components 0.1.4 → 0.2.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 +1021 -804
- package/dist/dashboard-components.js +647 -218
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +336 -126
- package/dist/style.css +214 -36
- package/package.json +4 -4
- package/src/preact/aggregatedData/aggregate.stories.tsx +2 -0
- package/src/preact/aggregatedData/aggregate.tsx +33 -28
- package/src/preact/components/error-boundary.stories.tsx +62 -0
- package/src/preact/components/error-boundary.tsx +31 -0
- package/src/preact/components/error-display.stories.tsx +24 -3
- package/src/preact/components/error-display.tsx +14 -1
- package/src/preact/components/headline.stories.tsx +19 -1
- package/src/preact/components/headline.tsx +9 -1
- package/src/preact/components/info.stories.tsx +24 -3
- package/src/preact/components/info.tsx +49 -5
- 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/dateRangeSelector/date-range-selector.stories.tsx +17 -0
- package/src/preact/dateRangeSelector/date-range-selector.tsx +43 -15
- package/src/preact/locationFilter/location-filter.stories.tsx +23 -6
- package/src/preact/locationFilter/location-filter.tsx +29 -18
- package/src/preact/mutationComparison/mutation-comparison.stories.tsx +3 -0
- package/src/preact/mutationComparison/mutation-comparison.tsx +31 -27
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +17 -2
- package/src/preact/mutationFilter/mutation-filter.tsx +26 -8
- package/src/preact/mutations/mutations.stories.tsx +3 -0
- package/src/preact/mutations/mutations.tsx +32 -26
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +4 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +57 -31
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +3 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +89 -32
- package/src/preact/textInput/text-input.tsx +26 -3
- package/src/web-components/app.stories.ts +1 -2
- package/src/web-components/app.ts +4 -2
- 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} +35 -3
- package/src/web-components/input/gs-date-range-selector.tsx +110 -0
- package/src/web-components/input/{location-filter-component.stories.ts → gs-location-filter.stories.ts} +29 -4
- package/src/web-components/input/{location-filter-component.tsx → gs-location-filter.tsx} +12 -1
- package/src/web-components/input/{mutation-filter-component.stories.ts → gs-mutation-filter.stories.ts} +30 -4
- package/src/web-components/input/gs-mutation-filter.tsx +114 -0
- package/src/web-components/input/{text-input-component.stories.ts → gs-text-input.stories.ts} +42 -3
- package/src/web-components/input/gs-text-input.tsx +73 -0
- package/src/web-components/input/index.ts +4 -4
- 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} +8 -6
- package/src/web-components/{display/aggregate-component.tsx → visualization/gs-aggregate.tsx} +16 -2
- package/src/web-components/{display/mutation-comparison-component.stories.ts → visualization/gs-mutation-comparison.stories.ts} +11 -9
- package/src/web-components/{display/mutation-comparison-component.tsx → visualization/gs-mutation-comparison.tsx} +8 -1
- package/src/web-components/{display/mutations-component.stories.ts → visualization/gs-mutations.stories.ts} +30 -11
- package/src/web-components/visualization/gs-mutations.tsx +94 -0
- package/src/web-components/{display/prevalence-over-time-component.stories.ts → visualization/gs-prevalence-over-time.stories.ts} +24 -1
- package/src/web-components/visualization/gs-prevalence-over-time.tsx +148 -0
- package/src/web-components/{display/relative-growth-advantage-component.stories.ts → visualization/gs-relative-growth-advantage.stories.ts} +21 -1
- package/src/web-components/visualization/gs-relative-growth-advantage.tsx +100 -0
- package/src/web-components/visualization/index.ts +5 -0
- package/src/web-components/display/index.ts +0 -5
- package/src/web-components/display/mutations-component.tsx +0 -40
- package/src/web-components/display/prevalence-over-time-component.tsx +0 -58
- package/src/web-components/display/relative-growth-advantage-component.tsx +0 -49
- package/src/web-components/input/date-range-selector-component.tsx +0 -46
- package/src/web-components/input/mutation-filter-component.tsx +0 -35
- package/src/web-components/input/text-input-component.tsx +0 -39
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
2
|
import { expect, 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,31 @@ const meta: Meta = {
|
|
|
12
13
|
export default meta;
|
|
13
14
|
|
|
14
15
|
export const ErrorStory: StoryObj = {
|
|
15
|
-
render: () =>
|
|
16
|
+
render: () => (
|
|
17
|
+
<ResizeContainer defaultSize={{ 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 defaultSize={{ height: '600px', width: '100%' }}>
|
|
33
|
+
<ErrorDisplay error={new UserFacingError('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
|
+
await waitFor(() => expect(error).toBeInTheDocument());
|
|
41
|
+
await waitFor(() => expect(canvas.getByText('some message')).toBeInTheDocument());
|
|
21
42
|
},
|
|
22
43
|
};
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
|
|
3
|
+
export class UserFacingError extends Error {
|
|
4
|
+
constructor(message: string) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'UserFacingError';
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
3
10
|
export const ErrorDisplay: FunctionComponent<{ error: Error }> = ({ error }) => {
|
|
4
|
-
return
|
|
11
|
+
return (
|
|
12
|
+
<div className='h-full w-full rounded-md border-2 border-gray-100 p-2 flex items-center justify-center flex-col'>
|
|
13
|
+
<div className='text-red-700 font-bold'>Error</div>
|
|
14
|
+
<div>Oops! Something went wrong.</div>
|
|
15
|
+
{error instanceof UserFacingError && <div className='text-sm text-gray-600'>{error.message}</div>}
|
|
16
|
+
</div>
|
|
17
|
+
);
|
|
5
18
|
};
|
|
@@ -3,10 +3,13 @@ import { expect, within } from '@storybook/test';
|
|
|
3
3
|
|
|
4
4
|
import Headline, { type HeadlineProps } from './headline';
|
|
5
5
|
|
|
6
|
-
const meta: Meta<
|
|
6
|
+
const meta: Meta<HeadlineProps> = {
|
|
7
7
|
title: 'Component/Headline',
|
|
8
8
|
component: Headline,
|
|
9
9
|
parameters: { fetchMock: {} },
|
|
10
|
+
argTypes: {
|
|
11
|
+
heading: { control: 'text' },
|
|
12
|
+
},
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
export default meta;
|
|
@@ -27,3 +30,18 @@ export const HeadlineStory: StoryObj<HeadlineProps> = {
|
|
|
27
30
|
await expect(canvas.getByText('Some Content')).toBeInTheDocument();
|
|
28
31
|
},
|
|
29
32
|
};
|
|
33
|
+
|
|
34
|
+
export const NoHeadlineStory: StoryObj<HeadlineProps> = {
|
|
35
|
+
render: (args) => (
|
|
36
|
+
<Headline {...args}>
|
|
37
|
+
<div class='flex justify-center px-4 py-16 bg-base-200'>Some Content</div>
|
|
38
|
+
</Headline>
|
|
39
|
+
),
|
|
40
|
+
args: {},
|
|
41
|
+
play: async ({ canvasElement }) => {
|
|
42
|
+
const canvas = within(canvasElement);
|
|
43
|
+
|
|
44
|
+
await expect(canvas.queryByText('My Headline')).not.toBeInTheDocument();
|
|
45
|
+
await expect(canvas.getByText('Some Content')).toBeInTheDocument();
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -2,10 +2,18 @@ import { type FunctionComponent } from 'preact';
|
|
|
2
2
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
3
3
|
|
|
4
4
|
export interface HeadlineProps {
|
|
5
|
-
heading
|
|
5
|
+
heading?: string;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
const Headline: FunctionComponent<HeadlineProps> = ({ heading, children }) => {
|
|
9
|
+
if (!heading) {
|
|
10
|
+
return <>{children}</>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return <ResizingHeadline heading={heading}>{children}</ResizingHeadline>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ResizingHeadline: FunctionComponent<HeadlineProps> = ({ heading, children }) => {
|
|
9
17
|
const ref = useRef<HTMLHeadingElement>(null);
|
|
10
18
|
|
|
11
19
|
const [h1Height, setH1Height] = useState('2rem');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
+
import { expect, fireEvent, waitFor, within } from '@storybook/test';
|
|
2
3
|
|
|
3
4
|
import Info, { type InfoProps } from './info';
|
|
4
5
|
|
|
@@ -7,16 +8,36 @@ const meta: Meta<InfoProps> = {
|
|
|
7
8
|
component: Info,
|
|
8
9
|
parameters: { fetchMock: {} },
|
|
9
10
|
args: {
|
|
10
|
-
|
|
11
|
+
size: { width: '400px', height: '100px' },
|
|
11
12
|
},
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
export default meta;
|
|
15
16
|
|
|
17
|
+
const tooltipText = 'This is a tooltip which shows some information.';
|
|
18
|
+
|
|
16
19
|
export const InfoStory: StoryObj<InfoProps> = {
|
|
17
20
|
render: (args) => (
|
|
18
|
-
<div class='flex justify-center px-4 py-16
|
|
19
|
-
<Info {...args}
|
|
21
|
+
<div class='flex justify-center px-4 py-16'>
|
|
22
|
+
<Info {...args}>{tooltipText}</Info>
|
|
20
23
|
</div>
|
|
21
24
|
),
|
|
22
25
|
};
|
|
26
|
+
|
|
27
|
+
export const ShowsInfoOnClick: StoryObj<InfoProps> = {
|
|
28
|
+
...InfoStory,
|
|
29
|
+
play: async ({ canvasElement }) => {
|
|
30
|
+
const canvas = within(canvasElement);
|
|
31
|
+
const loading = canvas.getByRole('button', { name: '?' });
|
|
32
|
+
|
|
33
|
+
await waitFor(() => expect(loading).toBeInTheDocument());
|
|
34
|
+
|
|
35
|
+
await fireEvent.click(loading);
|
|
36
|
+
|
|
37
|
+
await waitFor(() => expect(canvas.getByText(tooltipText, { exact: false })).toBeInTheDocument());
|
|
38
|
+
|
|
39
|
+
await fireEvent.click(canvas.getByRole('button', { name: 'Close' }));
|
|
40
|
+
|
|
41
|
+
await waitFor(() => expect(canvas.queryByText(tooltipText, { exact: false })).not.toBeInTheDocument());
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -1,16 +1,60 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { useState } from 'preact/hooks';
|
|
2
3
|
|
|
3
4
|
export interface InfoProps {
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
size?: {
|
|
6
|
+
height?: string;
|
|
7
|
+
width?: string;
|
|
8
|
+
};
|
|
6
9
|
}
|
|
7
10
|
|
|
8
|
-
const Info: FunctionComponent<InfoProps> = ({
|
|
11
|
+
const Info: FunctionComponent<InfoProps> = ({ children, size }) => {
|
|
12
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
13
|
+
|
|
14
|
+
const toggleHelp = () => {
|
|
15
|
+
setShowHelp(!showHelp);
|
|
16
|
+
};
|
|
17
|
+
|
|
9
18
|
return (
|
|
10
|
-
<div
|
|
11
|
-
<button
|
|
19
|
+
<div className='relative'>
|
|
20
|
+
<button className='btn btn-xs' onClick={toggleHelp}>
|
|
21
|
+
?
|
|
22
|
+
</button>
|
|
23
|
+
{showHelp && (
|
|
24
|
+
<div
|
|
25
|
+
className='absolute top-8 right-6 bg-white p-2 border border-black flex flex-col overflow-auto shadow-lg rounded z-50'
|
|
26
|
+
style={size}
|
|
27
|
+
>
|
|
28
|
+
<div className='flex flex-col'>{children}</div>
|
|
29
|
+
<div className='flex justify-end'>
|
|
30
|
+
<button className='text-sm underline mt-2' onClick={toggleHelp}>
|
|
31
|
+
Close
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
)}
|
|
12
36
|
</div>
|
|
13
37
|
);
|
|
14
38
|
};
|
|
15
39
|
|
|
40
|
+
export const InfoHeadline1: FunctionComponent = ({ children }) => {
|
|
41
|
+
return <h1 className='text-lg font-bold'>{children}</h1>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const InfoHeadline2: FunctionComponent = ({ children }) => {
|
|
45
|
+
return <h2 className='text-base font-bold mt-4'>{children}</h2>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const InfoParagraph: FunctionComponent = ({ children }) => {
|
|
49
|
+
return <p className='text-justify my-1'>{children}</p>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const InfoLink: FunctionComponent<{ href: string }> = ({ children, href }) => {
|
|
53
|
+
return (
|
|
54
|
+
<a className='text-blue-600 hover:text-blue-800' href={href} target='_blank' rel='noopener noreferrer'>
|
|
55
|
+
{children}
|
|
56
|
+
</a>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
16
60
|
export default Info;
|
|
@@ -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 defaultSize={{ 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
|
};
|
|
@@ -40,11 +40,27 @@ 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
|
+
width: '100%',
|
|
48
64
|
},
|
|
49
65
|
decorators: [withActions],
|
|
50
66
|
};
|
|
@@ -58,6 +74,7 @@ export const Primary: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
|
58
74
|
customSelectOptions={args.customSelectOptions}
|
|
59
75
|
earliestDate={args.earliestDate}
|
|
60
76
|
initialValue={args.initialValue}
|
|
77
|
+
width={args.width}
|
|
61
78
|
/>
|
|
62
79
|
</LapisUrlContext.Provider>
|
|
63
80
|
),
|
|
@@ -3,12 +3,18 @@ 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;
|
|
@@ -38,9 +44,31 @@ export const DateRangeSelector = <CustomLabel extends string>({
|
|
|
38
44
|
customSelectOptions,
|
|
39
45
|
earliestDate = '1900-01-01',
|
|
40
46
|
initialValue,
|
|
47
|
+
width,
|
|
48
|
+
}: DateRangeSelectorProps<CustomLabel>) => {
|
|
49
|
+
const defaultSize = { width: '100%', height: '3rem' };
|
|
50
|
+
const size = width === undefined ? undefined : { width, height: defaultSize.height };
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<ErrorBoundary defaultSize={defaultSize} size={size}>
|
|
54
|
+
<ResizeContainer defaultSize={defaultSize} size={size}>
|
|
55
|
+
<DateRangeSelectorInner
|
|
56
|
+
customSelectOptions={customSelectOptions}
|
|
57
|
+
earliestDate={earliestDate}
|
|
58
|
+
initialValue={initialValue}
|
|
59
|
+
/>
|
|
60
|
+
</ResizeContainer>
|
|
61
|
+
</ErrorBoundary>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const DateRangeSelectorInner = <CustomLabel extends string>({
|
|
66
|
+
customSelectOptions,
|
|
67
|
+
earliestDate = '1900-01-01',
|
|
68
|
+
initialValue,
|
|
41
69
|
}: DateRangeSelectorProps<CustomLabel>) => {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
70
|
+
const fromDatePickerRef = useRef<HTMLInputElement>(null);
|
|
71
|
+
const toDatePickerRef = useRef<HTMLInputElement>(null);
|
|
44
72
|
const divRef = useRef<HTMLDivElement>(null);
|
|
45
73
|
const [dateFromPicker, setDateFromPicker] = useState<flatpickr.Instance | null>(null);
|
|
46
74
|
const [dateToPicker, setDateToPicker] = useState<flatpickr.Instance | null>(null);
|
|
@@ -64,18 +92,18 @@ export const DateRangeSelector = <CustomLabel extends string>({
|
|
|
64
92
|
dateFormat: 'Y-m-d',
|
|
65
93
|
};
|
|
66
94
|
|
|
67
|
-
if (
|
|
95
|
+
if (fromDatePickerRef.current) {
|
|
68
96
|
setDateFromPicker(
|
|
69
|
-
flatpickr(
|
|
97
|
+
flatpickr(fromDatePickerRef.current, {
|
|
70
98
|
...commonConfig,
|
|
71
99
|
defaultDate: selectedDates.dateFrom,
|
|
72
100
|
}),
|
|
73
101
|
);
|
|
74
102
|
}
|
|
75
103
|
|
|
76
|
-
if (
|
|
104
|
+
if (toDatePickerRef.current) {
|
|
77
105
|
setDateToPicker(
|
|
78
|
-
flatpickr(
|
|
106
|
+
flatpickr(toDatePickerRef.current, {
|
|
79
107
|
...commonConfig,
|
|
80
108
|
defaultDate: selectedDates.dateTo,
|
|
81
109
|
}),
|
|
@@ -87,7 +115,7 @@ export const DateRangeSelector = <CustomLabel extends string>({
|
|
|
87
115
|
dateToPicker?.destroy();
|
|
88
116
|
};
|
|
89
117
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
90
|
-
}, [
|
|
118
|
+
}, [fromDatePickerRef, toDatePickerRef]);
|
|
91
119
|
|
|
92
120
|
const onSelectChange = (value: CustomLabel | PresetOptionValues) => {
|
|
93
121
|
setSelectedDateRange(value);
|
|
@@ -151,11 +179,11 @@ export const DateRangeSelector = <CustomLabel extends string>({
|
|
|
151
179
|
};
|
|
152
180
|
|
|
153
181
|
return (
|
|
154
|
-
<div class='join' ref={divRef}>
|
|
182
|
+
<div class='join w-full' ref={divRef}>
|
|
155
183
|
<Select
|
|
156
184
|
items={selectableOptions}
|
|
157
185
|
selected={selectedDateRange}
|
|
158
|
-
selectStyle='select-bordered rounded-none join-item'
|
|
186
|
+
selectStyle='select-bordered rounded-none join-item grow'
|
|
159
187
|
onChange={(event: Event) => {
|
|
160
188
|
event.preventDefault();
|
|
161
189
|
const select = event.target as HTMLSelectElement;
|
|
@@ -164,20 +192,20 @@ export const DateRangeSelector = <CustomLabel extends string>({
|
|
|
164
192
|
}}
|
|
165
193
|
/>
|
|
166
194
|
<input
|
|
167
|
-
class='input input-bordered rounded-none join-item'
|
|
195
|
+
class='input input-bordered rounded-none join-item grow'
|
|
168
196
|
type='text'
|
|
169
197
|
placeholder='Date from'
|
|
170
|
-
ref={
|
|
198
|
+
ref={fromDatePickerRef}
|
|
171
199
|
onChange={onChangeDateFrom}
|
|
172
200
|
onBlur={onChangeDateFrom}
|
|
173
201
|
/>
|
|
174
202
|
<input
|
|
175
|
-
class='input input-bordered rounded-none join-item'
|
|
203
|
+
class='input input-bordered rounded-none join-item grow'
|
|
176
204
|
type='text'
|
|
177
205
|
placeholder='Date to'
|
|
178
|
-
ref={
|
|
206
|
+
ref={toDatePickerRef}
|
|
179
207
|
onChange={onChangeDateTo}
|
|
180
|
-
onBlur={
|
|
208
|
+
onBlur={onChangeDateTo}
|
|
181
209
|
/>
|
|
182
210
|
</div>
|
|
183
211
|
);
|
|
@@ -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,37 @@
|
|
|
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 defaultSize = { width: '100%', height: '3rem' };
|
|
23
|
+
const size = width === undefined ? undefined : { width, height: defaultSize.height };
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<ErrorBoundary defaultSize={defaultSize} size={size}>
|
|
27
|
+
<ResizeContainer size={size} defaultSize={defaultSize}>
|
|
28
|
+
<LocationFilterInner initialValue={initialValue} fields={fields} />
|
|
29
|
+
</ResizeContainer>
|
|
30
|
+
</ErrorBoundary>
|
|
31
|
+
);
|
|
10
32
|
};
|
|
11
33
|
|
|
12
|
-
export const
|
|
34
|
+
export const LocationFilterInner = ({ initialValue, fields }: LocationFilterInnerProps) => {
|
|
13
35
|
const lapis = useContext(LapisUrlContext);
|
|
14
36
|
|
|
15
37
|
const [value, setValue] = useState(initialValue ?? '');
|
|
@@ -19,22 +41,11 @@ export const LocationFilter = ({ initialValue, fields }: LocationFilterProps) =>
|
|
|
19
41
|
|
|
20
42
|
const { data, error, isLoading } = useQuery(() => fetchAutocompletionList(fields, lapis), [fields, lapis]);
|
|
21
43
|
|
|
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
|
-
|
|
44
|
+
if (isLoading) {
|
|
45
|
+
return <LoadingDisplay />;
|
|
46
|
+
}
|
|
32
47
|
if (error) {
|
|
33
|
-
return
|
|
34
|
-
<p>
|
|
35
|
-
Error: {error.name} {error.message} {error.stack}
|
|
36
|
-
</p>
|
|
37
|
-
);
|
|
48
|
+
return <ErrorDisplay error={error} />;
|
|
38
49
|
}
|
|
39
50
|
|
|
40
51
|
const onInput = (event: InputEvent) => {
|
|
@@ -70,7 +81,7 @@ export const LocationFilter = ({ initialValue, fields }: LocationFilterProps) =>
|
|
|
70
81
|
};
|
|
71
82
|
|
|
72
83
|
return (
|
|
73
|
-
<form class='flex' onSubmit={submit} ref={formRef}>
|
|
84
|
+
<form class='flex w-full' onSubmit={submit} ref={formRef}>
|
|
74
85
|
<input
|
|
75
86
|
type='text'
|
|
76
87
|
class={`input input-bordered grow ${unknownLocation ? 'border-2 border-error' : ''}`}
|
|
@@ -28,6 +28,7 @@ const meta: Meta<MutationComparisonProps> = {
|
|
|
28
28
|
control: { type: 'check' },
|
|
29
29
|
},
|
|
30
30
|
size: [{ control: 'object' }],
|
|
31
|
+
headline: { control: 'text' },
|
|
31
32
|
},
|
|
32
33
|
parameters: {
|
|
33
34
|
fetchMock: {
|
|
@@ -81,6 +82,7 @@ const Template: StoryObj<MutationComparisonProps> = {
|
|
|
81
82
|
sequenceType={args.sequenceType}
|
|
82
83
|
views={args.views}
|
|
83
84
|
size={args.size}
|
|
85
|
+
headline={args.headline}
|
|
84
86
|
/>
|
|
85
87
|
</ReferenceGenomeContext.Provider>
|
|
86
88
|
</LapisUrlContext.Provider>
|
|
@@ -108,6 +110,7 @@ export const TwoVariants: StoryObj<MutationComparisonProps> = {
|
|
|
108
110
|
sequenceType: 'nucleotide',
|
|
109
111
|
views: ['table', 'venn'],
|
|
110
112
|
size: { width: '100%', height: '700px' },
|
|
113
|
+
headline: 'Mutation comparison',
|
|
111
114
|
},
|
|
112
115
|
};
|
|
113
116
|
|