@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.
@@ -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
- export const TextInputInner: FunctionComponent<TextInputInnerProps> = ({
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';
@@ -157,6 +157,7 @@ export const TwoDatasets: StoryObj<NumberSequencesOverTimeProps> = {
157
157
  body: {
158
158
  country: 'USA',
159
159
  pangoLineage: 'EG*',
160
+ dateFrom: '2022-10-01',
160
161
  fields: ['date'],
161
162
  },
162
163
  },