@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.
Files changed (65) hide show
  1. package/custom-elements.json +1021 -804
  2. package/dist/dashboard-components.js +647 -218
  3. package/dist/dashboard-components.js.map +1 -1
  4. package/dist/genspectrum-components.d.ts +336 -126
  5. package/dist/style.css +214 -36
  6. package/package.json +4 -4
  7. package/src/preact/aggregatedData/aggregate.stories.tsx +2 -0
  8. package/src/preact/aggregatedData/aggregate.tsx +33 -28
  9. package/src/preact/components/error-boundary.stories.tsx +62 -0
  10. package/src/preact/components/error-boundary.tsx +31 -0
  11. package/src/preact/components/error-display.stories.tsx +24 -3
  12. package/src/preact/components/error-display.tsx +14 -1
  13. package/src/preact/components/headline.stories.tsx +19 -1
  14. package/src/preact/components/headline.tsx +9 -1
  15. package/src/preact/components/info.stories.tsx +24 -3
  16. package/src/preact/components/info.tsx +49 -5
  17. package/src/preact/components/loading-display.stories.tsx +6 -6
  18. package/src/preact/components/loading-display.tsx +1 -1
  19. package/src/preact/components/no-data-display.tsx +5 -1
  20. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +17 -0
  21. package/src/preact/dateRangeSelector/date-range-selector.tsx +43 -15
  22. package/src/preact/locationFilter/location-filter.stories.tsx +23 -6
  23. package/src/preact/locationFilter/location-filter.tsx +29 -18
  24. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +3 -0
  25. package/src/preact/mutationComparison/mutation-comparison.tsx +31 -27
  26. package/src/preact/mutationFilter/mutation-filter.stories.tsx +17 -2
  27. package/src/preact/mutationFilter/mutation-filter.tsx +26 -8
  28. package/src/preact/mutations/mutations.stories.tsx +3 -0
  29. package/src/preact/mutations/mutations.tsx +32 -26
  30. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +4 -0
  31. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +57 -31
  32. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +3 -0
  33. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +89 -32
  34. package/src/preact/textInput/text-input.tsx +26 -3
  35. package/src/web-components/app.stories.ts +1 -2
  36. package/src/web-components/app.ts +4 -2
  37. package/src/web-components/index.ts +1 -1
  38. package/src/web-components/input/{date-range-selector-component.stories.ts → gs-date-range-selector.stories.ts} +35 -3
  39. package/src/web-components/input/gs-date-range-selector.tsx +110 -0
  40. package/src/web-components/input/{location-filter-component.stories.ts → gs-location-filter.stories.ts} +29 -4
  41. package/src/web-components/input/{location-filter-component.tsx → gs-location-filter.tsx} +12 -1
  42. package/src/web-components/input/{mutation-filter-component.stories.ts → gs-mutation-filter.stories.ts} +30 -4
  43. package/src/web-components/input/gs-mutation-filter.tsx +114 -0
  44. package/src/web-components/input/{text-input-component.stories.ts → gs-text-input.stories.ts} +42 -3
  45. package/src/web-components/input/gs-text-input.tsx +73 -0
  46. package/src/web-components/input/index.ts +4 -4
  47. package/src/web-components/visualization/data_visualization_statistical_analysis.mdx +26 -0
  48. package/src/web-components/{display/aggregate-component.stories.ts → visualization/gs-aggregate.stories.ts} +8 -6
  49. package/src/web-components/{display/aggregate-component.tsx → visualization/gs-aggregate.tsx} +16 -2
  50. package/src/web-components/{display/mutation-comparison-component.stories.ts → visualization/gs-mutation-comparison.stories.ts} +11 -9
  51. package/src/web-components/{display/mutation-comparison-component.tsx → visualization/gs-mutation-comparison.tsx} +8 -1
  52. package/src/web-components/{display/mutations-component.stories.ts → visualization/gs-mutations.stories.ts} +30 -11
  53. package/src/web-components/visualization/gs-mutations.tsx +94 -0
  54. package/src/web-components/{display/prevalence-over-time-component.stories.ts → visualization/gs-prevalence-over-time.stories.ts} +24 -1
  55. package/src/web-components/visualization/gs-prevalence-over-time.tsx +148 -0
  56. package/src/web-components/{display/relative-growth-advantage-component.stories.ts → visualization/gs-relative-growth-advantage.stories.ts} +21 -1
  57. package/src/web-components/visualization/gs-relative-growth-advantage.tsx +100 -0
  58. package/src/web-components/visualization/index.ts +5 -0
  59. package/src/web-components/display/index.ts +0 -5
  60. package/src/web-components/display/mutations-component.tsx +0 -40
  61. package/src/web-components/display/prevalence-over-time-component.tsx +0 -58
  62. package/src/web-components/display/relative-growth-advantage-component.tsx +0 -49
  63. package/src/web-components/input/date-range-selector-component.tsx +0 -46
  64. package/src/web-components/input/mutation-filter-component.tsx +0 -35
  65. 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: () => <ErrorDisplay error={new Error('some message')} />,
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('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 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 <div>Error: {error.message}</div>;
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<typeof Headline> = {
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: string;
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
- content: 'This is a tooltip which shows some information.',
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 bg-base-200'>
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
- content: string;
5
- className?: string;
5
+ size?: {
6
+ height?: string;
7
+ width?: string;
8
+ };
6
9
  }
7
10
 
8
- const Info: FunctionComponent<InfoProps> = ({ content, className }) => {
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 class={`${className} tooltip`} data-tip={content}>
11
- <button class='btn btn-xs'>?</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
- 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 defaultSize={{ 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
  };
@@ -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 datePickerRef = useRef<HTMLInputElement>(null);
43
- const endDatePickerRef = useRef<HTMLInputElement>(null);
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 (datePickerRef.current) {
95
+ if (fromDatePickerRef.current) {
68
96
  setDateFromPicker(
69
- flatpickr(datePickerRef.current, {
97
+ flatpickr(fromDatePickerRef.current, {
70
98
  ...commonConfig,
71
99
  defaultDate: selectedDates.dateFrom,
72
100
  }),
73
101
  );
74
102
  }
75
103
 
76
- if (endDatePickerRef.current) {
104
+ if (toDatePickerRef.current) {
77
105
  setDateToPicker(
78
- flatpickr(endDatePickerRef.current, {
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
- }, [datePickerRef, endDatePickerRef]);
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={datePickerRef}
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={endDatePickerRef}
206
+ ref={toDatePickerRef}
179
207
  onChange={onChangeDateTo}
180
- onBlur={onChangeDateFrom}
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<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,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 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 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 LocationFilter = ({ initialValue, fields }: LocationFilterProps) => {
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
- <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
-
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