@genspectrum/dashboard-components 0.6.0 → 0.6.2
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 +189 -1
- package/dist/dashboard-components.js +252 -146
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +57 -0
- package/package.json +1 -1
- package/src/preact/lineageFilter/__mockData__/aggregated.json +14510 -0
- package/src/preact/lineageFilter/fetchLineageAutocompleteList.spec.ts +14 -0
- package/src/preact/lineageFilter/fetchLineageAutocompleteList.ts +9 -0
- package/src/preact/lineageFilter/lineage-filter.stories.tsx +56 -0
- package/src/preact/lineageFilter/lineage-filter.tsx +100 -0
- package/src/preact/numberSequencesOverTime/__mockData__/twoVariantsEG.json +0 -8
- package/src/preact/textInput/text-input.tsx +1 -5
- package/src/web-components/input/gs-lineage-filter.stories.ts +137 -0
- package/src/web-components/input/gs-lineage-filter.tsx +79 -0
- package/src/web-components/input/index.ts +1 -0
- package/src/web-components/visualization/gs-number-sequences-over-time.stories.ts +1 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { fetchLineageAutocompleteList } from './fetchLineageAutocompleteList';
|
|
4
|
+
import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../../vitest.setup';
|
|
5
|
+
|
|
6
|
+
describe('fetchLineageAutocompleteList', () => {
|
|
7
|
+
test('should add sublineage values', async () => {
|
|
8
|
+
lapisRequestMocks.aggregated({ fields: ['lineageField'] }, { data: [{ lineageField: 'B.1.1.7', count: 1 }] });
|
|
9
|
+
|
|
10
|
+
const result = await fetchLineageAutocompleteList(DUMMY_LAPIS_URL, 'lineageField');
|
|
11
|
+
|
|
12
|
+
expect(result).to.deep.equal(['B.1.1.7', 'B.1.1.7*']);
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
|
|
2
|
+
|
|
3
|
+
export async function fetchLineageAutocompleteList(lapis: string, field: string, signal?: AbortSignal) {
|
|
4
|
+
const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string>>({}, [field]);
|
|
5
|
+
|
|
6
|
+
const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
|
|
7
|
+
|
|
8
|
+
return data.flatMap((item) => [item[field], `${item[field]}*`]).sort();
|
|
9
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { withActions } from '@storybook/addon-actions/decorator';
|
|
2
|
+
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
3
|
+
|
|
4
|
+
import { LineageFilter, type LineageFilterProps } from './lineage-filter';
|
|
5
|
+
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
|
|
6
|
+
import aggregatedData from '../../preact/lineageFilter/__mockData__/aggregated.json';
|
|
7
|
+
import { LapisUrlContext } from '../LapisUrlContext';
|
|
8
|
+
|
|
9
|
+
const meta: Meta = {
|
|
10
|
+
title: 'Input/LineageFilter',
|
|
11
|
+
component: LineageFilter,
|
|
12
|
+
parameters: {
|
|
13
|
+
actions: {
|
|
14
|
+
handles: ['gs-lineage-filter-changed'],
|
|
15
|
+
},
|
|
16
|
+
fetchMock: {
|
|
17
|
+
mocks: [
|
|
18
|
+
{
|
|
19
|
+
matcher: {
|
|
20
|
+
name: 'pangoLineage',
|
|
21
|
+
url: AGGREGATED_ENDPOINT,
|
|
22
|
+
body: {
|
|
23
|
+
fields: ['pangoLineage'],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
response: {
|
|
27
|
+
status: 200,
|
|
28
|
+
body: aggregatedData,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
decorators: [withActions],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default meta;
|
|
38
|
+
|
|
39
|
+
export const Default: StoryObj<LineageFilterProps> = {
|
|
40
|
+
render: (args) => (
|
|
41
|
+
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
42
|
+
<LineageFilter
|
|
43
|
+
lapisField={args.lapisField}
|
|
44
|
+
placeholderText={args.placeholderText}
|
|
45
|
+
initialValue={args.initialValue}
|
|
46
|
+
width={args.width}
|
|
47
|
+
/>
|
|
48
|
+
</LapisUrlContext.Provider>
|
|
49
|
+
),
|
|
50
|
+
args: {
|
|
51
|
+
lapisField: 'pangoLineage',
|
|
52
|
+
placeholderText: 'Enter lineage',
|
|
53
|
+
initialValue: '',
|
|
54
|
+
width: '100%',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { useContext, useRef } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import { fetchLineageAutocompleteList } from './fetchLineageAutocompleteList';
|
|
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 { NoDataDisplay } from '../components/no-data-display';
|
|
10
|
+
import { ResizeContainer } from '../components/resize-container';
|
|
11
|
+
import { useQuery } from '../useQuery';
|
|
12
|
+
|
|
13
|
+
export interface LineageFilterInnerProps {
|
|
14
|
+
lapisField: string;
|
|
15
|
+
placeholderText?: string;
|
|
16
|
+
initialValue?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LineageFilterProps extends LineageFilterInnerProps {
|
|
20
|
+
width: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const LineageFilter: FunctionComponent<LineageFilterProps> = ({ width, ...innerProps }) => {
|
|
24
|
+
const size = { width, height: '3rem' };
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<ErrorBoundary size={size}>
|
|
28
|
+
<ResizeContainer size={size}>
|
|
29
|
+
<LineageFilterInner {...innerProps} />
|
|
30
|
+
</ResizeContainer>
|
|
31
|
+
</ErrorBoundary>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const LineageFilterInner: FunctionComponent<LineageFilterInnerProps> = ({
|
|
36
|
+
lapisField,
|
|
37
|
+
placeholderText,
|
|
38
|
+
initialValue,
|
|
39
|
+
}) => {
|
|
40
|
+
const lapis = useContext(LapisUrlContext);
|
|
41
|
+
|
|
42
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
43
|
+
|
|
44
|
+
const { data, error, isLoading } = useQuery(
|
|
45
|
+
() => fetchLineageAutocompleteList(lapis, lapisField),
|
|
46
|
+
[lapisField, lapis],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
if (isLoading) {
|
|
50
|
+
return <LoadingDisplay />;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (error !== null) {
|
|
54
|
+
return <ErrorDisplay error={error} />;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (data === null) {
|
|
58
|
+
return <NoDataDisplay />;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const onInput = () => {
|
|
62
|
+
const value = inputRef.current?.value === '' ? undefined : inputRef.current?.value;
|
|
63
|
+
|
|
64
|
+
if (isValidValue(value)) {
|
|
65
|
+
inputRef.current?.dispatchEvent(
|
|
66
|
+
new CustomEvent('gs-lineage-filter-changed', {
|
|
67
|
+
detail: { [lapisField]: value },
|
|
68
|
+
bubbles: true,
|
|
69
|
+
composed: true,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const isValidValue = (value: string | undefined) => {
|
|
76
|
+
if (value === undefined) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return data.includes(value);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<>
|
|
84
|
+
<input
|
|
85
|
+
type='text'
|
|
86
|
+
class='input input-bordered w-full'
|
|
87
|
+
placeholder={placeholderText !== undefined ? placeholderText : lapisField}
|
|
88
|
+
onInput={onInput}
|
|
89
|
+
ref={inputRef}
|
|
90
|
+
list={lapisField}
|
|
91
|
+
value={initialValue}
|
|
92
|
+
/>
|
|
93
|
+
<datalist id={lapisField}>
|
|
94
|
+
{data.map((item) => (
|
|
95
|
+
<option value={item} key={item} />
|
|
96
|
+
))}
|
|
97
|
+
</datalist>
|
|
98
|
+
</>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
@@ -244,10 +244,6 @@
|
|
|
244
244
|
"count": 416,
|
|
245
245
|
"date": "2023-09-18"
|
|
246
246
|
},
|
|
247
|
-
{
|
|
248
|
-
"count": 208,
|
|
249
|
-
"date": null
|
|
250
|
-
},
|
|
251
247
|
{
|
|
252
248
|
"count": 271,
|
|
253
249
|
"date": "2023-09-15"
|
|
@@ -624,10 +620,6 @@
|
|
|
624
620
|
"count": 313,
|
|
625
621
|
"date": "2023-10-26"
|
|
626
622
|
},
|
|
627
|
-
{
|
|
628
|
-
"count": 1,
|
|
629
|
-
"date": "2022-01-23"
|
|
630
|
-
},
|
|
631
623
|
{
|
|
632
624
|
"count": 91,
|
|
633
625
|
"date": "2023-12-31"
|
|
@@ -32,11 +32,7 @@ export const TextInput: FunctionComponent<TextInputProps> = ({ width, ...innerPr
|
|
|
32
32
|
);
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
lapisField,
|
|
37
|
-
placeholderText,
|
|
38
|
-
initialValue,
|
|
39
|
-
}) => {
|
|
35
|
+
const TextInputInner: FunctionComponent<TextInputInnerProps> = ({ lapisField, placeholderText, initialValue }) => {
|
|
40
36
|
const lapis = useContext(LapisUrlContext);
|
|
41
37
|
|
|
42
38
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { withActions } from '@storybook/addon-actions/decorator';
|
|
2
|
+
import { expect, fn, userEvent, waitFor } from '@storybook/test';
|
|
3
|
+
import type { Meta, StoryObj } from '@storybook/web-components';
|
|
4
|
+
import { html } from 'lit';
|
|
5
|
+
|
|
6
|
+
import { withComponentDocs } from '../../../.storybook/ComponentDocsBlock';
|
|
7
|
+
import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
|
|
8
|
+
import '../app';
|
|
9
|
+
import './gs-lineage-filter';
|
|
10
|
+
import aggregatedData from '../../preact/lineageFilter/__mockData__/aggregated.json';
|
|
11
|
+
import { type LineageFilterProps } from '../../preact/lineageFilter/lineage-filter';
|
|
12
|
+
import { withinShadowRoot } from '../withinShadowRoot.story';
|
|
13
|
+
|
|
14
|
+
const codeExample = String.raw`
|
|
15
|
+
<gs-lineage-filter
|
|
16
|
+
lapisField="pangoLineage"
|
|
17
|
+
placeholderText="Enter lineage"
|
|
18
|
+
initialValue="B.1.1.7"
|
|
19
|
+
width="50%">
|
|
20
|
+
</gs-lineage-filter>`;
|
|
21
|
+
|
|
22
|
+
const meta: Meta<Required<LineageFilterProps>> = {
|
|
23
|
+
title: 'Input/Lineage filter',
|
|
24
|
+
component: 'gs-lineage-filter',
|
|
25
|
+
parameters: withComponentDocs({
|
|
26
|
+
actions: {
|
|
27
|
+
handles: ['gs-lineage-filter-changed'],
|
|
28
|
+
},
|
|
29
|
+
fetchMock: {
|
|
30
|
+
mocks: [
|
|
31
|
+
{
|
|
32
|
+
matcher: {
|
|
33
|
+
name: 'pangoLineage',
|
|
34
|
+
url: AGGREGATED_ENDPOINT,
|
|
35
|
+
body: {
|
|
36
|
+
fields: ['pangoLineage'],
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
response: {
|
|
40
|
+
status: 200,
|
|
41
|
+
body: aggregatedData,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
componentDocs: {
|
|
47
|
+
opensShadowDom: true,
|
|
48
|
+
expectsChildren: false,
|
|
49
|
+
codeExample,
|
|
50
|
+
},
|
|
51
|
+
}),
|
|
52
|
+
decorators: [withActions],
|
|
53
|
+
tags: ['autodocs'],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default meta;
|
|
57
|
+
|
|
58
|
+
export const Default: StoryObj<Required<LineageFilterProps>> = {
|
|
59
|
+
render: (args) => {
|
|
60
|
+
return html` <gs-app lapis="${LAPIS_URL}">
|
|
61
|
+
<div class="max-w-screen-lg">
|
|
62
|
+
<gs-lineage-filter
|
|
63
|
+
.lapisField=${args.lapisField}
|
|
64
|
+
.placeholderText=${args.placeholderText}
|
|
65
|
+
.initialValue=${args.initialValue}
|
|
66
|
+
.width=${args.width}
|
|
67
|
+
></gs-lineage-filter>
|
|
68
|
+
</div>
|
|
69
|
+
</gs-app>`;
|
|
70
|
+
},
|
|
71
|
+
args: {
|
|
72
|
+
lapisField: 'pangoLineage',
|
|
73
|
+
placeholderText: 'Enter lineage',
|
|
74
|
+
initialValue: 'B.1.1.7',
|
|
75
|
+
width: '100%',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const FiresEvent: StoryObj<Required<LineageFilterProps>> = {
|
|
80
|
+
...Default,
|
|
81
|
+
play: async ({ canvasElement, step }) => {
|
|
82
|
+
const canvas = await withinShadowRoot(canvasElement, 'gs-lineage-filter');
|
|
83
|
+
|
|
84
|
+
const inputField = () => canvas.getByPlaceholderText('Enter lineage');
|
|
85
|
+
const listenerMock = fn();
|
|
86
|
+
await step('Setup event listener mock', async () => {
|
|
87
|
+
canvasElement.addEventListener('gs-lineage-filter-changed', listenerMock);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await step('wait until data is loaded', async () => {
|
|
91
|
+
await waitFor(() => {
|
|
92
|
+
return expect(inputField()).toBeEnabled();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await step('Enters an invalid lineage value', async () => {
|
|
97
|
+
await userEvent.type(inputField(), 'notInList');
|
|
98
|
+
await expect(listenerMock).not.toHaveBeenCalled();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
await step('Empty input', async () => {
|
|
102
|
+
await userEvent.type(inputField(), '{backspace>9/}');
|
|
103
|
+
await expect(listenerMock.mock.calls.at(-1)[0].detail).toStrictEqual({
|
|
104
|
+
pangoLineage: undefined,
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await step('Enter a valid lineage value', async () => {
|
|
109
|
+
await userEvent.type(inputField(), 'B.1.1.7');
|
|
110
|
+
|
|
111
|
+
await expect(listenerMock).toHaveBeenCalledWith(
|
|
112
|
+
expect.objectContaining({
|
|
113
|
+
detail: {
|
|
114
|
+
pangoLineage: 'B.1.1.7',
|
|
115
|
+
},
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await step('Enter a valid lineage value', async () => {
|
|
121
|
+
await userEvent.type(inputField(), '{backspace>9/}');
|
|
122
|
+
await userEvent.type(inputField(), 'B.1.1.7*');
|
|
123
|
+
|
|
124
|
+
await expect(listenerMock).toHaveBeenCalledWith(
|
|
125
|
+
expect.objectContaining({
|
|
126
|
+
detail: {
|
|
127
|
+
pangoLineage: 'B.1.1.7*',
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
args: {
|
|
134
|
+
...Default.args,
|
|
135
|
+
initialValue: '',
|
|
136
|
+
},
|
|
137
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { customElement, property } from 'lit/decorators.js';
|
|
2
|
+
|
|
3
|
+
import { LineageFilter } from '../../preact/lineageFilter/lineage-filter';
|
|
4
|
+
import { PreactLitAdapter } from '../PreactLitAdapter';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
*
|
|
8
|
+
* ## Context
|
|
9
|
+
*
|
|
10
|
+
* This component provides a text input field to filter by lineages.
|
|
11
|
+
* Currently, it is designed to work well with Pango Lineages,
|
|
12
|
+
* but it may also be used for other lineage types, if suitable.
|
|
13
|
+
*
|
|
14
|
+
* It fetches all available values of the `lapisField` from the LAPIS instance
|
|
15
|
+
* and provides an autocomplete list with the available values of the lineage and sublineage queries
|
|
16
|
+
* (a `*` appended to the lineage value).
|
|
17
|
+
*
|
|
18
|
+
* @fires {CustomEvent<Record<string, string>>} gs-lineage-filter-changed
|
|
19
|
+
* Fired when the input field is changed.
|
|
20
|
+
* The `details` of this event contain an object with the `lapisField` as key and the input value as value.
|
|
21
|
+
* Example:
|
|
22
|
+
* ```
|
|
23
|
+
* {
|
|
24
|
+
* "pangoLineage": "B.1.1.7"
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
@customElement('gs-lineage-filter')
|
|
29
|
+
export class LineageFilterComponent extends PreactLitAdapter {
|
|
30
|
+
/**
|
|
31
|
+
* The initial value to use for this lineage filter.
|
|
32
|
+
*/
|
|
33
|
+
@property()
|
|
34
|
+
initialValue: string = '';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Required.
|
|
38
|
+
*
|
|
39
|
+
* The LAPIS field name to use for this lineage filter.
|
|
40
|
+
* The field must exist on this LAPIS instance.
|
|
41
|
+
*/
|
|
42
|
+
@property()
|
|
43
|
+
lapisField = '';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The placeholder text to display in the input field.
|
|
47
|
+
*/
|
|
48
|
+
@property()
|
|
49
|
+
placeholderText: string = '';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The width of the component.
|
|
53
|
+
*
|
|
54
|
+
* Visit https://genspectrum.github.io/dashboard-components/?path=/docs/components-size-of-components--docs for more information.
|
|
55
|
+
*/
|
|
56
|
+
@property({ type: String })
|
|
57
|
+
width: string = '100%';
|
|
58
|
+
|
|
59
|
+
override render() {
|
|
60
|
+
return (
|
|
61
|
+
<LineageFilter
|
|
62
|
+
lapisField={this.lapisField}
|
|
63
|
+
placeholderText={this.placeholderText}
|
|
64
|
+
initialValue={this.initialValue}
|
|
65
|
+
width={this.width}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
declare global {
|
|
72
|
+
interface HTMLElementTagNameMap {
|
|
73
|
+
'gs-lineage-filter': LineageFilterComponent;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface HTMLElementEventMap {
|
|
77
|
+
'gs-lineage-filter-changed': CustomEvent<Record<string, string>>;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -2,3 +2,4 @@ export { DateRangeSelectorComponent } from './gs-date-range-selector';
|
|
|
2
2
|
export { LocationFilterComponent } from './gs-location-filter';
|
|
3
3
|
export { TextInputComponent } from './gs-text-input';
|
|
4
4
|
export { MutationFilterComponent } from './gs-mutation-filter';
|
|
5
|
+
export { LineageFilterComponent } from './gs-lineage-filter';
|