@genspectrum/dashboard-components 0.12.1 → 0.13.1

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 (63) hide show
  1. package/custom-elements.json +292 -25
  2. package/dist/{LocationChangedEvent-CORvQvXv.js → LineageFilterChangedEvent-GedKNGFI.js} +25 -3
  3. package/dist/LineageFilterChangedEvent-GedKNGFI.js.map +1 -0
  4. package/dist/assets/mutationOverTimeWorker-B1-WrM4b.js.map +1 -0
  5. package/dist/components.d.ts +124 -25
  6. package/dist/components.js +765 -572
  7. package/dist/components.js.map +1 -1
  8. package/dist/style.css +3 -0
  9. package/dist/util.d.ts +48 -18
  10. package/dist/util.js +3 -1
  11. package/package.json +2 -2
  12. package/src/constants.ts +6 -0
  13. package/src/lapisApi/__mockData__/wiseReferenceGenome.json +9 -0
  14. package/src/lapisApi/lapisApi.ts +17 -0
  15. package/src/lapisApi/lapisTypes.ts +7 -1
  16. package/src/operator/FetchDetailsOperator.ts +28 -0
  17. package/src/preact/components/downshift-combobox.tsx +145 -0
  18. package/src/preact/components/tabs.tsx +1 -1
  19. package/src/preact/lineageFilter/LineageFilterChangedEvent.ts +11 -0
  20. package/src/preact/lineageFilter/fetchLineageAutocompleteList.spec.ts +16 -2
  21. package/src/preact/lineageFilter/fetchLineageAutocompleteList.ts +13 -2
  22. package/src/preact/lineageFilter/lineage-filter.stories.tsx +110 -9
  23. package/src/preact/lineageFilter/lineage-filter.tsx +40 -50
  24. package/src/preact/locationFilter/LocationChangedEvent.ts +1 -1
  25. package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +6 -2
  26. package/src/preact/locationFilter/fetchAutocompletionList.ts +16 -6
  27. package/src/preact/locationFilter/location-filter.stories.tsx +33 -30
  28. package/src/preact/locationFilter/location-filter.tsx +47 -144
  29. package/src/preact/mutationsOverTime/MutationOverTimeData.ts +9 -5
  30. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +5 -3
  31. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +4 -7
  32. package/src/preact/textInput/TextInputChangedEvent.ts +1 -1
  33. package/src/preact/textInput/fetchStringAutocompleteList.ts +20 -0
  34. package/src/preact/textInput/text-input.stories.tsx +14 -11
  35. package/src/preact/textInput/text-input.tsx +39 -140
  36. package/src/preact/wastewater/mutationsOverTime/__mockData__/details.json +88 -0
  37. package/src/preact/wastewater/mutationsOverTime/computeWastewaterMutationsOverTimeDataPerLocation.spec.ts +159 -0
  38. package/src/preact/wastewater/mutationsOverTime/computeWastewaterMutationsOverTimeDataPerLocation.ts +51 -0
  39. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +71 -0
  40. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +151 -0
  41. package/src/query/queryMutationsOverTime.ts +6 -14
  42. package/src/query/queryWastewaterMutationsOverTime.spec.ts +94 -0
  43. package/src/query/queryWastewaterMutationsOverTime.ts +55 -0
  44. package/src/types.ts +3 -0
  45. package/src/utilEntrypoint.ts +2 -0
  46. package/src/utils/map2d.ts +39 -0
  47. package/src/web-components/index.ts +1 -0
  48. package/src/web-components/input/gs-lineage-filter.stories.ts +120 -31
  49. package/src/web-components/input/gs-lineage-filter.tsx +24 -8
  50. package/src/web-components/input/gs-location-filter.stories.ts +9 -0
  51. package/src/web-components/input/gs-location-filter.tsx +21 -3
  52. package/src/web-components/input/gs-text-input.stories.ts +14 -5
  53. package/src/web-components/input/gs-text-input.tsx +23 -7
  54. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +82 -0
  55. package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +112 -0
  56. package/src/web-components/wastewaterVisualization/index.ts +1 -0
  57. package/standalone-bundle/assets/{mutationOverTimeWorker-DEybsZ5r.js.map → mutationOverTimeWorker-Cls1J0cl.js.map} +1 -1
  58. package/standalone-bundle/dashboard-components.js +6972 -6796
  59. package/standalone-bundle/dashboard-components.js.map +1 -1
  60. package/standalone-bundle/style.css +1 -1
  61. package/dist/LocationChangedEvent-CORvQvXv.js.map +0 -1
  62. package/dist/assets/mutationOverTimeWorker-DTv93Ere.js.map +0 -1
  63. package/src/preact/textInput/fetchAutocompleteList.ts +0 -9
@@ -1,4 +1,6 @@
1
- import { type Meta, type StoryObj } from '@storybook/preact';
1
+ import { type Meta, type PreactRenderer, type StoryObj } from '@storybook/preact';
2
+ import { expect, fn, userEvent, waitFor, within } from '@storybook/test';
3
+ import type { StepFunction } from '@storybook/types';
2
4
 
3
5
  import { LineageFilter, type LineageFilterProps } from './lineage-filter';
4
6
  import { previewHandles } from '../../../.storybook/preview';
@@ -22,6 +24,7 @@ const meta: Meta = {
22
24
  url: AGGREGATED_ENDPOINT,
23
25
  body: {
24
26
  fields: ['pangoLineage'],
27
+ country: 'Germany',
25
28
  },
26
29
  },
27
30
  response: {
@@ -32,10 +35,41 @@ const meta: Meta = {
32
35
  ],
33
36
  },
34
37
  },
38
+ argTypes: {
39
+ lapisField: {
40
+ control: {
41
+ type: 'text',
42
+ },
43
+ },
44
+ placeholderText: {
45
+ control: {
46
+ type: 'text',
47
+ },
48
+ },
49
+ value: {
50
+ control: {
51
+ type: 'text',
52
+ },
53
+ },
54
+ width: {
55
+ control: {
56
+ type: 'text',
57
+ },
58
+ },
59
+ lapisFilter: {
60
+ control: {
61
+ type: 'object',
62
+ },
63
+ },
64
+ },
65
+
35
66
  args: {
36
67
  lapisField: 'pangoLineage',
37
- placeholderText: 'Enter lineage',
38
- initialValue: '',
68
+ lapisFilter: {
69
+ country: 'Germany',
70
+ },
71
+ placeholderText: 'Enter a lineage',
72
+ value: 'A.1',
39
73
  width: '100%',
40
74
  },
41
75
  };
@@ -45,14 +79,62 @@ export default meta;
45
79
  export const Default: StoryObj<LineageFilterProps> = {
46
80
  render: (args) => (
47
81
  <LapisUrlContext.Provider value={LAPIS_URL}>
48
- <LineageFilter
49
- lapisField={args.lapisField}
50
- placeholderText={args.placeholderText}
51
- initialValue={args.initialValue}
52
- width={args.width}
53
- />
82
+ <LineageFilter {...args} />
54
83
  </LapisUrlContext.Provider>
55
84
  ),
85
+ play: async ({ canvasElement, step }) => {
86
+ const { canvas, lineageChangedListenerMock } = await prepare(canvasElement, step);
87
+
88
+ step('change lineage filter value fires event', async () => {
89
+ const input = await inputField(canvas);
90
+ await userEvent.clear(input);
91
+ await userEvent.type(input, 'B.1');
92
+ await userEvent.click(canvas.getByRole('option', { name: 'B.1' }));
93
+
94
+ await waitFor(() => {
95
+ return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
96
+ pangoLineage: 'B.1',
97
+ });
98
+ });
99
+ });
100
+ },
101
+ };
102
+
103
+ export const ClearSelection: StoryObj<LineageFilterProps> = {
104
+ ...Default,
105
+ play: async ({ canvasElement, step }) => {
106
+ const { canvas, lineageChangedListenerMock } = await prepare(canvasElement, step);
107
+
108
+ step('clear selection fires event with empty filter', async () => {
109
+ const clearSelectionButton = await canvas.findByLabelText('clear selection');
110
+ await userEvent.click(clearSelectionButton);
111
+
112
+ await waitFor(() => {
113
+ return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
114
+ pangoLineage: undefined,
115
+ });
116
+ });
117
+ });
118
+ },
119
+ };
120
+
121
+ export const OnBlurInput: StoryObj<LineageFilterProps> = {
122
+ ...Default,
123
+ play: async ({ canvasElement, step }) => {
124
+ const { canvas, lineageChangedListenerMock } = await prepare(canvasElement, step);
125
+
126
+ step('after cleared selection by hand and then blur fires event with empty filter', async () => {
127
+ const input = await inputField(canvas);
128
+ await userEvent.clear(input);
129
+ await userEvent.click(canvas.getByLabelText('toggle menu'));
130
+
131
+ await waitFor(() => {
132
+ return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
133
+ pangoLineage: undefined,
134
+ });
135
+ });
136
+ });
137
+ },
56
138
  };
57
139
 
58
140
  export const WithNoLapisField: StoryObj<LineageFilterProps> = {
@@ -67,3 +149,22 @@ export const WithNoLapisField: StoryObj<LineageFilterProps> = {
67
149
  });
68
150
  },
69
151
  };
152
+
153
+ async function prepare(canvasElement: HTMLElement, step: StepFunction<PreactRenderer, unknown>) {
154
+ const canvas = within(canvasElement);
155
+
156
+ const lineageChangedListenerMock = fn();
157
+ step('Setup event listener mock', () => {
158
+ canvasElement.addEventListener('gs-lineage-filter-changed', lineageChangedListenerMock);
159
+ });
160
+
161
+ step('location filter is rendered with value', async () => {
162
+ await waitFor(async () => {
163
+ return expect(await inputField(canvas)).toHaveValue('A.1');
164
+ });
165
+ });
166
+
167
+ return { canvas, lineageChangedListenerMock };
168
+ }
169
+
170
+ const inputField = (canvas: ReturnType<typeof within>) => canvas.findByPlaceholderText('Enter a lineage');
@@ -1,27 +1,32 @@
1
1
  import { type FunctionComponent } from 'preact';
2
- import { useContext, useRef } from 'preact/hooks';
2
+ import { useContext } from 'preact/hooks';
3
3
  import z from 'zod';
4
4
 
5
5
  import { fetchLineageAutocompleteList } from './fetchLineageAutocompleteList';
6
6
  import { LapisUrlContext } from '../LapisUrlContext';
7
+ import { LineageFilterChangedEvent } from './LineageFilterChangedEvent';
8
+ import { lapisFilterSchema } from '../../types';
9
+ import { DownshiftCombobox } from '../components/downshift-combobox';
7
10
  import { ErrorBoundary } from '../components/error-boundary';
8
11
  import { LoadingDisplay } from '../components/loading-display';
9
- import { NoDataDisplay } from '../components/no-data-display';
10
12
  import { ResizeContainer } from '../components/resize-container';
11
13
  import { useQuery } from '../useQuery';
12
14
 
13
- const lineageFilterInnerPropsSchema = z.object({
15
+ const lineageSelectorPropsSchema = z.object({
14
16
  lapisField: z.string().min(1),
15
17
  placeholderText: z.string().optional(),
16
- initialValue: z.string(),
18
+ value: z.string(),
19
+ });
20
+ const lineageFilterInnerPropsSchema = lineageSelectorPropsSchema.extend({
21
+ lapisFilter: lapisFilterSchema,
17
22
  });
18
-
19
23
  const lineageFilterPropsSchema = lineageFilterInnerPropsSchema.extend({
20
24
  width: z.string(),
21
25
  });
22
26
 
23
27
  export type LineageFilterInnerProps = z.infer<typeof lineageFilterInnerPropsSchema>;
24
28
  export type LineageFilterProps = z.infer<typeof lineageFilterPropsSchema>;
29
+ type LineageSelectorProps = z.infer<typeof lineageSelectorPropsSchema>;
25
30
 
26
31
  export const LineageFilter: FunctionComponent<LineageFilterProps> = (props) => {
27
32
  const { width, ...innerProps } = props;
@@ -39,15 +44,14 @@ export const LineageFilter: FunctionComponent<LineageFilterProps> = (props) => {
39
44
  const LineageFilterInner: FunctionComponent<LineageFilterInnerProps> = ({
40
45
  lapisField,
41
46
  placeholderText,
42
- initialValue,
47
+ value,
48
+ lapisFilter,
43
49
  }) => {
44
50
  const lapis = useContext(LapisUrlContext);
45
51
 
46
- const inputRef = useRef<HTMLInputElement>(null);
47
-
48
52
  const { data, error, isLoading } = useQuery(
49
- () => fetchLineageAutocompleteList(lapis, lapisField),
50
- [lapisField, lapis],
53
+ () => fetchLineageAutocompleteList({ lapis, field: lapisField, lapisFilter }),
54
+ [lapisField, lapis, lapisFilter],
51
55
  );
52
56
 
53
57
  if (isLoading) {
@@ -58,47 +62,33 @@ const LineageFilterInner: FunctionComponent<LineageFilterInnerProps> = ({
58
62
  throw error;
59
63
  }
60
64
 
61
- if (data === null) {
62
- return <NoDataDisplay />;
63
- }
64
-
65
- const onInput = () => {
66
- const value = inputRef.current?.value === '' ? undefined : inputRef.current?.value;
67
-
68
- if (isValidValue(value)) {
69
- inputRef.current?.dispatchEvent(
70
- new CustomEvent('gs-lineage-filter-changed', {
71
- detail: { [lapisField]: value },
72
- bubbles: true,
73
- composed: true,
74
- }),
75
- );
76
- }
77
- };
78
-
79
- const isValidValue = (value: string | undefined) => {
80
- if (value === undefined) {
81
- return true;
82
- }
83
- return data.includes(value);
84
- };
65
+ return <LineageSelector lapisField={lapisField} value={value} placeholderText={placeholderText} data={data} />;
66
+ };
85
67
 
68
+ const LineageSelector = ({
69
+ lapisField,
70
+ value,
71
+ placeholderText,
72
+ data,
73
+ }: LineageSelectorProps & {
74
+ data: string[];
75
+ }) => {
86
76
  return (
87
- <>
88
- <input
89
- type='text'
90
- class='input input-bordered w-full'
91
- placeholder={placeholderText !== undefined ? placeholderText : lapisField}
92
- onInput={onInput}
93
- ref={inputRef}
94
- list={lapisField}
95
- value={initialValue}
96
- />
97
- <datalist id={lapisField}>
98
- {data.map((item) => (
99
- <option value={item} key={item} />
100
- ))}
101
- </datalist>
102
- </>
77
+ <DownshiftCombobox
78
+ allItems={data}
79
+ value={value}
80
+ filterItemsByInputValue={filterByInputValue}
81
+ createEvent={(item: string | null) => new LineageFilterChangedEvent({ [lapisField]: item ?? undefined })}
82
+ itemToString={(item: string | undefined | null) => item ?? ''}
83
+ placeholderText={placeholderText}
84
+ formatItemInList={(item: string) => item}
85
+ />
103
86
  );
104
87
  };
88
+
89
+ function filterByInputValue(item: string, inputValue: string | undefined | null) {
90
+ if (inputValue === undefined || inputValue === null || inputValue === '') {
91
+ return true;
92
+ }
93
+ return item?.toLowerCase().includes(inputValue?.toLowerCase() || '');
94
+ }
@@ -1,4 +1,4 @@
1
- export type LapisLocationFilter = Record<string, string | null | undefined>;
1
+ import { type LapisLocationFilter } from '../../types';
2
2
 
3
3
  export class LocationChangedEvent extends CustomEvent<LapisLocationFilter> {
4
4
  constructor(detail: LapisLocationFilter) {
@@ -8,7 +8,7 @@ describe('fetchAutocompletionList', () => {
8
8
  const fields = ['region', 'country', 'division'];
9
9
 
10
10
  lapisRequestMocks.aggregated(
11
- { fields },
11
+ { fields, country: 'Germany' },
12
12
  {
13
13
  data: [
14
14
  { count: 0, region: 'region1', country: 'country1_1', division: 'division1_1_1' },
@@ -20,7 +20,11 @@ describe('fetchAutocompletionList', () => {
20
20
  },
21
21
  );
22
22
 
23
- const result = await fetchAutocompletionList(fields, DUMMY_LAPIS_URL);
23
+ const result = await fetchAutocompletionList({
24
+ fields,
25
+ lapis: DUMMY_LAPIS_URL,
26
+ lapisFilter: { country: 'Germany' },
27
+ });
24
28
 
25
29
  expect(result).to.deep.equal([
26
30
  { region: 'region1', country: undefined, division: undefined },
@@ -1,16 +1,26 @@
1
1
  import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
2
+ import type { LapisFilter } from '../../types';
2
3
 
3
- export async function fetchAutocompletionList(
4
- fields: string[],
5
- lapis: string,
6
- signal?: AbortSignal,
7
- ): Promise<Record<string, string | undefined>[]> {
4
+ export async function fetchAutocompletionList({
5
+ fields,
6
+ lapis,
7
+ signal,
8
+ lapisFilter,
9
+ }: {
10
+ fields: string[];
11
+ lapis: string;
12
+ lapisFilter?: LapisFilter;
13
+ signal?: AbortSignal;
14
+ }): Promise<Record<string, string | undefined>[]> {
8
15
  const toAncestorInHierarchyOverwriteValues = Array(fields.length - 1)
9
16
  .fill(0)
10
17
  .map((_, i) => i + 1)
11
18
  .map((i) => fields.slice(i).reduce((acc, field) => ({ ...acc, [field]: null }), {}));
12
19
 
13
- const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string | null>>({}, fields);
20
+ const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string | null>>(
21
+ lapisFilter ?? {},
22
+ fields,
23
+ );
14
24
 
15
25
  const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
16
26
 
@@ -21,6 +21,7 @@ const meta: Meta<LocationFilterProps> = {
21
21
  url: AGGREGATED_ENDPOINT,
22
22
  body: {
23
23
  fields: ['region', 'country', 'division', 'location'],
24
+ age: 18,
24
25
  },
25
26
  },
26
27
  response: {
@@ -39,6 +40,9 @@ const meta: Meta<LocationFilterProps> = {
39
40
  fields: ['region', 'country', 'division', 'location'],
40
41
  value: { region: 'Europe', country: undefined, division: undefined, location: undefined },
41
42
  placeholderText: 'Enter a location',
43
+ lapisFilter: {
44
+ age: 18,
45
+ },
42
46
  },
43
47
  argTypes: {
44
48
  fields: {
@@ -61,6 +65,11 @@ const meta: Meta<LocationFilterProps> = {
61
65
  type: 'text',
62
66
  },
63
67
  },
68
+ lapisFilter: {
69
+ control: {
70
+ type: 'object',
71
+ },
72
+ },
64
73
  },
65
74
  };
66
75
 
@@ -81,16 +90,14 @@ export const Primary: StoryObj<LocationFilterProps> = {
81
90
  await userEvent.type(input, 'Germany');
82
91
  await userEvent.click(canvas.getByRole('option', { name: 'Germany Europe / Germany' }));
83
92
 
84
- await expect(locationChangedListenerMock).toHaveBeenCalledWith(
85
- expect.objectContaining({
86
- detail: {
87
- country: 'Germany',
88
- region: 'Europe',
89
- division: undefined,
90
- location: undefined,
91
- },
92
- }),
93
- );
93
+ await waitFor(() => {
94
+ return expect(locationChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
95
+ country: 'Germany',
96
+ region: 'Europe',
97
+ division: undefined,
98
+ location: undefined,
99
+ });
100
+ });
94
101
  });
95
102
  },
96
103
  };
@@ -104,16 +111,14 @@ export const ClearSelection: StoryObj<LocationFilterProps> = {
104
111
  const clearSelectionButton = await canvas.findByLabelText('clear selection');
105
112
  await userEvent.click(clearSelectionButton);
106
113
 
107
- await expect(locationChangedListenerMock).toHaveBeenCalledWith(
108
- expect.objectContaining({
109
- detail: {
110
- country: undefined,
111
- region: undefined,
112
- division: undefined,
113
- location: undefined,
114
- },
115
- }),
116
- );
114
+ await waitFor(() => {
115
+ return expect(locationChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
116
+ country: undefined,
117
+ region: undefined,
118
+ division: undefined,
119
+ location: undefined,
120
+ });
121
+ });
117
122
  });
118
123
  },
119
124
  };
@@ -128,16 +133,14 @@ export const OnBlurInput: StoryObj<LocationFilterProps> = {
128
133
  await userEvent.clear(input);
129
134
  await userEvent.click(canvas.getByLabelText('toggle menu'));
130
135
 
131
- await expect(locationChangedListenerMock).toHaveBeenCalledWith(
132
- expect.objectContaining({
133
- detail: {
134
- country: undefined,
135
- region: undefined,
136
- division: undefined,
137
- location: undefined,
138
- },
139
- }),
140
- );
136
+ await waitFor(() => {
137
+ return expect(locationChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
138
+ country: undefined,
139
+ region: undefined,
140
+ division: undefined,
141
+ location: undefined,
142
+ });
143
+ });
141
144
  });
142
145
  },
143
146
  };