@genspectrum/dashboard-components 0.19.7 → 0.19.9
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 +33 -15
- package/dist/assets/mutationOverTimeWorker-BzmkceEA.js.map +1 -0
- package/dist/components.d.ts +24 -24
- package/dist/components.js +121 -21
- package/dist/components.js.map +1 -1
- package/dist/util.d.ts +24 -24
- package/package.json +1 -1
- package/src/lapisApi/LineageDefinition.ts +9 -0
- package/src/lapisApi/lapisApi.ts +27 -0
- package/src/preact/lineageFilter/__mockData__/lineageDefinition.json +38118 -0
- package/src/preact/lineageFilter/fetchLineageAutocompleteList.spec.ts +264 -6
- package/src/preact/lineageFilter/fetchLineageAutocompleteList.ts +104 -7
- package/src/preact/lineageFilter/lineage-filter.stories.tsx +13 -1
- package/src/preact/lineageFilter/lineage-filter.tsx +21 -11
- package/src/web-components/input/gs-lineage-filter.stories.ts +13 -1
- package/src/web-components/input/introduction.mdx +57 -2
- package/src/web-components/tutorials/CreateYourFirstOwnDashboard.mdx +85 -0
- package/src/web-components/tutorials/UseTheComponentsWithPlainJavaScript.mdx +140 -0
- package/src/web-components/tutorials/UseTheComponentsWithReact.mdx +166 -0
- package/src/web-components/visualization/gs-mutations.tsx +2 -2
- package/src/web-components/visualization/introduction.mdx +51 -0
- package/standalone-bundle/assets/mutationOverTimeWorker-jUeItsGM.js.map +1 -0
- package/standalone-bundle/dashboard-components.js +7007 -6953
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/dist/assets/mutationOverTimeWorker-DQGh08AS.js.map +0 -1
- package/src/web-components/visualization/data_visualization_statistical_analysis.mdx +0 -26
- package/standalone-bundle/assets/mutationOverTimeWorker-DAf2_NiP.js.map +0 -1
- /package/src/web-components/{MutationAnnotations.mdx → mutationAnnotations.mdx} +0 -0
- /package/src/web-components/{ResizeContainer.mdx → sizeOfComponents.mdx} +0 -0
|
@@ -4,25 +4,283 @@ import { fetchLineageAutocompleteList } from './fetchLineageAutocompleteList';
|
|
|
4
4
|
import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../../vitest.setup';
|
|
5
5
|
|
|
6
6
|
describe('fetchLineageAutocompleteList', () => {
|
|
7
|
+
const lapisFilter = { country: 'Germany' };
|
|
8
|
+
const lineageField = 'lineageField';
|
|
9
|
+
|
|
10
|
+
test('should return single lineage', async () => {
|
|
11
|
+
lapisRequestMocks.aggregated(
|
|
12
|
+
{ fields: [lineageField], ...lapisFilter },
|
|
13
|
+
{
|
|
14
|
+
data: [
|
|
15
|
+
{
|
|
16
|
+
[lineageField]: 'A',
|
|
17
|
+
count: 1,
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
lapisRequestMocks.lineageDefinition(
|
|
24
|
+
{
|
|
25
|
+
A: {
|
|
26
|
+
aliases: ['a'],
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
lineageField,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const result = await fetchLineageAutocompleteList({
|
|
33
|
+
lapisUrl: DUMMY_LAPIS_URL,
|
|
34
|
+
lapisField: lineageField,
|
|
35
|
+
lapisFilter,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
expect(result).to.deep.equal([
|
|
39
|
+
{
|
|
40
|
+
lineage: 'A',
|
|
41
|
+
count: 1,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
lineage: 'A*',
|
|
45
|
+
count: 1,
|
|
46
|
+
},
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
7
50
|
test('should add sublineage values', async () => {
|
|
8
51
|
lapisRequestMocks.aggregated(
|
|
9
|
-
{ fields: [
|
|
52
|
+
{ fields: [lineageField], ...lapisFilter },
|
|
10
53
|
{
|
|
11
54
|
data: [
|
|
12
55
|
{
|
|
13
|
-
lineageField: '
|
|
56
|
+
[lineageField]: 'A',
|
|
14
57
|
count: 1,
|
|
15
58
|
},
|
|
59
|
+
{
|
|
60
|
+
[lineageField]: 'A.1',
|
|
61
|
+
count: 2,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
[lineageField]: 'A.2',
|
|
65
|
+
count: 3,
|
|
66
|
+
},
|
|
16
67
|
],
|
|
17
68
|
},
|
|
18
69
|
);
|
|
19
70
|
|
|
71
|
+
lapisRequestMocks.lineageDefinition(
|
|
72
|
+
{
|
|
73
|
+
A: {
|
|
74
|
+
aliases: ['a'],
|
|
75
|
+
},
|
|
76
|
+
'A.1': {
|
|
77
|
+
parents: ['A'],
|
|
78
|
+
aliases: ['a.1'],
|
|
79
|
+
},
|
|
80
|
+
'A.2': {
|
|
81
|
+
parents: ['A'],
|
|
82
|
+
aliases: ['a.1'],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
lineageField,
|
|
86
|
+
);
|
|
87
|
+
|
|
20
88
|
const result = await fetchLineageAutocompleteList({
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
lapisFilter
|
|
89
|
+
lapisUrl: DUMMY_LAPIS_URL,
|
|
90
|
+
lapisField: lineageField,
|
|
91
|
+
lapisFilter,
|
|
24
92
|
});
|
|
25
93
|
|
|
26
|
-
expect(result).to.deep.equal([
|
|
94
|
+
expect(result).to.deep.equal([
|
|
95
|
+
{
|
|
96
|
+
lineage: 'A',
|
|
97
|
+
count: 1,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
lineage: 'A*',
|
|
101
|
+
count: 6,
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
{
|
|
105
|
+
lineage: 'A.1',
|
|
106
|
+
count: 2,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
lineage: 'A.1*',
|
|
110
|
+
count: 2,
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
lineage: 'A.2',
|
|
115
|
+
count: 3,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
lineage: 'A.2*',
|
|
119
|
+
count: 3,
|
|
120
|
+
},
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('should work with recombinations', async () => {
|
|
125
|
+
lapisRequestMocks.aggregated(
|
|
126
|
+
{ fields: [lineageField], ...lapisFilter },
|
|
127
|
+
{
|
|
128
|
+
data: [
|
|
129
|
+
{
|
|
130
|
+
[lineageField]: 'A',
|
|
131
|
+
count: 1,
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
[lineageField]: 'A.1',
|
|
135
|
+
count: 2,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
[lineageField]: 'A.2',
|
|
139
|
+
count: 3,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
[lineageField]: 'XA',
|
|
143
|
+
count: 4,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
lapisRequestMocks.lineageDefinition(
|
|
150
|
+
{
|
|
151
|
+
A: {
|
|
152
|
+
aliases: ['a'],
|
|
153
|
+
},
|
|
154
|
+
'A.1': {
|
|
155
|
+
parents: ['A'],
|
|
156
|
+
aliases: ['a.1'],
|
|
157
|
+
},
|
|
158
|
+
'A.2': {
|
|
159
|
+
parents: ['A'],
|
|
160
|
+
aliases: ['a.1'],
|
|
161
|
+
},
|
|
162
|
+
XA: {
|
|
163
|
+
aliases: ['xa'],
|
|
164
|
+
parents: ['A.1', 'A.2'],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
lineageField,
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const result = await fetchLineageAutocompleteList({
|
|
171
|
+
lapisUrl: DUMMY_LAPIS_URL,
|
|
172
|
+
lapisField: lineageField,
|
|
173
|
+
lapisFilter,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
expect(result).to.deep.equal([
|
|
177
|
+
{
|
|
178
|
+
lineage: 'A',
|
|
179
|
+
count: 1,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
lineage: 'A*',
|
|
183
|
+
count: 10,
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
{
|
|
187
|
+
lineage: 'A.1',
|
|
188
|
+
count: 2,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
lineage: 'A.1*',
|
|
192
|
+
count: 6,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
lineage: 'A.2',
|
|
196
|
+
count: 3,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
lineage: 'A.2*',
|
|
200
|
+
count: 7,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
lineage: 'XA',
|
|
204
|
+
count: 4,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
lineage: 'XA*',
|
|
208
|
+
count: 4,
|
|
209
|
+
},
|
|
210
|
+
]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('should work with grandchildren', async () => {
|
|
214
|
+
lapisRequestMocks.aggregated(
|
|
215
|
+
{ fields: [lineageField], ...lapisFilter },
|
|
216
|
+
{
|
|
217
|
+
data: [
|
|
218
|
+
{
|
|
219
|
+
[lineageField]: 'A',
|
|
220
|
+
count: 1,
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
[lineageField]: 'A.1',
|
|
224
|
+
count: 2,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
[lineageField]: 'A.1.1',
|
|
228
|
+
count: 3,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
},
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
lapisRequestMocks.lineageDefinition(
|
|
235
|
+
{
|
|
236
|
+
'A.1': {
|
|
237
|
+
parents: ['A'],
|
|
238
|
+
aliases: ['a.1'],
|
|
239
|
+
},
|
|
240
|
+
A: {
|
|
241
|
+
aliases: ['a'],
|
|
242
|
+
},
|
|
243
|
+
'A.1.1': {
|
|
244
|
+
parents: ['A.1'],
|
|
245
|
+
aliases: ['a.1.1'],
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
lineageField,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const result = await fetchLineageAutocompleteList({
|
|
252
|
+
lapisUrl: DUMMY_LAPIS_URL,
|
|
253
|
+
lapisField: lineageField,
|
|
254
|
+
lapisFilter,
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(result).to.deep.equal([
|
|
258
|
+
{
|
|
259
|
+
lineage: 'A',
|
|
260
|
+
count: 1,
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
lineage: 'A*',
|
|
264
|
+
count: 6,
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
{
|
|
268
|
+
lineage: 'A.1',
|
|
269
|
+
count: 2,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
lineage: 'A.1*',
|
|
273
|
+
count: 5,
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
{
|
|
277
|
+
lineage: 'A.1.1',
|
|
278
|
+
count: 3,
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
lineage: 'A.1.1*',
|
|
282
|
+
count: 3,
|
|
283
|
+
},
|
|
284
|
+
]);
|
|
27
285
|
});
|
|
28
286
|
});
|
|
@@ -1,20 +1,117 @@
|
|
|
1
|
+
import { fetchLineageDefinition } from '../../lapisApi/lapisApi';
|
|
1
2
|
import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
|
|
2
3
|
import type { LapisFilter } from '../../types';
|
|
3
4
|
|
|
4
5
|
export async function fetchLineageAutocompleteList({
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
lapisUrl,
|
|
7
|
+
lapisField,
|
|
8
|
+
lapisFilter,
|
|
7
9
|
signal,
|
|
10
|
+
}: {
|
|
11
|
+
lapisUrl: string;
|
|
12
|
+
lapisField: string;
|
|
13
|
+
lapisFilter?: LapisFilter;
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}): Promise<LineageItem[]> {
|
|
16
|
+
const [countsByLineage, lineageTree] = await Promise.all([
|
|
17
|
+
getCountsByLineage({
|
|
18
|
+
lapisUrl,
|
|
19
|
+
lapisField,
|
|
20
|
+
lapisFilter,
|
|
21
|
+
signal,
|
|
22
|
+
}),
|
|
23
|
+
getLineageTree({ lapisUrl, lapisField, signal }),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
return Array.from(lineageTree.keys())
|
|
27
|
+
.sort((a, b) => a.localeCompare(b))
|
|
28
|
+
.map((lineage) => {
|
|
29
|
+
return [
|
|
30
|
+
{
|
|
31
|
+
lineage,
|
|
32
|
+
count: countsByLineage.get(lineage) ?? 0,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
lineage: `${lineage}*`,
|
|
36
|
+
count: getCountsIncludingSublineages(lineage, lineageTree, countsByLineage),
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
})
|
|
40
|
+
.flat();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type LineageItem = { lineage: string; count: number };
|
|
44
|
+
|
|
45
|
+
async function getCountsByLineage({
|
|
46
|
+
lapisUrl,
|
|
47
|
+
lapisField,
|
|
8
48
|
lapisFilter,
|
|
49
|
+
signal,
|
|
9
50
|
}: {
|
|
10
|
-
|
|
11
|
-
|
|
51
|
+
lapisUrl: string;
|
|
52
|
+
lapisField: string;
|
|
12
53
|
lapisFilter?: LapisFilter;
|
|
13
54
|
signal?: AbortSignal;
|
|
14
55
|
}) {
|
|
15
|
-
const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string>>(lapisFilter ?? {}, [
|
|
56
|
+
const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string>>(lapisFilter ?? {}, [
|
|
57
|
+
lapisField,
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const countsByLineageArray = (await fetchAggregatedOperator.evaluate(lapisUrl, signal)).content;
|
|
61
|
+
return new Map<string, number>(countsByLineageArray.map((value) => [value[lapisField], value.count]));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function getLineageTree({
|
|
65
|
+
lapisUrl,
|
|
66
|
+
lapisField,
|
|
67
|
+
signal,
|
|
68
|
+
}: {
|
|
69
|
+
lapisUrl: string;
|
|
70
|
+
lapisField: string;
|
|
71
|
+
signal?: AbortSignal;
|
|
72
|
+
}) {
|
|
73
|
+
const lineageDefinitions = await fetchLineageDefinition({ lapisUrl, lapisField, signal });
|
|
74
|
+
|
|
75
|
+
const lineageTree = new Map<string, { children: string[] }>();
|
|
76
|
+
|
|
77
|
+
Object.entries(lineageDefinitions).forEach(([lineage, definition]) => {
|
|
78
|
+
if (!lineageTree.has(lineage)) {
|
|
79
|
+
lineageTree.set(lineage, { children: [] });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
definition.parents?.forEach((parent) => {
|
|
83
|
+
const parentChildren = lineageTree.get(parent)?.children;
|
|
84
|
+
|
|
85
|
+
const newParentChildren = parentChildren ? [...parentChildren, lineage] : [lineage];
|
|
86
|
+
|
|
87
|
+
lineageTree.set(parent, { children: newParentChildren });
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return lineageTree;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function getCountsIncludingSublineages(
|
|
95
|
+
lineage: string,
|
|
96
|
+
lineageTree: Map<string, { children: string[] }>,
|
|
97
|
+
countsByLineage: Map<string, number>,
|
|
98
|
+
): number {
|
|
99
|
+
const descendants = getAllDescendants(lineage, lineageTree);
|
|
100
|
+
|
|
101
|
+
const countOfChildren = [...descendants].reduce((sum, child) => {
|
|
102
|
+
return sum + (countsByLineage.get(child) ?? 0);
|
|
103
|
+
}, 0);
|
|
104
|
+
const countLineage = countsByLineage.get(lineage) ?? 0;
|
|
105
|
+
|
|
106
|
+
return countOfChildren + countLineage;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getAllDescendants(lineage: string, lineageTree: Map<string, { children: string[] }>): Set<string> {
|
|
110
|
+
const children = lineageTree.get(lineage)?.children ?? [];
|
|
16
111
|
|
|
17
|
-
const
|
|
112
|
+
const childrenOfChildren = children.flatMap((child) => {
|
|
113
|
+
return getAllDescendants(child, lineageTree);
|
|
114
|
+
});
|
|
18
115
|
|
|
19
|
-
return
|
|
116
|
+
return new Set([...children, ...childrenOfChildren.flatMap((child) => Array.from(child))]);
|
|
20
117
|
}
|
|
@@ -5,6 +5,8 @@ import type { StepFunction } from '@storybook/types';
|
|
|
5
5
|
import { LineageFilter, type LineageFilterProps } from './lineage-filter';
|
|
6
6
|
import { previewHandles } from '../../../.storybook/preview';
|
|
7
7
|
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
|
|
8
|
+
import lineageDefinition from './__mockData__/lineageDefinition.json';
|
|
9
|
+
import { lineageDefinitionEndpoint } from '../../lapisApi/lapisApi';
|
|
8
10
|
import aggregatedData from '../../preact/lineageFilter/__mockData__/aggregated.json';
|
|
9
11
|
import { gsEventNames } from '../../utils/gsEventNames';
|
|
10
12
|
import { LapisUrlContextProvider } from '../LapisUrlContext';
|
|
@@ -33,6 +35,16 @@ const meta: Meta = {
|
|
|
33
35
|
body: aggregatedData,
|
|
34
36
|
},
|
|
35
37
|
},
|
|
38
|
+
{
|
|
39
|
+
matcher: {
|
|
40
|
+
name: 'lineageDefinition',
|
|
41
|
+
url: lineageDefinitionEndpoint(LAPIS_URL, 'pangoLineage'),
|
|
42
|
+
},
|
|
43
|
+
response: {
|
|
44
|
+
status: 200,
|
|
45
|
+
body: lineageDefinition,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
36
48
|
],
|
|
37
49
|
},
|
|
38
50
|
},
|
|
@@ -90,7 +102,7 @@ export const Default: StoryObj<LineageFilterProps> = {
|
|
|
90
102
|
const input = await inputField(canvas);
|
|
91
103
|
await userEvent.clear(input);
|
|
92
104
|
await userEvent.type(input, 'B.1');
|
|
93
|
-
await userEvent.click(canvas.getByRole('option', { name: 'B.1' }));
|
|
105
|
+
await userEvent.click(canvas.getByRole('option', { name: 'B.1(53802)' }));
|
|
94
106
|
|
|
95
107
|
await waitFor(() => {
|
|
96
108
|
return expect(lineageChangedListenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { useMemo } from 'preact/hooks';
|
|
2
3
|
import z from 'zod';
|
|
3
4
|
|
|
4
5
|
import { useLapisUrl } from '../LapisUrlContext';
|
|
5
6
|
import { LineageFilterChangedEvent } from './LineageFilterChangedEvent';
|
|
6
|
-
import { fetchLineageAutocompleteList } from './fetchLineageAutocompleteList';
|
|
7
|
+
import { fetchLineageAutocompleteList, type LineageItem } from './fetchLineageAutocompleteList';
|
|
7
8
|
import { lapisFilterSchema } from '../../types';
|
|
8
9
|
import { DownshiftCombobox } from '../components/downshift-combobox';
|
|
9
10
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
@@ -46,11 +47,11 @@ const LineageFilterInner: FunctionComponent<LineageFilterInnerProps> = ({
|
|
|
46
47
|
value,
|
|
47
48
|
lapisFilter,
|
|
48
49
|
}) => {
|
|
49
|
-
const
|
|
50
|
+
const lapisUrl = useLapisUrl();
|
|
50
51
|
|
|
51
52
|
const { data, error, isLoading } = useQuery(
|
|
52
|
-
() => fetchLineageAutocompleteList({
|
|
53
|
-
[lapisField,
|
|
53
|
+
() => fetchLineageAutocompleteList({ lapisUrl, lapisField, lapisFilter }),
|
|
54
|
+
[lapisField, lapisUrl, lapisFilter],
|
|
54
55
|
);
|
|
55
56
|
|
|
56
57
|
if (isLoading) {
|
|
@@ -70,24 +71,33 @@ const LineageSelector = ({
|
|
|
70
71
|
placeholderText,
|
|
71
72
|
data,
|
|
72
73
|
}: LineageSelectorProps & {
|
|
73
|
-
data:
|
|
74
|
+
data: LineageItem[];
|
|
74
75
|
}) => {
|
|
76
|
+
const selectedItem = useMemo(() => {
|
|
77
|
+
return data.find((item) => item.lineage === value) ?? null;
|
|
78
|
+
}, [data, value]);
|
|
79
|
+
|
|
75
80
|
return (
|
|
76
81
|
<DownshiftCombobox
|
|
77
82
|
allItems={data}
|
|
78
|
-
value={
|
|
83
|
+
value={selectedItem}
|
|
79
84
|
filterItemsByInputValue={filterByInputValue}
|
|
80
|
-
createEvent={(item) => new LineageFilterChangedEvent({ [lapisField]: item ?? undefined })}
|
|
81
|
-
itemToString={(item) => item ?? ''}
|
|
85
|
+
createEvent={(item) => new LineageFilterChangedEvent({ [lapisField]: item?.lineage ?? undefined })}
|
|
86
|
+
itemToString={(item) => item?.lineage ?? ''}
|
|
82
87
|
placeholderText={placeholderText}
|
|
83
|
-
formatItemInList={(item:
|
|
88
|
+
formatItemInList={(item: LineageItem) => (
|
|
89
|
+
<p>
|
|
90
|
+
<span>{item.lineage}</span>
|
|
91
|
+
<span className='ml-2 text-gray-500'>({item.count})</span>
|
|
92
|
+
</p>
|
|
93
|
+
)}
|
|
84
94
|
/>
|
|
85
95
|
);
|
|
86
96
|
};
|
|
87
97
|
|
|
88
|
-
function filterByInputValue(item:
|
|
98
|
+
function filterByInputValue(item: LineageItem, inputValue: string | null) {
|
|
89
99
|
if (inputValue === null || inputValue === '') {
|
|
90
100
|
return true;
|
|
91
101
|
}
|
|
92
|
-
return item?.toLowerCase().includes(inputValue?.toLowerCase() || '');
|
|
102
|
+
return item.lineage?.toLowerCase().includes(inputValue?.toLowerCase() || '');
|
|
93
103
|
}
|
|
@@ -7,7 +7,9 @@ import { previewHandles } from '../../../.storybook/preview';
|
|
|
7
7
|
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
|
|
8
8
|
import '../gs-app';
|
|
9
9
|
import './gs-lineage-filter';
|
|
10
|
+
import { lineageDefinitionEndpoint } from '../../lapisApi/lapisApi';
|
|
10
11
|
import aggregatedData from '../../preact/lineageFilter/__mockData__/aggregated.json';
|
|
12
|
+
import lineageDefinition from '../../preact/lineageFilter/__mockData__/lineageDefinition.json';
|
|
11
13
|
import { type LineageFilterProps } from '../../preact/lineageFilter/lineage-filter';
|
|
12
14
|
import { gsEventNames } from '../../utils/gsEventNames';
|
|
13
15
|
import { withinShadowRoot } from '../withinShadowRoot.story';
|
|
@@ -44,6 +46,16 @@ const meta: Meta<Required<LineageFilterProps>> = {
|
|
|
44
46
|
body: aggregatedData,
|
|
45
47
|
},
|
|
46
48
|
},
|
|
49
|
+
{
|
|
50
|
+
matcher: {
|
|
51
|
+
name: 'lineageDefinition',
|
|
52
|
+
url: lineageDefinitionEndpoint(LAPIS_URL, 'pangoLineage'),
|
|
53
|
+
},
|
|
54
|
+
response: {
|
|
55
|
+
status: 200,
|
|
56
|
+
body: lineageDefinition,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
47
59
|
],
|
|
48
60
|
},
|
|
49
61
|
componentDocs: {
|
|
@@ -210,7 +222,7 @@ export const FiresEvent: StoryObj<Required<LineageFilterProps>> = {
|
|
|
210
222
|
|
|
211
223
|
await step('Enter a valid lineage value', async () => {
|
|
212
224
|
await userEvent.type(inputField(), 'B.1.1.7*');
|
|
213
|
-
await userEvent.click(canvas.getByRole('option', { name: 'B.1.1.7*' }));
|
|
225
|
+
await userEvent.click(canvas.getByRole('option', { name: 'B.1.1.7*(677146)' }));
|
|
214
226
|
|
|
215
227
|
await waitFor(() => {
|
|
216
228
|
return expect(listenerMock.mock.calls.at(-1)![0].detail).toStrictEqual({
|
|
@@ -7,5 +7,60 @@ import { Meta } from '@storybook/blocks';
|
|
|
7
7
|
The components in this section let the user specify values for LAPIS filters.
|
|
8
8
|
The filters can then be used as input to the visualization components.
|
|
9
9
|
|
|
10
|
-
Every component fires `CustomEvent`s when the user interacts with it
|
|
11
|
-
|
|
10
|
+
Every component fires `CustomEvent`s when the user interacts with it.
|
|
11
|
+
`event.detail` contains the payload of the event.
|
|
12
|
+
|
|
13
|
+
Every component fires an event that can be used to update the LAPIS filters.
|
|
14
|
+
It is supposed to be used in the style of:
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
component.addEventListener('gs-example-event', (event) => {
|
|
18
|
+
setNewLapisFilter({
|
|
19
|
+
...previousLapisFilter,
|
|
20
|
+
...event.detail,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Controlled Input Components
|
|
26
|
+
|
|
27
|
+
HTML input components can be controlled or uncontrolled.
|
|
28
|
+
In a controlled component, the value is controlled by surrounding Javascript code.
|
|
29
|
+
In an uncontrolled component, the value is controlled by the DOM and the surrounding Javascript code only reads the value
|
|
30
|
+
(e.g. by listening to events).
|
|
31
|
+
|
|
32
|
+
All our input components can be used in both ways.
|
|
33
|
+
Every component fires one or two events.
|
|
34
|
+
If the event details can be used to update the LAPIS filter
|
|
35
|
+
_and_ the value then the component will only fire one event,
|
|
36
|
+
otherwise it will fire one event to update the LAPIS filter and one to update the value of the component.
|
|
37
|
+
Refer to the documentation of the individual components for details on which event you can use for which purpose.
|
|
38
|
+
|
|
39
|
+
**Example**: A controlled input component in a React app could conceptually look like this:
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
import { useEffect, useRef, useState } from 'react';
|
|
43
|
+
|
|
44
|
+
const ExampleInput = () => {
|
|
45
|
+
const [value, setValue] = useState('foo');
|
|
46
|
+
const inputRef = useRef(null);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!inputRef.current) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const eventListener = (event) => {
|
|
54
|
+
setValue(event.detail);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
inputRef.current.addEventListener('gs-input', eventListener);
|
|
58
|
+
|
|
59
|
+
return () => {
|
|
60
|
+
inputRef.current.removeEventListener('gs-input', eventListener);
|
|
61
|
+
};
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
return <gs-example-input ref={inputRef} value={value} />;
|
|
65
|
+
};
|
|
66
|
+
```
|