@genspectrum/dashboard-components 0.1.3 → 0.1.5
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 +488 -117
- package/dist/dashboard-components.js +904 -466
- package/dist/dashboard-components.js.map +1 -1
- package/dist/genspectrum-components.d.ts +473 -67
- package/dist/style.css +273 -153
- package/package.json +11 -7
- package/src/preact/aggregatedData/aggregate.stories.tsx +7 -5
- package/src/preact/aggregatedData/aggregate.tsx +16 -7
- package/src/preact/components/ReferenceGenomesAwaiter.tsx +25 -0
- package/src/preact/components/csv-download-button.tsx +8 -2
- package/src/preact/components/headline.stories.tsx +19 -1
- package/src/preact/components/headline.tsx +25 -5
- package/src/preact/components/info.stories.tsx +24 -3
- package/src/preact/components/info.tsx +49 -5
- package/src/preact/components/min-max-range-slider.tsx +4 -4
- package/src/preact/components/percent-intput.tsx +2 -3
- package/src/preact/components/resize-container.tsx +23 -0
- package/src/preact/components/table.tsx +1 -0
- package/src/preact/components/tabs.stories.tsx +2 -2
- package/src/preact/components/tabs.tsx +47 -24
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +36 -4
- package/src/preact/dateRangeSelector/date-range-selector.tsx +67 -53
- package/src/preact/locationFilter/location-filter.tsx +2 -2
- package/src/preact/mutationComparison/getMutationComparisonTableData.spec.ts +5 -5
- package/src/preact/mutationComparison/getMutationComparisonTableData.ts +45 -10
- package/src/preact/mutationComparison/mutation-comparison-table.tsx +20 -22
- package/src/preact/mutationComparison/mutation-comparison-venn.tsx +6 -3
- package/src/preact/mutationComparison/mutation-comparison.stories.tsx +11 -1
- package/src/preact/mutationComparison/mutation-comparison.tsx +16 -7
- package/src/preact/mutationFilter/mutation-filter.stories.tsx +70 -31
- package/src/preact/mutationFilter/mutation-filter.tsx +62 -14
- package/src/preact/mutations/getInsertionsTableData.spec.ts +6 -4
- package/src/preact/mutations/getInsertionsTableData.ts +1 -1
- package/src/preact/mutations/getMutationsTableData.spec.ts +9 -19
- package/src/preact/mutations/getMutationsTableData.ts +1 -1
- package/src/preact/mutations/mutations-insertions-table.tsx +3 -1
- package/src/preact/mutations/mutations-table.tsx +3 -1
- package/src/preact/mutations/mutations.stories.tsx +11 -1
- package/src/preact/mutations/mutations.tsx +24 -7
- package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +1 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +1 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +1 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +8 -0
- package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +31 -13
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +8 -5
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +15 -0
- package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +62 -12
- package/src/preact/shared/sort/sortInsertions.spec.ts +11 -10
- package/src/preact/shared/sort/sortInsertions.ts +10 -17
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +19 -10
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +45 -12
- package/src/preact/textInput/text-input.stories.tsx +22 -1
- package/src/preact/textInput/text-input.tsx +3 -1
- package/src/utils/typeAssertions.spec.ts +31 -0
- package/src/utils/typeAssertions.ts +16 -0
- package/src/web-components/PreactLitAdapter.tsx +0 -1
- package/src/web-components/app.stories.ts +129 -0
- package/src/web-components/app.ts +27 -6
- package/src/web-components/display/aggregate-component.stories.ts +24 -11
- package/src/web-components/display/aggregate-component.tsx +26 -5
- package/src/web-components/display/mutation-comparison-component.stories.ts +32 -11
- package/src/web-components/display/mutation-comparison-component.tsx +79 -4
- package/src/web-components/display/mutations-component.stories.ts +40 -19
- package/src/web-components/display/mutations-component.tsx +71 -4
- package/src/web-components/display/prevalence-over-time-component.stories.ts +44 -18
- package/src/web-components/display/prevalence-over-time-component.tsx +105 -5
- package/src/web-components/display/relative-growth-advantage-component.stories.ts +32 -10
- package/src/web-components/display/relative-growth-advantage-component.tsx +66 -3
- package/src/web-components/input/date-range-selector-component.stories.ts +51 -9
- package/src/web-components/input/date-range-selector-component.tsx +69 -4
- package/src/web-components/input/location-filter-component.stories.ts +15 -4
- package/src/web-components/input/location-filter-component.tsx +2 -6
- package/src/web-components/input/mutation-filter-component.stories.ts +33 -12
- package/src/web-components/input/mutation-filter-component.tsx +60 -4
- package/src/web-components/input/text-input-component.stories.ts +26 -6
- package/src/web-components/input/text-input-component.tsx +34 -3
- package/src/web-components/display/aggregate-component.mdx +0 -25
- package/src/web-components/input/location-filter.mdx +0 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genspectrum/dashboard-components",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "GenSpectrum web components for building dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -35,9 +35,10 @@
|
|
|
35
35
|
"lint:lit-analyzer": "lit-analyzer",
|
|
36
36
|
"generate-manifest": "npx custom-elements-manifest analyze --litelement --globs src/web-components/**",
|
|
37
37
|
"generate-manifest:watch": "npm run generate-manifest -- --watch",
|
|
38
|
-
"format": "prettier \"**/*.{cjs,html,js,json,md,ts,tsx}\" --
|
|
38
|
+
"format": "prettier \"**/*.{cjs,html,js,json,md,ts,tsx}\" --write",
|
|
39
39
|
"check-format": "prettier --check \"**/*.{ts,tsx,json,md,mdx,mjs,cjs}\"",
|
|
40
40
|
"check-types": "tsc --noEmit",
|
|
41
|
+
"check-dependencies": "depcheck",
|
|
41
42
|
"storybook": "storybook dev -p 6006",
|
|
42
43
|
"storybook-preact": "storybook dev --port 6007 --config-dir .storybook-preact",
|
|
43
44
|
"build-storybook": "storybook build",
|
|
@@ -55,8 +56,8 @@
|
|
|
55
56
|
"lit"
|
|
56
57
|
],
|
|
57
58
|
"dependencies": {
|
|
58
|
-
"@floating-ui/dom": "^1.6.3",
|
|
59
59
|
"@lit/context": "^1.1.1",
|
|
60
|
+
"@lit/reactive-element": "^2.0.4",
|
|
60
61
|
"@lit/task": "^1.0.0",
|
|
61
62
|
"chart.js": "^4.4.2",
|
|
62
63
|
"chartjs-chart-error-bars": "^4.4.0",
|
|
@@ -69,17 +70,18 @@
|
|
|
69
70
|
"zod": "^3.23.0"
|
|
70
71
|
},
|
|
71
72
|
"devDependencies": {
|
|
72
|
-
"@custom-elements-manifest/analyzer": "^0.
|
|
73
|
+
"@custom-elements-manifest/analyzer": "^0.10.2",
|
|
73
74
|
"@playwright/test": "^1.43.1",
|
|
74
|
-
"@
|
|
75
|
+
"@storybook/addon-actions": "^8.0.9",
|
|
75
76
|
"@storybook/addon-essentials": "^8.0.9",
|
|
76
77
|
"@storybook/addon-interactions": "^8.0.9",
|
|
77
78
|
"@storybook/addon-links": "^8.0.9",
|
|
78
|
-
"@storybook/blocks": "^8.0.
|
|
79
|
+
"@storybook/blocks": "^8.0.10",
|
|
79
80
|
"@storybook/preact": "^8.0.9",
|
|
80
81
|
"@storybook/preact-vite": "^8.0.9",
|
|
81
82
|
"@storybook/test": "^8.0.0",
|
|
82
|
-
"@storybook/test-runner": "^0.
|
|
83
|
+
"@storybook/test-runner": "^0.18.0",
|
|
84
|
+
"@storybook/types": "^8.0.9",
|
|
83
85
|
"@storybook/web-components": "^8.0.9",
|
|
84
86
|
"@storybook/web-components-vite": "^8.0.9",
|
|
85
87
|
"@types/node": "^20.12.7",
|
|
@@ -87,6 +89,7 @@
|
|
|
87
89
|
"@typescript-eslint/parser": "^7.7.0",
|
|
88
90
|
"autoprefixer": "^10.4.19",
|
|
89
91
|
"daisyui": "^4.10.2",
|
|
92
|
+
"depcheck": "^1.4.7",
|
|
90
93
|
"eslint": "^8.57.0",
|
|
91
94
|
"eslint-config-preact": "^1.3.0",
|
|
92
95
|
"eslint-plugin-import": "^2.29.1",
|
|
@@ -97,6 +100,7 @@
|
|
|
97
100
|
"msw": "^2.2.14",
|
|
98
101
|
"postcss": "^8.4.38",
|
|
99
102
|
"prettier": "^3.2.5",
|
|
103
|
+
"react": "^18.3.1",
|
|
100
104
|
"release-please": "^16.10.2",
|
|
101
105
|
"storybook": "^8.0.9",
|
|
102
106
|
"storybook-addon-fetch-mock": "^2.0.0",
|
|
@@ -10,6 +10,8 @@ const meta: Meta<AggregateProps> = {
|
|
|
10
10
|
component: Aggregate,
|
|
11
11
|
argTypes: {
|
|
12
12
|
fields: [{ control: 'object' }],
|
|
13
|
+
size: [{ control: 'object' }],
|
|
14
|
+
headline: { control: 'text' },
|
|
13
15
|
},
|
|
14
16
|
parameters: {
|
|
15
17
|
fetchMock: {
|
|
@@ -37,11 +39,9 @@ export default meta;
|
|
|
37
39
|
|
|
38
40
|
export const Default: StoryObj<AggregateProps> = {
|
|
39
41
|
render: (args) => (
|
|
40
|
-
<
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
</LapisUrlContext.Provider>
|
|
44
|
-
</div>
|
|
42
|
+
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
43
|
+
<Aggregate {...args} />
|
|
44
|
+
</LapisUrlContext.Provider>
|
|
45
45
|
),
|
|
46
46
|
args: {
|
|
47
47
|
fields: ['division', 'host'],
|
|
@@ -49,5 +49,7 @@ export const Default: StoryObj<AggregateProps> = {
|
|
|
49
49
|
filter: {
|
|
50
50
|
country: 'USA',
|
|
51
51
|
},
|
|
52
|
+
size: { width: '100%', height: '70vh' },
|
|
53
|
+
headline: 'Aggregate',
|
|
52
54
|
},
|
|
53
55
|
};
|
|
@@ -11,6 +11,7 @@ import Headline from '../components/headline';
|
|
|
11
11
|
import Info from '../components/info';
|
|
12
12
|
import { LoadingDisplay } from '../components/loading-display';
|
|
13
13
|
import { NoDataDisplay } from '../components/no-data-display';
|
|
14
|
+
import { ResizeContainer, type Size } from '../components/resize-container';
|
|
14
15
|
import Tabs from '../components/tabs';
|
|
15
16
|
import { useQuery } from '../useQuery';
|
|
16
17
|
|
|
@@ -20,17 +21,23 @@ export interface AggregateProps {
|
|
|
20
21
|
filter: LapisFilter;
|
|
21
22
|
fields: string[];
|
|
22
23
|
views: View[];
|
|
24
|
+
size?: Size;
|
|
25
|
+
headline?: string;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
export const Aggregate: FunctionComponent<AggregateProps> = ({
|
|
28
|
+
export const Aggregate: FunctionComponent<AggregateProps> = ({
|
|
29
|
+
fields,
|
|
30
|
+
views,
|
|
31
|
+
filter,
|
|
32
|
+
size,
|
|
33
|
+
headline = 'Aggregate',
|
|
34
|
+
}) => {
|
|
26
35
|
const lapis = useContext(LapisUrlContext);
|
|
27
36
|
|
|
28
37
|
const { data, error, isLoading } = useQuery(async () => {
|
|
29
38
|
return queryAggregateData(filter, fields, lapis);
|
|
30
39
|
}, [filter, fields, lapis]);
|
|
31
40
|
|
|
32
|
-
const headline = 'Aggregate';
|
|
33
|
-
|
|
34
41
|
if (isLoading) {
|
|
35
42
|
return (
|
|
36
43
|
<Headline heading={headline}>
|
|
@@ -56,9 +63,11 @@ export const Aggregate: FunctionComponent<AggregateProps> = ({ fields, views, fi
|
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
return (
|
|
59
|
-
<
|
|
60
|
-
<
|
|
61
|
-
|
|
66
|
+
<ResizeContainer size={size} defaultSize={{ height: '700px', width: '100%' }}>
|
|
67
|
+
<Headline heading={headline}>
|
|
68
|
+
<AggregatedDataTabs data={data} views={views} fields={fields} />
|
|
69
|
+
</Headline>
|
|
70
|
+
</ResizeContainer>
|
|
62
71
|
);
|
|
63
72
|
};
|
|
64
73
|
|
|
@@ -92,7 +101,7 @@ const Toolbar: FunctionComponent<ToolbarProps> = ({ data }) => {
|
|
|
92
101
|
return (
|
|
93
102
|
<div class='flex flex-row'>
|
|
94
103
|
<CsvDownloadButton className='mx-1 btn btn-xs' getData={() => data} filename='aggregate.csv' />
|
|
95
|
-
<Info
|
|
104
|
+
<Info>Info for aggregate</Info>
|
|
96
105
|
</div>
|
|
97
106
|
);
|
|
98
107
|
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ComponentChildren, type FunctionalComponent } from 'preact';
|
|
2
|
+
import { useContext } from 'preact/hooks';
|
|
3
|
+
|
|
4
|
+
import { type ReferenceGenome } from '../../lapisApi/ReferenceGenome';
|
|
5
|
+
import { ReferenceGenomeContext } from '../ReferenceGenomeContext';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Sometimes the reference genome is not immediately available.
|
|
9
|
+
* This component will display a loading spinner until the reference genome is available.
|
|
10
|
+
* Child components can assume that the reference genome is available on the first render,
|
|
11
|
+
* which e.g. matters for initial values of `useState`.
|
|
12
|
+
*/
|
|
13
|
+
export const ReferenceGenomesAwaiter: FunctionalComponent<{ children: ComponentChildren }> = ({ children }) => {
|
|
14
|
+
const referenceGenome = useContext(ReferenceGenomeContext);
|
|
15
|
+
|
|
16
|
+
if (isNotInitialized(referenceGenome)) {
|
|
17
|
+
return <div className='laoding loading-spinner loading-md'>Loading...</div>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return <>{children}</>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function isNotInitialized(referenceGenome: ReferenceGenome) {
|
|
24
|
+
return referenceGenome.nucleotideSequences.length === 0 && referenceGenome.genes.length === 0;
|
|
25
|
+
}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
2
|
|
|
3
|
+
type ToStringable = {
|
|
4
|
+
toString: () => string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type DataValue = string | number | boolean | null | ToStringable;
|
|
8
|
+
|
|
3
9
|
export interface CsvDownloadButtonProps {
|
|
4
10
|
label?: string;
|
|
5
11
|
filename?: string;
|
|
6
|
-
getData: () => Record<string,
|
|
12
|
+
getData: () => Record<string, DataValue>[];
|
|
7
13
|
className?: string;
|
|
8
14
|
}
|
|
9
15
|
|
|
@@ -32,7 +38,7 @@ export const CsvDownloadButton: FunctionComponent<CsvDownloadButtonProps> = ({
|
|
|
32
38
|
return header + rows;
|
|
33
39
|
};
|
|
34
40
|
|
|
35
|
-
const getDataKeys = (data: Record<string,
|
|
41
|
+
const getDataKeys = (data: Record<string, DataValue>[]) => {
|
|
36
42
|
const keysSet = data
|
|
37
43
|
.map((row) => Object.keys(row))
|
|
38
44
|
.reduce((accumulatedKeys, keys) => {
|
|
@@ -3,10 +3,13 @@ import { expect, within } from '@storybook/test';
|
|
|
3
3
|
|
|
4
4
|
import Headline, { type HeadlineProps } from './headline';
|
|
5
5
|
|
|
6
|
-
const meta: Meta<
|
|
6
|
+
const meta: Meta<HeadlineProps> = {
|
|
7
7
|
title: 'Component/Headline',
|
|
8
8
|
component: Headline,
|
|
9
9
|
parameters: { fetchMock: {} },
|
|
10
|
+
argTypes: {
|
|
11
|
+
heading: { control: 'text' },
|
|
12
|
+
},
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
export default meta;
|
|
@@ -27,3 +30,18 @@ export const HeadlineStory: StoryObj<HeadlineProps> = {
|
|
|
27
30
|
await expect(canvas.getByText('Some Content')).toBeInTheDocument();
|
|
28
31
|
},
|
|
29
32
|
};
|
|
33
|
+
|
|
34
|
+
export const NoHeadlineStory: StoryObj<HeadlineProps> = {
|
|
35
|
+
render: (args) => (
|
|
36
|
+
<Headline {...args}>
|
|
37
|
+
<div class='flex justify-center px-4 py-16 bg-base-200'>Some Content</div>
|
|
38
|
+
</Headline>
|
|
39
|
+
),
|
|
40
|
+
args: {},
|
|
41
|
+
play: async ({ canvasElement }) => {
|
|
42
|
+
const canvas = within(canvasElement);
|
|
43
|
+
|
|
44
|
+
await expect(canvas.queryByText('My Headline')).not.toBeInTheDocument();
|
|
45
|
+
await expect(canvas.getByText('Some Content')).toBeInTheDocument();
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -1,15 +1,35 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
2
3
|
|
|
3
4
|
export interface HeadlineProps {
|
|
4
|
-
heading
|
|
5
|
+
heading?: string;
|
|
5
6
|
}
|
|
6
7
|
|
|
7
8
|
const Headline: FunctionComponent<HeadlineProps> = ({ heading, children }) => {
|
|
9
|
+
if (!heading) {
|
|
10
|
+
return <>{children}</>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return <ResizingHeadline heading={heading}>{children}</ResizingHeadline>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const ResizingHeadline: FunctionComponent<HeadlineProps> = ({ heading, children }) => {
|
|
17
|
+
const ref = useRef<HTMLHeadingElement>(null);
|
|
18
|
+
|
|
19
|
+
const [h1Height, setH1Height] = useState('2rem');
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (ref.current) {
|
|
23
|
+
const h1Height = ref.current.getBoundingClientRect().height;
|
|
24
|
+
setH1Height(`${h1Height}px`);
|
|
25
|
+
}
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
8
28
|
return (
|
|
9
|
-
|
|
10
|
-
<h1>{heading}</h1>
|
|
11
|
-
{children}
|
|
12
|
-
|
|
29
|
+
<div className='h-full w-full'>
|
|
30
|
+
<h1 ref={ref}>{heading}</h1>
|
|
31
|
+
<div style={{ height: `calc(100% - ${h1Height})` }}>{children}</div>
|
|
32
|
+
</div>
|
|
13
33
|
);
|
|
14
34
|
};
|
|
15
35
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
2
|
+
import { expect, fireEvent, waitFor, within } from '@storybook/test';
|
|
2
3
|
|
|
3
4
|
import Info, { type InfoProps } from './info';
|
|
4
5
|
|
|
@@ -7,16 +8,36 @@ const meta: Meta<InfoProps> = {
|
|
|
7
8
|
component: Info,
|
|
8
9
|
parameters: { fetchMock: {} },
|
|
9
10
|
args: {
|
|
10
|
-
|
|
11
|
+
size: { width: '400px', height: '100px' },
|
|
11
12
|
},
|
|
12
13
|
};
|
|
13
14
|
|
|
14
15
|
export default meta;
|
|
15
16
|
|
|
17
|
+
const tooltipText = 'This is a tooltip which shows some information.';
|
|
18
|
+
|
|
16
19
|
export const InfoStory: StoryObj<InfoProps> = {
|
|
17
20
|
render: (args) => (
|
|
18
|
-
<div class='flex justify-center px-4 py-16
|
|
19
|
-
<Info {...args}
|
|
21
|
+
<div class='flex justify-center px-4 py-16'>
|
|
22
|
+
<Info {...args}>{tooltipText}</Info>
|
|
20
23
|
</div>
|
|
21
24
|
),
|
|
22
25
|
};
|
|
26
|
+
|
|
27
|
+
export const ShowsInfoOnClick: StoryObj<InfoProps> = {
|
|
28
|
+
...InfoStory,
|
|
29
|
+
play: async ({ canvasElement }) => {
|
|
30
|
+
const canvas = within(canvasElement);
|
|
31
|
+
const loading = canvas.getByRole('button', { name: '?' });
|
|
32
|
+
|
|
33
|
+
await waitFor(() => expect(loading).toBeInTheDocument());
|
|
34
|
+
|
|
35
|
+
await fireEvent.click(loading);
|
|
36
|
+
|
|
37
|
+
await waitFor(() => expect(canvas.getByText(tooltipText, { exact: false })).toBeInTheDocument());
|
|
38
|
+
|
|
39
|
+
await fireEvent.click(canvas.getByRole('button', { name: 'Close' }));
|
|
40
|
+
|
|
41
|
+
await waitFor(() => expect(canvas.queryByText(tooltipText, { exact: false })).not.toBeInTheDocument());
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -1,16 +1,60 @@
|
|
|
1
1
|
import { type FunctionComponent } from 'preact';
|
|
2
|
+
import { useState } from 'preact/hooks';
|
|
2
3
|
|
|
3
4
|
export interface InfoProps {
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
size?: {
|
|
6
|
+
height?: string;
|
|
7
|
+
width?: string;
|
|
8
|
+
};
|
|
6
9
|
}
|
|
7
10
|
|
|
8
|
-
const Info: FunctionComponent<InfoProps> = ({
|
|
11
|
+
const Info: FunctionComponent<InfoProps> = ({ children, size }) => {
|
|
12
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
13
|
+
|
|
14
|
+
const toggleHelp = () => {
|
|
15
|
+
setShowHelp(!showHelp);
|
|
16
|
+
};
|
|
17
|
+
|
|
9
18
|
return (
|
|
10
|
-
<div
|
|
11
|
-
<button
|
|
19
|
+
<div className='relative'>
|
|
20
|
+
<button className='btn btn-xs' onClick={toggleHelp}>
|
|
21
|
+
?
|
|
22
|
+
</button>
|
|
23
|
+
{showHelp && (
|
|
24
|
+
<div
|
|
25
|
+
className='absolute top-8 right-6 bg-white p-2 border border-black flex flex-col overflow-auto shadow-lg rounded z-50'
|
|
26
|
+
style={size}
|
|
27
|
+
>
|
|
28
|
+
<div className='flex flex-col'>{children}</div>
|
|
29
|
+
<div className='flex justify-end'>
|
|
30
|
+
<button className='text-sm underline mt-2' onClick={toggleHelp}>
|
|
31
|
+
Close
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
)}
|
|
12
36
|
</div>
|
|
13
37
|
);
|
|
14
38
|
};
|
|
15
39
|
|
|
40
|
+
export const InfoHeadline1: FunctionComponent = ({ children }) => {
|
|
41
|
+
return <h1 className='text-lg font-bold'>{children}</h1>;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const InfoHeadline2: FunctionComponent = ({ children }) => {
|
|
45
|
+
return <h2 className='text-base font-bold mt-4'>{children}</h2>;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const InfoParagraph: FunctionComponent = ({ children }) => {
|
|
49
|
+
return <p className='text-justify my-1'>{children}</p>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const InfoLink: FunctionComponent<{ href: string }> = ({ children, href }) => {
|
|
53
|
+
return (
|
|
54
|
+
<a className='text-blue-600 hover:text-blue-800' href={href} target='_blank' rel='noopener noreferrer'>
|
|
55
|
+
{children}
|
|
56
|
+
</a>
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
16
60
|
export default Info;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type FunctionComponent } from 'preact';
|
|
1
|
+
import { type FunctionComponent, type JSX } from 'preact';
|
|
2
2
|
import { useState } from 'preact/hooks';
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
import './min-max-percent-slider.css';
|
|
5
5
|
|
|
6
6
|
export interface MinMaxPercentSliderProps {
|
|
@@ -27,7 +27,7 @@ export const MinMaxRangeSlider: FunctionComponent<MinMaxPercentSliderProps> = ({
|
|
|
27
27
|
|
|
28
28
|
const [zIndexTo, setZIndexTo] = useState(0);
|
|
29
29
|
|
|
30
|
-
const onMinChange = (event:
|
|
30
|
+
const onMinChange = (event: JSX.TargetedInputEvent<HTMLInputElement>) => {
|
|
31
31
|
const input = event.target as HTMLInputElement;
|
|
32
32
|
const minValue = Number(input.value);
|
|
33
33
|
|
|
@@ -39,7 +39,7 @@ export const MinMaxRangeSlider: FunctionComponent<MinMaxPercentSliderProps> = ({
|
|
|
39
39
|
}
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
-
const onMaxChange = (event:
|
|
42
|
+
const onMaxChange = (event: JSX.TargetedInputEvent<HTMLInputElement>) => {
|
|
43
43
|
const input = event.target as HTMLInputElement;
|
|
44
44
|
const maxValue = Number(input.value);
|
|
45
45
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { type FunctionComponent } from 'preact';
|
|
1
|
+
import { type FunctionComponent, type JSX } from 'preact';
|
|
2
2
|
import { useEffect, useState } from 'preact/hooks';
|
|
3
|
-
import { type ChangeEvent } from 'react';
|
|
4
3
|
|
|
5
4
|
export type PercentInputProps = {
|
|
6
5
|
percentage: number;
|
|
@@ -18,7 +17,7 @@ export const PercentInput: FunctionComponent<PercentInputProps> = ({ percentage,
|
|
|
18
17
|
setInternalPercentage(percentage);
|
|
19
18
|
}, [percentage]);
|
|
20
19
|
|
|
21
|
-
const handleInputChange = (event:
|
|
20
|
+
const handleInputChange = (event: JSX.TargetedInputEvent<HTMLInputElement>) => {
|
|
22
21
|
const input = event.target as HTMLInputElement;
|
|
23
22
|
const value = Number(input.value);
|
|
24
23
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type FunctionComponent } from 'preact';
|
|
2
|
+
|
|
3
|
+
export type Size = {
|
|
4
|
+
width?: string;
|
|
5
|
+
height?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export interface ResizeContainerProps {
|
|
9
|
+
size?: Size;
|
|
10
|
+
defaultSize: Size;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const ResizeContainer: FunctionComponent<ResizeContainerProps> = ({ children, size, defaultSize }) => {
|
|
14
|
+
return <div style={extendByDefault(size, defaultSize)}>{children}</div>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const extendByDefault = (size: Size | undefined, defaultSize: Size) => {
|
|
18
|
+
if (size === undefined) {
|
|
19
|
+
return defaultSize;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { ...defaultSize, ...size };
|
|
23
|
+
};
|
|
@@ -3,6 +3,7 @@ import { type OneDArray, type TColumn, type TData } from 'gridjs/dist/src/types'
|
|
|
3
3
|
import { type PaginationConfig } from 'gridjs/dist/src/view/plugin/pagination';
|
|
4
4
|
import { type ComponentChild } from 'preact';
|
|
5
5
|
import { useEffect, useRef } from 'preact/hooks';
|
|
6
|
+
|
|
6
7
|
import 'gridjs/dist/theme/mermaid.css';
|
|
7
8
|
|
|
8
9
|
export const tableStyle = {
|
|
@@ -51,10 +51,10 @@ export const TabsWithToolbarOnlyShowingOnSecondTab: StoryObj = {
|
|
|
51
51
|
play: async ({ canvasElement }) => {
|
|
52
52
|
const canvas = within(canvasElement);
|
|
53
53
|
|
|
54
|
-
await waitFor(() => expect(canvas.
|
|
54
|
+
await waitFor(() => expect(canvas.getByRole('button', { name: 'SecondTab' })).toBeVisible());
|
|
55
55
|
await expect(canvas.queryByText('Toolbar')).not.toBeInTheDocument();
|
|
56
56
|
|
|
57
|
-
await fireEvent.click(canvas.
|
|
57
|
+
await fireEvent.click(canvas.getByRole('button', { name: 'SecondTab' }));
|
|
58
58
|
await waitFor(() => expect(canvas.getByText('Toolbar')).toBeVisible());
|
|
59
59
|
},
|
|
60
60
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment, type FunctionComponent } from 'preact';
|
|
2
|
-
import { useState } from 'preact/hooks';
|
|
2
|
+
import { useEffect, useRef, useState } from 'preact/hooks';
|
|
3
3
|
import { type JSXInternal } from 'preact/src/jsx';
|
|
4
4
|
|
|
5
5
|
type Tab = {
|
|
@@ -14,34 +14,57 @@ interface ComponentTabsProps {
|
|
|
14
14
|
|
|
15
15
|
const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
|
|
16
16
|
const [activeTab, setActiveTab] = useState(tabs[0].title);
|
|
17
|
+
const [heightOfTabs, setHeightOfTabs] = useState('3rem');
|
|
18
|
+
const tabRef = useRef<HTMLDivElement>(null);
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (tabRef.current) {
|
|
22
|
+
const heightOfTabs = tabRef.current.getBoundingClientRect().height;
|
|
23
|
+
setHeightOfTabs(`${heightOfTabs}px`);
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
const tabElements = (
|
|
28
|
+
<div className='flex flex-row'>
|
|
29
|
+
{tabs.map((tab) => {
|
|
30
|
+
return (
|
|
31
|
+
<Fragment key={tab.title}>
|
|
32
|
+
<button
|
|
33
|
+
className={`px-4 py-2 text-sm font-medium leading-5 transition-colors duration-150 ${
|
|
34
|
+
activeTab === tab.title
|
|
35
|
+
? 'border-b-2 border-gray-400'
|
|
36
|
+
: 'text-gray-600 hover:bg-gray-100 hover:text-gray-700'
|
|
37
|
+
}`}
|
|
38
|
+
onClick={() => {
|
|
39
|
+
setActiveTab(tab.title);
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
{tab.title}
|
|
43
|
+
</button>
|
|
44
|
+
</Fragment>
|
|
45
|
+
);
|
|
46
|
+
})}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
38
49
|
|
|
39
50
|
const toolbarElement = typeof toolbar === 'function' ? toolbar(activeTab) : toolbar;
|
|
40
51
|
|
|
41
52
|
return (
|
|
42
|
-
<div
|
|
43
|
-
{
|
|
44
|
-
|
|
53
|
+
<div className='h-full w-full'>
|
|
54
|
+
<div ref={tabRef} className='flex flex-row justify-between'>
|
|
55
|
+
{tabElements}
|
|
56
|
+
{toolbar && <div className='py-2'>{toolbarElement}</div>}
|
|
57
|
+
</div>
|
|
58
|
+
<div
|
|
59
|
+
className={`p-2 border-2 border-gray-100 rounded-b-md rounded-tr-md ${activeTab === tabs[0].title ? '' : 'rounded-tl-md'}`}
|
|
60
|
+
style={{ height: `calc(100% - ${heightOfTabs})` }}
|
|
61
|
+
>
|
|
62
|
+
{tabs.map((tab) => (
|
|
63
|
+
<div className='h-full overflow-auto' key={tab.title} hidden={activeTab !== tab.title}>
|
|
64
|
+
{tab.content}
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
45
68
|
</div>
|
|
46
69
|
);
|
|
47
70
|
};
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { withActions } from '@storybook/addon-actions/decorator';
|
|
2
2
|
import { type Meta, type StoryObj } from '@storybook/preact';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
DateRangeSelector,
|
|
6
|
+
type DateRangeSelectorProps,
|
|
7
|
+
PRESET_VALUE_ALL_TIMES,
|
|
8
|
+
PRESET_VALUE_CUSTOM,
|
|
9
|
+
PRESET_VALUE_LAST_2_MONTHS,
|
|
10
|
+
PRESET_VALUE_LAST_2_WEEKS,
|
|
11
|
+
PRESET_VALUE_LAST_3_MONTHS,
|
|
12
|
+
PRESET_VALUE_LAST_6_MONTHS,
|
|
13
|
+
PRESET_VALUE_LAST_MONTH,
|
|
14
|
+
} from './date-range-selector';
|
|
5
15
|
import { LAPIS_URL } from '../../constants';
|
|
6
16
|
import { LapisUrlContext } from '../LapisUrlContext';
|
|
7
17
|
|
|
8
|
-
const meta: Meta<DateRangeSelectorProps
|
|
18
|
+
const meta: Meta<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
9
19
|
title: 'Input/DateRangeSelector',
|
|
10
20
|
component: DateRangeSelector,
|
|
11
21
|
parameters: {
|
|
@@ -14,19 +24,41 @@ const meta: Meta<DateRangeSelectorProps> = {
|
|
|
14
24
|
},
|
|
15
25
|
fetchMock: {},
|
|
16
26
|
},
|
|
27
|
+
argTypes: {
|
|
28
|
+
initialValue: {
|
|
29
|
+
control: {
|
|
30
|
+
type: 'select',
|
|
31
|
+
},
|
|
32
|
+
options: [
|
|
33
|
+
PRESET_VALUE_CUSTOM,
|
|
34
|
+
PRESET_VALUE_ALL_TIMES,
|
|
35
|
+
PRESET_VALUE_LAST_2_WEEKS,
|
|
36
|
+
PRESET_VALUE_LAST_MONTH,
|
|
37
|
+
PRESET_VALUE_LAST_2_MONTHS,
|
|
38
|
+
PRESET_VALUE_LAST_3_MONTHS,
|
|
39
|
+
PRESET_VALUE_LAST_6_MONTHS,
|
|
40
|
+
'CustomDateRange',
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
},
|
|
17
44
|
args: {
|
|
18
45
|
customSelectOptions: [{ label: 'CustomDateRange', dateFrom: '2021-01-01', dateTo: '2021-12-31' }],
|
|
19
46
|
earliestDate: '1970-01-01',
|
|
47
|
+
initialValue: PRESET_VALUE_LAST_3_MONTHS,
|
|
20
48
|
},
|
|
21
49
|
decorators: [withActions],
|
|
22
50
|
};
|
|
23
51
|
|
|
24
52
|
export default meta;
|
|
25
53
|
|
|
26
|
-
export const Primary: StoryObj<DateRangeSelectorProps
|
|
54
|
+
export const Primary: StoryObj<DateRangeSelectorProps<'CustomDateRange'>> = {
|
|
27
55
|
render: (args) => (
|
|
28
56
|
<LapisUrlContext.Provider value={LAPIS_URL}>
|
|
29
|
-
<DateRangeSelector
|
|
57
|
+
<DateRangeSelector
|
|
58
|
+
customSelectOptions={args.customSelectOptions}
|
|
59
|
+
earliestDate={args.earliestDate}
|
|
60
|
+
initialValue={args.initialValue}
|
|
61
|
+
/>
|
|
30
62
|
</LapisUrlContext.Provider>
|
|
31
63
|
),
|
|
32
64
|
};
|