@genspectrum/dashboard-components 0.18.5 → 0.19.0

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 (46) hide show
  1. package/README.md +12 -2
  2. package/custom-elements.json +3 -3
  3. package/dist/assets/{mutationOverTimeWorker--b8ZHlji.js.map → mutationOverTimeWorker-ChQTFL68.js.map} +1 -1
  4. package/dist/components.d.ts +15 -14
  5. package/dist/components.js +1602 -332
  6. package/dist/components.js.map +1 -1
  7. package/dist/util.d.ts +14 -14
  8. package/package.json +3 -4
  9. package/src/preact/MutationAnnotationsContext.tsx +34 -27
  10. package/src/preact/components/dropdown.tsx +1 -1
  11. package/src/preact/components/info.tsx +1 -2
  12. package/src/preact/components/min-max-range-slider.tsx +0 -2
  13. package/src/preact/components/mutations-over-time-text-filter.stories.tsx +57 -0
  14. package/src/preact/components/mutations-over-time-text-filter.tsx +63 -0
  15. package/src/preact/components/segment-selector.tsx +1 -1
  16. package/src/preact/components/table.tsx +0 -2
  17. package/src/preact/dateRangeFilter/date-picker.tsx +15 -10
  18. package/src/preact/mutationFilter/mutation-filter.stories.tsx +169 -50
  19. package/src/preact/mutationFilter/mutation-filter.tsx +239 -234
  20. package/src/preact/mutationFilter/parseAndValidateMutation.ts +62 -10
  21. package/src/preact/mutationFilter/parseMutation.spec.ts +62 -47
  22. package/src/preact/mutationsOverTime/getFilteredMutationsOverTime.spec.ts +128 -0
  23. package/src/preact/mutationsOverTime/getFilteredMutationsOverTimeData.ts +39 -2
  24. package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +8 -11
  25. package/src/preact/mutationsOverTime/mutations-over-time.stories.tsx +27 -0
  26. package/src/preact/mutationsOverTime/mutations-over-time.tsx +26 -5
  27. package/src/preact/shared/tanstackTable/pagination-context.tsx +30 -0
  28. package/src/preact/shared/tanstackTable/pagination.tsx +19 -6
  29. package/src/preact/shared/tanstackTable/tanstackTable.tsx +17 -3
  30. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +19 -1
  31. package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +6 -1
  32. package/src/styles/replaceCssProperties.stories.tsx +49 -0
  33. package/src/styles/replaceCssProperties.ts +25 -0
  34. package/src/styles/tailwind.css +1 -0
  35. package/src/web-components/PreactLitAdapter.tsx +6 -3
  36. package/src/web-components/PreactLitAdapterWithGridJsStyles.tsx +0 -2
  37. package/src/web-components/gs-app.stories.ts +6 -2
  38. package/src/web-components/gs-app.ts +4 -1
  39. package/src/web-components/input/gs-date-range-filter.tsx +6 -0
  40. package/src/web-components/input/gs-mutation-filter.stories.ts +4 -4
  41. package/src/web-components/visualization/gs-prevalence-over-time.stories.ts +1 -1
  42. package/standalone-bundle/assets/mutationOverTimeWorker-jChgWnwp.js.map +1 -1
  43. package/standalone-bundle/dashboard-components.js +10836 -11289
  44. package/standalone-bundle/dashboard-components.js.map +1 -1
  45. package/dist/style.css +0 -392
  46. package/standalone-bundle/style.css +0 -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
- import z from 'zod'; // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
+ import z from 'zod';
3
3
 
4
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
- type PaginationProps = { table: Table<any> };
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
 
@@ -50,6 +54,8 @@ function PageSizeSelector({
50
54
  }: PaginationProps & {
51
55
  pageSizes: PageSizes;
52
56
  }) {
57
+ const { pageSize, setPageSize } = usePageSizeContext();
58
+
53
59
  if (typeof pageSizes === 'number' || pageSizes.length <= 1) {
54
60
  return null;
55
61
  }
@@ -59,9 +65,16 @@ function PageSizeSelector({
59
65
  <div className={'text-nowrap text-sm'}>Rows per page:</div>
60
66
  <select
61
67
  className={`select select-ghost select-sm ${heightForSmallerLines}`}
62
- value={table.getState().pagination.pageSize}
68
+ value={pageSize}
63
69
  onChange={(e) => {
64
- table.setPageSize(Number(e.currentTarget?.value));
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);
65
78
  }}
66
79
  aria-label='Select number of rows per page'
67
80
  >
@@ -90,7 +103,7 @@ function GotoPageSelector({ table }: PaginationProps) {
90
103
  defaultValue={table.getState().pagination.pageIndex + 1}
91
104
  onChange={(e) => {
92
105
  const page = e.currentTarget.value ? Number(e.currentTarget.value) - 1 : 0;
93
- table.setPageIndex(page);
106
+ table.setPageIndex(Math.min(page, table.getPageCount() - 1));
94
107
  }}
95
108
  className={`input input-ghost input-sm ${heightForSmallerLines}`}
96
109
  aria-label='Enter page number to go to'
@@ -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
- export * from '@tanstack/table-core';
5
+ import { usePageSizeContext } from './pagination-context';
6
6
 
7
- // Adapted from https://github.com/TanStack/table/blob/55ea94863b6b6e6d17bd51ecda61c6a6a1262c88/packages/preact-table/src/FlexRender.tsx
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: {
@@ -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 <Tabs tabs={tabs} toolbar={toolbar} />;
165
+ return (
166
+ <PageSizeContextProvider pageSizes={originalComponentProps.pageSizes}>
167
+ <Tabs tabs={tabs} toolbar={toolbar} />
168
+ </PageSizeContextProvider>
169
+ );
165
170
  };
166
171
 
167
172
  type ToolbarProps = {
@@ -0,0 +1,49 @@
1
+ import { css } from '@lit/reactive-element';
2
+ import { type Meta, type StoryObj } from '@storybook/preact';
3
+ import { expect } from '@storybook/test';
4
+ import { type FunctionComponent } from 'preact';
5
+
6
+ import { replaceCssProperties } from './replaceCssProperties';
7
+
8
+ const DummyComponent: FunctionComponent = () => 'This just runs a function in the browser';
9
+
10
+ const meta: Meta = {
11
+ title: 'Test/replaceCssProperties',
12
+ component: DummyComponent,
13
+ };
14
+
15
+ export default meta;
16
+
17
+ /**
18
+ * This test somehow doesn't work with vitest on node, because the @properties are not parsed by `css()`.
19
+ */
20
+ export const InvalidProps: StoryObj = {
21
+ play: async () => {
22
+ const styleSheet = css`
23
+ .some-other-rule {
24
+ color: red;
25
+ }
26
+
27
+ @property --test-with-initial-value {
28
+ syntax: '*';
29
+ inherits: false;
30
+ initial-value: solid;
31
+ }
32
+
33
+ @property --test-without-initial-value {
34
+ syntax: '*';
35
+ inherits: false;
36
+ }
37
+ `.styleSheet!;
38
+
39
+ replaceCssProperties(styleSheet);
40
+
41
+ const resultingCss = [...styleSheet.cssRules]
42
+ .map((rule) => rule.cssText)
43
+ .join('\n')
44
+ .replaceAll(' ', '');
45
+
46
+ await expect(resultingCss).toContain(':host{--test-with-initial-value:solid;}');
47
+ await expect(resultingCss).toContain('.some-other-rule{color:red;}');
48
+ },
49
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Replaces `@property` rules in a CSSStyleSheet with a `:host` rule.
3
+ *
4
+ * Tailwind uses `@property` rules, but they don't work in the shadow DOM.
5
+ * https://github.com/tailwindlabs/tailwindcss/issues/15005
6
+ *
7
+ * Inspired by https://github.com/tailwindlabs/tailwindcss/issues/15005#issuecomment-2737489813
8
+ */
9
+ export function replaceCssProperties(styleSheet: CSSStyleSheet) {
10
+ const properties: string[] = [];
11
+
12
+ [...styleSheet.cssRules]
13
+ .map((rule, index) => [rule, index] as const)
14
+ .reverse()
15
+ .forEach(([rule, index]) => {
16
+ if (rule instanceof CSSPropertyRule && rule.initialValue !== null) {
17
+ styleSheet.deleteRule(index);
18
+ properties.push(`${rule.name}: ${rule.initialValue}`);
19
+ }
20
+ });
21
+
22
+ if (properties.length > 0) {
23
+ styleSheet.insertRule(`:host { ${properties.join('; ')} }`);
24
+ }
25
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  @plugin "daisyui" {
4
4
  themes: light --default;
5
+ root: ':host';
5
6
  }
6
7
  @plugin "@iconify/tailwind4" {
7
8
  prefixes: mdi, mdi-light;
@@ -10,14 +10,17 @@ import { type ReferenceGenome } from '../lapisApi/ReferenceGenome';
10
10
  import { LapisUrlContextProvider } from '../preact/LapisUrlContext';
11
11
  import { INITIAL_REFERENCE_GENOMES, ReferenceGenomeContext } from '../preact/ReferenceGenomeContext';
12
12
  import minMaxPercentSliderCss from '../preact/components/min-max-percent-slider.css?inline';
13
+ import { replaceCssProperties } from '../styles/replaceCssProperties';
13
14
  import tailwindStyle from '../styles/tailwind.css?inline';
14
15
 
15
- import '../styles/tailwind.css';
16
- import '../preact/components/min-max-percent-slider.css';
17
-
18
16
  const tailwindElementCss = unsafeCSS(tailwindStyle);
19
17
  const minMaxPercentSliderElementCss = unsafeCSS(minMaxPercentSliderCss);
20
18
 
19
+ const styleSheet = tailwindElementCss.styleSheet;
20
+ if (styleSheet !== undefined) {
21
+ replaceCssProperties(styleSheet);
22
+ }
23
+
21
24
  export abstract class PreactLitAdapter extends ReactiveElement {
22
25
  static override styles = [tailwindElementCss, minMaxPercentSliderElementCss];
23
26
 
@@ -3,8 +3,6 @@ import { unsafeCSS } from 'lit';
3
3
 
4
4
  import { PreactLitAdapter } from './PreactLitAdapter';
5
5
 
6
- import 'gridjs/dist/theme/mermaid.css';
7
-
8
6
  const gridJsElementCss = unsafeCSS(gridJsStyle);
9
7
 
10
8
  export abstract class PreactLitAdapterWithGridJsStyles extends PreactLitAdapter {
@@ -82,7 +82,9 @@ export const WithNoLapisUrl: StoryObj<StoryProps> = {
82
82
  const canvas = within(canvasElement);
83
83
 
84
84
  await waitFor(async () => {
85
- await expect(canvas.getByText("Error: Invalid LAPIS URL: 'notAValidUrl'", { exact: false })).toBeVisible();
85
+ await expect(
86
+ canvas.getByText("Error in gs-app: Invalid LAPIS URL: 'notAValidUrl'", { exact: false }),
87
+ ).toBeVisible();
86
88
  });
87
89
  },
88
90
  };
@@ -119,7 +121,9 @@ export const FailsToFetchReferenceGenome: StoryObj<StoryProps> = {
119
121
  const canvas = within(canvasElement);
120
122
 
121
123
  await waitFor(async () => {
122
- await expect(canvas.getByText('Error: Cannot fetch reference genome.', { exact: false })).toBeVisible();
124
+ await expect(
125
+ canvas.getByText('Error in gs-app: Cannot fetch reference genome.', { exact: false }),
126
+ ).toBeVisible();
123
127
  });
124
128
  },
125
129
  };
@@ -98,7 +98,10 @@ export class AppComponent extends LitElement {
98
98
  }
99
99
 
100
100
  function GsAppError(error: string) {
101
- return html` <div class="m-2 w-full alert alert-error">Error: ${error}</div>`;
101
+ // We're in the light dom, we must not use Tailwind so that we don't pollute the user's styles.
102
+ return html`<div style="padding: 0.5rem; border: solid red; background-color: lightcoral; border-radius: 0.5rem;">
103
+ Error in gs-app: ${error}
104
+ </div>`;
102
105
  }
103
106
 
104
107
  declare global {
@@ -1,3 +1,5 @@
1
+ import flatpickrStyle from 'flatpickr/dist/flatpickr.css?inline';
2
+ import { unsafeCSS } from 'lit';
1
3
  import { customElement, property } from 'lit/decorators.js';
2
4
  import type { DetailedHTMLProps, HTMLAttributes } from 'react';
3
5
 
@@ -6,6 +8,8 @@ import { type DateRangeOptionChangedEvent } from '../../preact/dateRangeFilter/d
6
8
  import { type Equals, type Expect } from '../../utils/typeAssertions';
7
9
  import { PreactLitAdapter } from '../PreactLitAdapter';
8
10
 
11
+ const flatpickrCss = unsafeCSS(flatpickrStyle);
12
+
9
13
  /**
10
14
  * ## Context
11
15
  * This component is a group of input fields designed to specify date range filters
@@ -52,6 +56,8 @@ import { PreactLitAdapter } from '../PreactLitAdapter';
52
56
  */
53
57
  @customElement('gs-date-range-filter')
54
58
  export class DateRangeFilterComponent extends PreactLitAdapter {
59
+ static override styles = [...PreactLitAdapter.styles, flatpickrCss];
60
+
55
61
  /**
56
62
  * An array of date range options that the select field should provide.
57
63
  * The `label` will be shown to the user, and it will be available as `value`.
@@ -107,7 +107,7 @@ export const MultiSegmentedReferenceGenomes: StoryObj<MutationFilterProps> = {
107
107
  ...Template,
108
108
  args: {
109
109
  ...Template.args,
110
- initialValue: ['seg1:123T', 'gene2:56', 'ins_seg2:78:AAA'],
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:123T')).toBeVisible();
167
- await expect(canvas.getByText('gene2:56')).toBeVisible();
168
- await expect(canvas.getByText('ins_seg2:78:AAA')).toBeVisible();
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
- numeratorFilter='[{ "displayName": "EG", "lapisFilter": { "country": "USA", "pangoLineage": "EG*" }}, { "displayName": "JN.1", "lapisFilter": { "country": "USA", "pangoLineage": "JN.1*" }}]'
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"