@genspectrum/dashboard-components 0.18.4 → 0.18.6
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/README.md +12 -0
- package/custom-elements.json +1 -1
- package/dist/components.d.ts +44 -44
- package/dist/components.js +826 -343
- package/dist/components.js.map +1 -1
- package/dist/style.css +2 -2
- package/dist/util.d.ts +44 -44
- package/package.json +2 -2
- package/src/preact/MutationAnnotationsContext.tsx +34 -27
- package/src/preact/components/dropdown.tsx +1 -1
- package/src/preact/components/info.tsx +1 -1
- package/src/preact/components/mutations-over-time-text-filter.stories.tsx +57 -0
- package/src/preact/components/mutations-over-time-text-filter.tsx +63 -0
- package/src/preact/components/segment-selector.stories.tsx +12 -5
- package/src/preact/components/segment-selector.tsx +11 -7
- package/src/preact/mutationComparison/mutation-comparison.tsx +5 -1
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +169 -50
- package/src/preact/mutationFilter/mutation-filter.tsx +239 -234
- package/src/preact/mutationFilter/parseAndValidateMutation.ts +62 -10
- package/src/preact/mutationFilter/parseMutation.spec.ts +62 -47
- package/src/preact/mutations/mutations.tsx +5 -1
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +128 -0
- package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +39 -2
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +9 -12
- package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +27 -0
- package/src/preact/mutationsOverTime/mutations-over-time.tsx +31 -6
- package/src/preact/sequencesByLocation/__mockData__/worldAtlas.json +1 -1
- package/src/preact/shared/tanstackTable/pagination-context.tsx +30 -0
- package/src/preact/shared/tanstackTable/pagination.tsx +41 -21
- package/src/preact/shared/tanstackTable/tanstackTable.tsx +17 -3
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +22 -4
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +11 -2
- package/src/web-components/input/gs-mutation-filter.stories.ts +4 -4
- package/src/web-components/visualization/gs-prevalence-over-time.stories.ts +1 -1
- package/standalone-bundle/dashboard-components.js +12896 -13334
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createContext, type FunctionComponent } from 'preact';
|
|
2
|
+
import { type Dispatch, type StateUpdater, useContext, useState } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import type { PageSizes } from './pagination';
|
|
5
|
+
|
|
6
|
+
type PageSizeContext = {
|
|
7
|
+
pageSize: number;
|
|
8
|
+
setPageSize: Dispatch<StateUpdater<number>>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const pageSizeContext = createContext<PageSizeContext>({
|
|
12
|
+
pageSize: -1,
|
|
13
|
+
setPageSize: () => {
|
|
14
|
+
throw new Error('pageSizeContext not initialized');
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export function usePageSizeContext() {
|
|
19
|
+
return useContext(pageSizeContext);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type PageSizeContextProviderProps = {
|
|
23
|
+
pageSizes: PageSizes;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const PageSizeContextProvider: FunctionComponent<PageSizeContextProviderProps> = ({ children, pageSizes }) => {
|
|
27
|
+
const [pageSize, setPageSize] = useState(typeof pageSizes === 'number' ? pageSizes : (pageSizes.at(0) ?? 10));
|
|
28
|
+
|
|
29
|
+
return <pageSizeContext.Provider value={{ pageSize, setPageSize }}>{children}</pageSizeContext.Provider>;
|
|
30
|
+
};
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { Table } from '@tanstack/table-core';
|
|
2
2
|
import z from 'zod';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import { usePageSizeContext } from './pagination-context';
|
|
5
|
+
|
|
6
|
+
type PaginationProps = {
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
table: Table<any>;
|
|
9
|
+
};
|
|
6
10
|
export const pageSizesSchema = z.union([z.array(z.number()), z.number()]);
|
|
7
11
|
export type PageSizes = z.infer<typeof pageSizesSchema>;
|
|
8
12
|
|
|
@@ -13,11 +17,15 @@ export function Pagination({
|
|
|
13
17
|
pageSizes: PageSizes;
|
|
14
18
|
}) {
|
|
15
19
|
return (
|
|
16
|
-
<div className='
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
<div className='@container'>
|
|
21
|
+
<div className='flex items-center gap-x-6 gap-y-2 flex-wrap @xl:justify-end justify-center'>
|
|
22
|
+
<PageSizeSelector table={table} pageSizes={pageSizes} />
|
|
23
|
+
<PageIndicator table={table} />
|
|
24
|
+
<div className='@xl:block hidden'>
|
|
25
|
+
<GotoPageSelector table={table} />
|
|
26
|
+
</div>
|
|
27
|
+
<SelectPageButtons table={table} />
|
|
28
|
+
</div>
|
|
21
29
|
</div>
|
|
22
30
|
);
|
|
23
31
|
}
|
|
@@ -27,34 +35,46 @@ function PageIndicator({ table }: PaginationProps) {
|
|
|
27
35
|
return null;
|
|
28
36
|
}
|
|
29
37
|
|
|
38
|
+
const minRow = table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1;
|
|
39
|
+
const maxRow = minRow + table.getRowModel().rows.length - 1;
|
|
40
|
+
const numRows = table.getCoreRowModel().rows.length;
|
|
41
|
+
|
|
30
42
|
return (
|
|
31
|
-
<span className='
|
|
32
|
-
|
|
33
|
-
<strong>
|
|
34
|
-
{table.getState().pagination.pageIndex + 1} of {table.getPageCount().toLocaleString()}
|
|
35
|
-
</strong>
|
|
43
|
+
<span className='text-sm'>
|
|
44
|
+
{minRow} - {maxRow} of {numRows}
|
|
36
45
|
</span>
|
|
37
46
|
);
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
const heightForSmallerLines = 'h-[calc(var(--size)*0.7)]';
|
|
50
|
+
|
|
40
51
|
function PageSizeSelector({
|
|
41
52
|
table,
|
|
42
53
|
pageSizes,
|
|
43
54
|
}: PaginationProps & {
|
|
44
55
|
pageSizes: PageSizes;
|
|
45
56
|
}) {
|
|
57
|
+
const { pageSize, setPageSize } = usePageSizeContext();
|
|
58
|
+
|
|
46
59
|
if (typeof pageSizes === 'number' || pageSizes.length <= 1) {
|
|
47
60
|
return null;
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
return (
|
|
51
|
-
<label className='flex items-center
|
|
52
|
-
<div className={'text-nowrap'}>Rows per page:</div>
|
|
64
|
+
<label className='flex items-center'>
|
|
65
|
+
<div className={'text-nowrap text-sm'}>Rows per page:</div>
|
|
53
66
|
<select
|
|
54
|
-
className={
|
|
55
|
-
value={
|
|
67
|
+
className={`select select-ghost select-sm ${heightForSmallerLines}`}
|
|
68
|
+
value={pageSize}
|
|
56
69
|
onChange={(e) => {
|
|
57
|
-
|
|
70
|
+
const pageSize = Number(e.currentTarget?.value);
|
|
71
|
+
if (Number.isNaN(pageSize)) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Invalid page size selected: The value ${e.currentTarget?.value} could not be parsed as a number.`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
setPageSize(pageSize);
|
|
77
|
+
table.setPageSize(pageSize);
|
|
58
78
|
}}
|
|
59
79
|
aria-label='Select number of rows per page'
|
|
60
80
|
>
|
|
@@ -74,8 +94,8 @@ function GotoPageSelector({ table }: PaginationProps) {
|
|
|
74
94
|
}
|
|
75
95
|
|
|
76
96
|
return (
|
|
77
|
-
<label className='
|
|
78
|
-
Go to page
|
|
97
|
+
<label className='items-center flex'>
|
|
98
|
+
<span className='text-nowrap text-sm'>Go to page:</span>
|
|
79
99
|
<input
|
|
80
100
|
type='number'
|
|
81
101
|
min='1'
|
|
@@ -83,9 +103,9 @@ function GotoPageSelector({ table }: PaginationProps) {
|
|
|
83
103
|
defaultValue={table.getState().pagination.pageIndex + 1}
|
|
84
104
|
onChange={(e) => {
|
|
85
105
|
const page = e.currentTarget.value ? Number(e.currentTarget.value) - 1 : 0;
|
|
86
|
-
table.setPageIndex(page);
|
|
106
|
+
table.setPageIndex(Math.min(page, table.getPageCount() - 1));
|
|
87
107
|
}}
|
|
88
|
-
className=
|
|
108
|
+
className={`input input-ghost input-sm ${heightForSmallerLines}`}
|
|
89
109
|
aria-label='Enter page number to go to'
|
|
90
110
|
/>
|
|
91
111
|
</label>
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { createTable, type RowData, type TableOptions, type TableOptionsResolved } from '@tanstack/table-core';
|
|
2
2
|
import { type ComponentType, h, type VNode } from 'preact';
|
|
3
|
-
import { useState } from 'preact/hooks';
|
|
3
|
+
import { useEffect, useState } from 'preact/hooks';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import { usePageSizeContext } from './pagination-context';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
export * from '@tanstack/table-core';
|
|
8
8
|
|
|
9
9
|
export type Renderable<TProps> = VNode<TProps> | ComponentType<TProps> | undefined | null | string | number | boolean;
|
|
10
10
|
|
|
11
|
+
/*
|
|
12
|
+
* Adapted from https://github.com/TanStack/table/blob/55ea94863b6b6e6d17bd51ecda61c6a6a1262c88/packages/preact-table/src/FlexRender.tsx
|
|
13
|
+
*/
|
|
11
14
|
export function flexRender<TProps extends object>(Comp: Renderable<TProps>, props: TProps) {
|
|
12
15
|
return !Comp ? null : typeof Comp === 'function' ? <Comp {...props} /> : Comp;
|
|
13
16
|
}
|
|
14
17
|
|
|
18
|
+
/*
|
|
19
|
+
* Taken from https://github.com/TanStack/table/blob/f7bf6f1adfa4f8b28b9968b29745f2452d4be9d8/packages/react-table/src/index.tsx
|
|
20
|
+
*/
|
|
15
21
|
export function usePreactTable<TData extends RowData>(options: TableOptions<TData>) {
|
|
16
22
|
const resolvedOptions: TableOptionsResolved<TData> = {
|
|
17
23
|
state: {},
|
|
@@ -39,5 +45,13 @@ export function usePreactTable<TData extends RowData>(options: TableOptions<TDat
|
|
|
39
45
|
},
|
|
40
46
|
}));
|
|
41
47
|
|
|
48
|
+
const { pageSize } = usePageSizeContext();
|
|
49
|
+
useEffect(
|
|
50
|
+
() => {
|
|
51
|
+
tableRef.current.setPageSize(pageSize);
|
|
52
|
+
},
|
|
53
|
+
[pageSize], // eslint-disable-line react-hooks/exhaustive-deps -- only run this when the pageSize changes
|
|
54
|
+
);
|
|
55
|
+
|
|
42
56
|
return tableRef.current;
|
|
43
57
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
-
import { expect } from '@storybook/test';
|
|
2
|
+
import { expect, userEvent } from '@storybook/test';
|
|
3
3
|
|
|
4
4
|
import { WastewaterMutationsOverTime, type WastewaterMutationsOverTimeProps } from './wastewater-mutations-over-time';
|
|
5
5
|
import { WISE_DETAILS_ENDPOINT, WISE_LAPIS_URL } from '../../../constants';
|
|
@@ -67,6 +67,24 @@ export const Default: StoryObj<WastewaterMutationsOverTimeProps> = {
|
|
|
67
67
|
},
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
+
export const ChangingRowsPerPageChangesItForEveryTag: StoryObj<WastewaterMutationsOverTimeProps> = {
|
|
71
|
+
...Default,
|
|
72
|
+
play: async ({ canvas, step }) => {
|
|
73
|
+
await step('Wait for component to render', async () => {
|
|
74
|
+
await canvas.findByText('Lugano');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const getRowsPerPageSelectors = async () => await canvas.findAllByLabelText('Rows per page', { exact: false });
|
|
78
|
+
|
|
79
|
+
await step('change rows per page', async () => {
|
|
80
|
+
await expect((await getRowsPerPageSelectors())[0]).toHaveValue('10');
|
|
81
|
+
await expect((await getRowsPerPageSelectors())[1]).toHaveValue('10');
|
|
82
|
+
await userEvent.selectOptions((await getRowsPerPageSelectors())[0], '20');
|
|
83
|
+
await expect((await getRowsPerPageSelectors())[1]).toHaveValue('20');
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
70
88
|
export const AminoAcids: StoryObj<WastewaterMutationsOverTimeProps> = {
|
|
71
89
|
...Default,
|
|
72
90
|
args: {
|
|
@@ -75,11 +93,11 @@ export const AminoAcids: StoryObj<WastewaterMutationsOverTimeProps> = {
|
|
|
75
93
|
},
|
|
76
94
|
play: async ({ canvas, step }) => {
|
|
77
95
|
await step('Wait for component to render', async () => {
|
|
78
|
-
await canvas.findByText('All
|
|
96
|
+
await canvas.findByText('All genes');
|
|
79
97
|
});
|
|
80
98
|
|
|
81
|
-
await step("Click 'All
|
|
82
|
-
canvas.getByRole('button', { name: 'All
|
|
99
|
+
await step("Click 'All genes' button", async () => {
|
|
100
|
+
canvas.getByRole('button', { name: 'All genes' }).click();
|
|
83
101
|
await expect(canvas.getByText('Select none')).toBeInTheDocument();
|
|
84
102
|
canvas.getByRole('button', { name: 'Select none' }).click();
|
|
85
103
|
await canvas.findAllByText('No data available for your filters.');
|
|
@@ -19,6 +19,7 @@ import Tabs from '../../components/tabs';
|
|
|
19
19
|
import { type MutationOverTimeDataMap } from '../../mutationsOverTime/MutationOverTimeData';
|
|
20
20
|
import MutationsOverTimeGrid from '../../mutationsOverTime/mutations-over-time-grid';
|
|
21
21
|
import { pageSizesSchema } from '../../shared/tanstackTable/pagination';
|
|
22
|
+
import { PageSizeContextProvider } from '../../shared/tanstackTable/pagination-context';
|
|
22
23
|
import { useQuery } from '../../useQuery';
|
|
23
24
|
|
|
24
25
|
const wastewaterMutationOverTimeSchema = z.object({
|
|
@@ -161,7 +162,11 @@ const MutationsOverTimeTabs: FunctionComponent<MutationOverTimeTabsProps> = ({
|
|
|
161
162
|
/>
|
|
162
163
|
);
|
|
163
164
|
|
|
164
|
-
return
|
|
165
|
+
return (
|
|
166
|
+
<PageSizeContextProvider pageSizes={originalComponentProps.pageSizes}>
|
|
167
|
+
<Tabs tabs={tabs} toolbar={toolbar} />
|
|
168
|
+
</PageSizeContextProvider>
|
|
169
|
+
);
|
|
165
170
|
};
|
|
166
171
|
|
|
167
172
|
type ToolbarProps = {
|
|
@@ -183,7 +188,11 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({
|
|
|
183
188
|
return (
|
|
184
189
|
<>
|
|
185
190
|
<ColorScaleSelectorDropdown colorScale={colorScale} setColorScale={setColorScale} />
|
|
186
|
-
<SegmentSelector
|
|
191
|
+
<SegmentSelector
|
|
192
|
+
displayedSegments={displayedSegments}
|
|
193
|
+
setDisplayedSegments={setDisplayedSegments}
|
|
194
|
+
sequenceType={originalComponentProps.sequenceType}
|
|
195
|
+
/>
|
|
187
196
|
<WastewaterMutationsOverTimeInfo originalComponentProps={originalComponentProps} />
|
|
188
197
|
<Fullscreen />
|
|
189
198
|
</>
|
|
@@ -107,7 +107,7 @@ export const MultiSegmentedReferenceGenomes: StoryObj<MutationFilterProps> = {
|
|
|
107
107
|
...Template,
|
|
108
108
|
args: {
|
|
109
109
|
...Template.args,
|
|
110
|
-
initialValue: ['seg1:
|
|
110
|
+
initialValue: ['seg1:3T', 'gene2:4', 'ins_seg2:4:AAA'],
|
|
111
111
|
},
|
|
112
112
|
parameters: {
|
|
113
113
|
fetchMock: {
|
|
@@ -163,9 +163,9 @@ export const MultiSegmentedReferenceGenomes: StoryObj<MutationFilterProps> = {
|
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
await waitFor(async () => {
|
|
166
|
-
await expect(canvas.getByText('seg1:
|
|
167
|
-
await expect(canvas.getByText('gene2:
|
|
168
|
-
await expect(canvas.getByText('ins_seg2:
|
|
166
|
+
await expect(canvas.getByText('seg1:3T')).toBeVisible();
|
|
167
|
+
await expect(canvas.getByText('gene2:4')).toBeVisible();
|
|
168
|
+
await expect(canvas.getByText('ins_seg2:4:AAA')).toBeVisible();
|
|
169
169
|
});
|
|
170
170
|
},
|
|
171
171
|
};
|
|
@@ -16,7 +16,7 @@ import { withinShadowRoot } from '../withinShadowRoot.story';
|
|
|
16
16
|
|
|
17
17
|
const codeExample = String.raw`
|
|
18
18
|
<gs-prevalence-over-time
|
|
19
|
-
|
|
19
|
+
numeratorFilters='[{ "displayName": "EG", "lapisFilter": { "country": "USA", "pangoLineage": "EG*" }}, { "displayName": "JN.1", "lapisFilter": { "country": "USA", "pangoLineage": "JN.1*" }}]'
|
|
20
20
|
denominatorFilter='{ "country": "USA"}'
|
|
21
21
|
granularity="month"
|
|
22
22
|
smoothingWindow="0"
|