@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.
Files changed (69) hide show
  1. package/custom-elements.json +1161 -928
  2. package/dist/dashboard-components.js +663 -237
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +177 -140
  5. package/dist/style.css +247 -50
  6. package/package.json +2 -3
  7. package/src/constants.ts +1 -1
  8. package/src/lapisApi/lapisApi.ts +46 -2
  9. package/src/lapisApi/lapisTypes.ts +14 -0
  10. package/src/preact/aggregatedData/aggregate.stories.tsx +4 -2
  11. package/src/preact/aggregatedData/aggregate.tsx +31 -29
  12. package/src/preact/components/error-boundary.stories.tsx +54 -0
  13. package/src/preact/components/error-boundary.tsx +22 -0
  14. package/src/preact/components/error-display.stories.tsx +32 -4
  15. package/src/preact/components/error-display.tsx +48 -1
  16. package/src/preact/components/loading-display.stories.tsx +6 -6
  17. package/src/preact/components/loading-display.tsx +1 -1
  18. package/src/preact/components/no-data-display.tsx +5 -1
  19. package/src/preact/components/resize-container.tsx +5 -14
  20. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +19 -0
  21. package/src/preact/dateRangeSelector/date-range-selector.tsx +38 -7
  22. package/src/preact/locationFilter/fetchAutocompletionList.ts +15 -1
  23. package/src/preact/locationFilter/location-filter.stories.tsx +23 -6
  24. package/src/preact/locationFilter/location-filter.tsx +28 -18
  25. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +6 -3
  26. package/src/preact/mutationComparison/mutation-comparison.tsx +33 -32
  27. package/src/preact/mutationComparison/queryMutationData.ts +2 -3
  28. package/src/preact/mutationFilter/mutation-filter.stories.tsx +18 -3
  29. package/src/preact/mutationFilter/mutation-filter.tsx +26 -7
  30. package/src/preact/mutations/mutations.stories.tsx +6 -3
  31. package/src/preact/mutations/mutations.tsx +28 -26
  32. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +14 -7
  33. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +50 -32
  34. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +6 -3
  35. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +46 -32
  36. package/src/preact/textInput/text-input.stories.tsx +26 -0
  37. package/src/preact/textInput/text-input.tsx +25 -3
  38. package/src/query/queryPrevalenceOverTime.ts +4 -10
  39. package/src/types.ts +4 -1
  40. package/src/web-components/ResizeContainer.mdx +13 -0
  41. package/src/web-components/app.stories.ts +1 -2
  42. package/src/web-components/app.ts +7 -3
  43. package/src/web-components/index.ts +1 -1
  44. package/src/web-components/input/{date-range-selector-component.stories.ts → gs-date-range-selector.stories.ts} +29 -4
  45. package/src/web-components/input/{date-range-selector-component.tsx → gs-date-range-selector.tsx} +32 -10
  46. package/src/web-components/input/{location-filter-component.stories.ts → gs-location-filter.stories.ts} +32 -5
  47. package/src/web-components/input/{location-filter-component.tsx → gs-location-filter.tsx} +11 -1
  48. package/src/web-components/input/{mutation-filter-component.stories.ts → gs-mutation-filter.stories.ts} +23 -4
  49. package/src/web-components/input/gs-mutation-filter.tsx +126 -0
  50. package/src/web-components/input/{text-input-component.stories.ts → gs-text-input.stories.ts} +34 -6
  51. package/src/web-components/input/{text-input-component.tsx → gs-text-input.tsx} +16 -4
  52. package/src/web-components/input/index.ts +4 -4
  53. package/src/web-components/input/introduction.mdx +11 -0
  54. package/src/web-components/introduction.mdx +15 -0
  55. package/src/web-components/visualization/data_visualization_statistical_analysis.mdx +26 -0
  56. package/src/web-components/{display/aggregate-component.stories.ts → visualization/gs-aggregate.stories.ts} +23 -11
  57. package/src/web-components/visualization/gs-aggregate.tsx +88 -0
  58. package/src/web-components/{display/mutation-comparison-component.stories.ts → visualization/gs-mutation-comparison.stories.ts} +21 -16
  59. package/src/web-components/{display/mutation-comparison-component.tsx → visualization/gs-mutation-comparison.tsx} +27 -18
  60. package/src/web-components/{display/mutations-component.stories.ts → visualization/gs-mutations.stories.ts} +20 -15
  61. package/src/web-components/{display/mutations-component.tsx → visualization/gs-mutations.tsx} +20 -10
  62. package/src/web-components/{display/prevalence-over-time-component.stories.ts → visualization/gs-prevalence-over-time.stories.ts} +29 -20
  63. package/src/web-components/{display/prevalence-over-time-component.tsx → visualization/gs-prevalence-over-time.tsx} +47 -22
  64. package/src/web-components/{display/relative-growth-advantage-component.stories.ts → visualization/gs-relative-growth-advantage.stories.ts} +12 -7
  65. package/src/web-components/{display/relative-growth-advantage-component.tsx → visualization/gs-relative-growth-advantage.tsx} +21 -9
  66. package/src/web-components/visualization/index.ts +5 -0
  67. package/src/web-components/display/aggregate-component.tsx +0 -72
  68. package/src/web-components/display/index.ts +0 -5
  69. 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: () => <ErrorDisplay error={new Error('some message')} />,
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('Error: ', { exact: false });
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
- return <div>Error: {error.message}</div>;
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
- play: async ({ canvasElement }) => {
16
- const canvas = within(canvasElement);
17
- const loading = canvas.getByText('Loading...');
18
- await waitFor(() => expect(loading).toBeInTheDocument());
19
- },
15
+ render: () => (
16
+ <ResizeContainer size={{ height: '600px', width: '100%' }}>
17
+ <LoadingDisplay />
18
+ </ResizeContainer>
19
+ ),
20
20
  };
@@ -1,5 +1,5 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
 
3
3
  export const LoadingDisplay: FunctionComponent = () => {
4
- return <div>Loading...</div>;
4
+ return <div aria-label={'Loading'} className='h-full w-full skeleton' />;
5
5
  };
@@ -1,5 +1,9 @@
1
1
  import { type FunctionComponent } from 'preact';
2
2
 
3
3
  export const NoDataDisplay: FunctionComponent = () => {
4
- return <div>No data available.</div>;
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?: string;
5
- height?: string;
4
+ width: string;
5
+ height: string;
6
6
  };
7
7
 
8
8
  export interface ResizeContainerProps {
9
- size?: Size;
10
- defaultSize: Size;
9
+ size: Size;
11
10
  }
12
11
 
13
- export const ResizeContainer: FunctionComponent<ResizeContainerProps> = ({ children, size, defaultSize }) => {
14
- return <div style={extendByDefault(size, defaultSize)}>{children}</div>;
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
- const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
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<typeof LocationFilter> = {
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
- <div class='max-w-screen-lg'>
45
- <LapisUrlContext.Provider value={LAPIS_URL}>
46
- <LocationFilter fields={args.fields} />
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 type LocationFilterProps = {
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 LocationFilter = ({ initialValue, fields }: LocationFilterProps) => {
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
- <form class='flex'>
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
- size: [{ control: 'object' }],
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
- size={args.size}
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
- size: { width: '100%', height: '700px' },
114
+ width: '100%',
115
+ height: '700px',
113
116
  headline: 'Mutation comparison',
114
117
  },
115
118
  };