@genspectrum/dashboard-components 0.8.4 → 0.9.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.
- package/README.md +14 -2
- package/custom-elements.json +30 -7
- package/dist/assets/{mutationOverTimeWorker-kjUXkRmn.js.map → mutationOverTimeWorker-DuWGESoO.js.map} +1 -1
- package/dist/{genspectrum-components.d.ts → components.d.ts} +25 -59
- package/dist/{dashboard-components.js → components.js} +43 -66
- package/dist/components.js.map +1 -0
- package/dist/util.d.ts +301 -0
- package/dist/util.js +6 -0
- package/dist/util.js.map +1 -0
- package/dist/utilEntrypoint-g4DsyhU7.js +61 -0
- package/dist/utilEntrypoint-g4DsyhU7.js.map +1 -0
- package/package.json +10 -5
- package/src/{index.ts → componentsEntrypoint.ts} +0 -1
- package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +101 -44
- package/src/preact/dateRangeSelector/date-range-selector.tsx +33 -14
- package/src/preact/dateRangeSelector/dateConversion.ts +1 -5
- package/src/preact/dateRangeSelector/dateRangeOption.ts +18 -0
- package/src/preact/webWorkers/useWebWorker.ts +32 -10
- package/src/preact/webWorkers/workerFunction.ts +19 -3
- package/src/standaloneEntrypoint.ts +2 -0
- package/src/utilEntrypoint.ts +6 -0
- package/src/web-components/input/gs-date-range-selector.stories.ts +41 -10
- package/src/web-components/input/gs-date-range-selector.tsx +16 -2
- package/standalone-bundle/assets/mutationOverTimeWorker-MVSt1FVw.js.map +1 -0
- package/standalone-bundle/dashboard-components.js +12986 -16649
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -0
- package/dist/dashboard-components.js.map +0 -1
|
@@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from 'preact/hooks';
|
|
|
4
4
|
|
|
5
5
|
import { computeInitialValues } from './computeInitialValues';
|
|
6
6
|
import { toYYYYMMDD } from './dateConversion';
|
|
7
|
-
import { type DateRangeOption } from './dateRangeOption';
|
|
7
|
+
import { type DateRangeOption, DateRangeOptionChangedEvent, type DateRangeSelectOption } from './dateRangeOption';
|
|
8
8
|
import { getDatesForSelectorValue, getSelectableOptions } from './selectableOptions';
|
|
9
9
|
import { ErrorBoundary } from '../components/error-boundary';
|
|
10
10
|
import { Select } from '../components/select';
|
|
@@ -115,7 +115,8 @@ export const DateRangeSelectorInner = ({
|
|
|
115
115
|
dateTo: dateRange.dateTo,
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
|
|
118
|
+
fireFilterChangedEvent();
|
|
119
|
+
fireOptionChangedEvent(value);
|
|
119
120
|
};
|
|
120
121
|
|
|
121
122
|
const onChangeDateFrom = () => {
|
|
@@ -123,11 +124,18 @@ export const DateRangeSelectorInner = ({
|
|
|
123
124
|
return;
|
|
124
125
|
}
|
|
125
126
|
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
const dateTo = dateToPicker?.selectedDates[0];
|
|
128
|
+
const dateFrom = dateFromPicker?.selectedDates[0];
|
|
129
|
+
|
|
130
|
+
selectedDates.dateFrom = dateFrom || new Date();
|
|
131
|
+
dateToPicker?.set('minDate', dateFrom);
|
|
128
132
|
setSelectedDateRange(customOption);
|
|
129
133
|
|
|
130
|
-
|
|
134
|
+
fireFilterChangedEvent();
|
|
135
|
+
fireOptionChangedEvent({
|
|
136
|
+
dateFrom: dateFrom !== undefined ? toYYYYMMDD(dateFrom) : earliestDate,
|
|
137
|
+
dateTo: toYYYYMMDD(dateTo || new Date())!,
|
|
138
|
+
});
|
|
131
139
|
};
|
|
132
140
|
|
|
133
141
|
const onChangeDateTo = () => {
|
|
@@ -135,24 +143,35 @@ export const DateRangeSelectorInner = ({
|
|
|
135
143
|
return;
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
|
|
139
|
-
dateFromPicker?.
|
|
146
|
+
const dateTo = dateToPicker?.selectedDates[0];
|
|
147
|
+
const dateFrom = dateFromPicker?.selectedDates[0];
|
|
148
|
+
|
|
149
|
+
selectedDates.dateTo = dateTo || new Date();
|
|
150
|
+
dateFromPicker?.set('maxDate', dateTo);
|
|
140
151
|
setSelectedDateRange(customOption);
|
|
141
152
|
|
|
142
|
-
|
|
153
|
+
fireFilterChangedEvent();
|
|
154
|
+
fireOptionChangedEvent({
|
|
155
|
+
dateFrom: dateFrom !== undefined ? toYYYYMMDD(dateFrom) : earliestDate,
|
|
156
|
+
dateTo: toYYYYMMDD(dateTo || new Date())!,
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const fireOptionChangedEvent = (option: DateRangeSelectOption) => {
|
|
161
|
+
divRef.current?.dispatchEvent(new DateRangeOptionChangedEvent(option));
|
|
143
162
|
};
|
|
144
163
|
|
|
145
|
-
const
|
|
146
|
-
const dateFrom =
|
|
147
|
-
const dateTo =
|
|
164
|
+
const fireFilterChangedEvent = () => {
|
|
165
|
+
const dateFrom = dateFromPicker?.selectedDates[0];
|
|
166
|
+
const dateTo = dateToPicker?.selectedDates[0];
|
|
148
167
|
|
|
149
168
|
const detail = {
|
|
150
|
-
...(dateFrom !== undefined && { [`${dateColumn}From`]: dateFrom }),
|
|
151
|
-
...(dateTo !== undefined && { [`${dateColumn}To`]: dateTo }),
|
|
169
|
+
...(dateFrom !== undefined && { [`${dateColumn}From`]: toYYYYMMDD(dateFrom) }),
|
|
170
|
+
...(dateTo !== undefined && { [`${dateColumn}To`]: toYYYYMMDD(dateTo) }),
|
|
152
171
|
};
|
|
153
172
|
|
|
154
173
|
divRef.current?.dispatchEvent(
|
|
155
|
-
new CustomEvent('gs-date-range-changed', {
|
|
174
|
+
new CustomEvent('gs-date-range-filter-changed', {
|
|
156
175
|
detail,
|
|
157
176
|
bubbles: true,
|
|
158
177
|
composed: true,
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
export const toYYYYMMDD = (date
|
|
2
|
-
if (!date) {
|
|
3
|
-
return undefined;
|
|
4
|
-
}
|
|
5
|
-
|
|
1
|
+
export const toYYYYMMDD = (date: Date) => {
|
|
6
2
|
const options: Intl.DateTimeFormatOptions = { year: 'numeric', month: '2-digit', day: '2-digit' };
|
|
7
3
|
return date.toLocaleDateString('en-CA', options);
|
|
8
4
|
};
|
|
@@ -18,6 +18,17 @@ export type DateRangeOption = {
|
|
|
18
18
|
dateTo?: string;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
+
export type DateRangeSelectOption = string | { dateFrom: string; dateTo: string };
|
|
22
|
+
export class DateRangeOptionChangedEvent extends CustomEvent<DateRangeSelectOption> {
|
|
23
|
+
constructor(detail: DateRangeSelectOption) {
|
|
24
|
+
super('gs-date-range-option-changed', {
|
|
25
|
+
detail,
|
|
26
|
+
bubbles: true,
|
|
27
|
+
composed: true,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
const today = new Date();
|
|
22
33
|
|
|
23
34
|
const twoWeeksAgo = new Date();
|
|
@@ -35,6 +46,9 @@ last3Months.setMonth(today.getMonth() - 3);
|
|
|
35
46
|
const last6Months = new Date(today);
|
|
36
47
|
last6Months.setMonth(today.getMonth() - 6);
|
|
37
48
|
|
|
49
|
+
const lastYear = new Date(today);
|
|
50
|
+
lastYear.setFullYear(today.getFullYear() - 1);
|
|
51
|
+
|
|
38
52
|
/**
|
|
39
53
|
* Presets for the `gs-date-range-selector` component that can be used as `dateRangeOptions`.
|
|
40
54
|
*/
|
|
@@ -59,6 +73,10 @@ export const dateRangeOptionPresets = {
|
|
|
59
73
|
label: 'Last 6 months',
|
|
60
74
|
dateFrom: toYYYYMMDD(last6Months),
|
|
61
75
|
},
|
|
76
|
+
lastYear: {
|
|
77
|
+
label: 'Last year',
|
|
78
|
+
dateFrom: toYYYYMMDD(lastYear),
|
|
79
|
+
},
|
|
62
80
|
allTimes: {
|
|
63
81
|
label: 'All times',
|
|
64
82
|
},
|
|
@@ -1,31 +1,53 @@
|
|
|
1
1
|
import { useEffect, useState } from 'preact/hooks';
|
|
2
2
|
|
|
3
|
+
import { UserFacingError } from '../components/error-display';
|
|
4
|
+
|
|
5
|
+
export type LoadingWorkerStatus = {
|
|
6
|
+
status: 'loading';
|
|
7
|
+
};
|
|
8
|
+
export type SuccessWorkerStatus<Response> = {
|
|
9
|
+
status: 'success';
|
|
10
|
+
data: Response;
|
|
11
|
+
};
|
|
12
|
+
export type ErrorWorkerStatus =
|
|
13
|
+
| {
|
|
14
|
+
status: 'error';
|
|
15
|
+
userFacing: false;
|
|
16
|
+
error: Error;
|
|
17
|
+
}
|
|
18
|
+
| {
|
|
19
|
+
status: 'error';
|
|
20
|
+
userFacing: true;
|
|
21
|
+
headline: string;
|
|
22
|
+
error: Error;
|
|
23
|
+
};
|
|
24
|
+
export type WorkerStatus<Response> = LoadingWorkerStatus | SuccessWorkerStatus<Response> | ErrorWorkerStatus;
|
|
25
|
+
|
|
3
26
|
export function useWebWorker<Request, Response>(messageToWorker: Request, worker: Worker) {
|
|
4
27
|
const [data, setData] = useState<Response | undefined>(undefined);
|
|
5
28
|
const [error, setError] = useState<Error | undefined>(undefined);
|
|
6
29
|
const [isLoading, setIsLoading] = useState(true);
|
|
7
30
|
|
|
8
31
|
useEffect(() => {
|
|
9
|
-
worker.onmessage = (
|
|
10
|
-
event
|
|
11
|
-
|
|
12
|
-
data?: Response;
|
|
13
|
-
error?: Error;
|
|
14
|
-
}>,
|
|
15
|
-
) => {
|
|
16
|
-
const { status, data, error } = event.data;
|
|
32
|
+
worker.onmessage = (event: MessageEvent<WorkerStatus<Response>>) => {
|
|
33
|
+
const eventData = event.data;
|
|
34
|
+
const status = eventData.status;
|
|
17
35
|
|
|
18
36
|
switch (status) {
|
|
19
37
|
case 'loading':
|
|
20
38
|
setIsLoading(true);
|
|
21
39
|
break;
|
|
22
40
|
case 'success':
|
|
23
|
-
setData(data);
|
|
41
|
+
setData(eventData.data);
|
|
24
42
|
setError(undefined);
|
|
25
43
|
setIsLoading(false);
|
|
26
44
|
break;
|
|
27
45
|
case 'error':
|
|
28
|
-
setError(
|
|
46
|
+
setError(
|
|
47
|
+
eventData.userFacing
|
|
48
|
+
? new UserFacingError(eventData.headline, eventData.error.message)
|
|
49
|
+
: eventData.error,
|
|
50
|
+
);
|
|
29
51
|
setIsLoading(false);
|
|
30
52
|
break;
|
|
31
53
|
default:
|
|
@@ -1,14 +1,30 @@
|
|
|
1
|
+
import { type ErrorWorkerStatus, type LoadingWorkerStatus, type SuccessWorkerStatus } from './useWebWorker';
|
|
2
|
+
import { UserFacingError } from '../components/error-display';
|
|
3
|
+
|
|
1
4
|
export async function workerFunction<R>(queryFunction: () => R) {
|
|
2
5
|
try {
|
|
3
|
-
postMessage({ status: 'loading' });
|
|
6
|
+
postMessage({ status: 'loading' } satisfies LoadingWorkerStatus);
|
|
4
7
|
|
|
5
8
|
const workerResponse = await queryFunction();
|
|
6
9
|
|
|
7
10
|
postMessage({
|
|
8
11
|
status: 'success',
|
|
9
12
|
data: workerResponse,
|
|
10
|
-
});
|
|
13
|
+
} satisfies SuccessWorkerStatus<R>);
|
|
11
14
|
} catch (error) {
|
|
12
|
-
postMessage(
|
|
15
|
+
postMessage(
|
|
16
|
+
(error instanceof UserFacingError
|
|
17
|
+
? {
|
|
18
|
+
status: 'error',
|
|
19
|
+
userFacing: true,
|
|
20
|
+
headline: error.headline,
|
|
21
|
+
error,
|
|
22
|
+
}
|
|
23
|
+
: {
|
|
24
|
+
status: 'error',
|
|
25
|
+
userFacing: false,
|
|
26
|
+
error: error instanceof Error ? error : new Error(`${error}`),
|
|
27
|
+
}) satisfies ErrorWorkerStatus,
|
|
28
|
+
);
|
|
13
29
|
}
|
|
14
30
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { expect, waitFor } from '@storybook/test';
|
|
1
|
+
import { expect, fn, userEvent, waitFor, type within } from '@storybook/test';
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/web-components';
|
|
3
3
|
import { html } from 'lit';
|
|
4
4
|
|
|
@@ -23,12 +23,14 @@ const codeExample = String.raw`
|
|
|
23
23
|
dateColumn="myDateColumn"
|
|
24
24
|
></gs-date-range-selector>`;
|
|
25
25
|
|
|
26
|
+
const customDateRange = { label: 'CustomDateRange', dateFrom: '2021-01-01', dateTo: '2021-12-31' };
|
|
27
|
+
|
|
26
28
|
const meta: Meta<Required<DateRangeSelectorProps>> = {
|
|
27
29
|
title: 'Input/DateRangeSelector',
|
|
28
30
|
component: 'gs-date-range-selector',
|
|
29
31
|
parameters: withComponentDocs({
|
|
30
32
|
actions: {
|
|
31
|
-
handles: ['gs-date-range-changed', ...previewHandles],
|
|
33
|
+
handles: ['gs-date-range-filter-changed', 'gs-date-range-option-changed', ...previewHandles],
|
|
32
34
|
},
|
|
33
35
|
fetchMock: {},
|
|
34
36
|
componentDocs: {
|
|
@@ -66,7 +68,7 @@ const meta: Meta<Required<DateRangeSelectorProps>> = {
|
|
|
66
68
|
dateRangeOptionPresets.lastMonth,
|
|
67
69
|
dateRangeOptionPresets.last3Months,
|
|
68
70
|
dateRangeOptionPresets.allTimes,
|
|
69
|
-
|
|
71
|
+
customDateRange,
|
|
70
72
|
],
|
|
71
73
|
earliestDate: '1970-01-01',
|
|
72
74
|
initialValue: dateRangeOptionPresets.lastMonth.label,
|
|
@@ -80,7 +82,7 @@ const meta: Meta<Required<DateRangeSelectorProps>> = {
|
|
|
80
82
|
|
|
81
83
|
export default meta;
|
|
82
84
|
|
|
83
|
-
export const
|
|
85
|
+
export const Default: StoryObj<Required<DateRangeSelectorProps>> = {
|
|
84
86
|
render: (args) =>
|
|
85
87
|
html` <gs-app lapis="${LAPIS_URL}">
|
|
86
88
|
<div class="max-w-screen-lg">
|
|
@@ -95,19 +97,48 @@ export const DateRangeSelectorStory: StoryObj<Required<DateRangeSelectorProps>>
|
|
|
95
97
|
></gs-date-range-selector>
|
|
96
98
|
</div>
|
|
97
99
|
</gs-app>`,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const FiresEvents: StoryObj<Required<DateRangeSelectorProps>> = {
|
|
103
|
+
...Default,
|
|
98
104
|
play: async ({ canvasElement, step }) => {
|
|
99
105
|
const canvas = await withinShadowRoot(canvasElement, 'gs-date-range-selector');
|
|
100
|
-
|
|
106
|
+
|
|
107
|
+
const filterChangedListenerMock = fn();
|
|
108
|
+
const optionChangedListenerMock = fn();
|
|
109
|
+
await step('Setup event listener mock', async () => {
|
|
110
|
+
canvasElement.addEventListener('gs-date-range-filter-changed', filterChangedListenerMock);
|
|
111
|
+
canvasElement.addEventListener('gs-date-range-option-changed', optionChangedListenerMock);
|
|
112
|
+
});
|
|
101
113
|
|
|
102
114
|
await step('Expect last 6 months to be selected', async () => {
|
|
103
|
-
await expect(canvas
|
|
115
|
+
await expect(selectField(canvas)).toHaveValue('Last month');
|
|
104
116
|
await waitFor(() => {
|
|
105
|
-
expect(
|
|
117
|
+
expect(dateToPicker(canvas)).toHaveValue(toYYYYMMDD(new Date()));
|
|
106
118
|
});
|
|
107
119
|
});
|
|
108
120
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
await step('Expect event to be fired when selecting a different value', async () => {
|
|
122
|
+
await userEvent.selectOptions(selectField(canvas), 'CustomDateRange');
|
|
123
|
+
await expect(dateToPicker(canvas)).toHaveValue(customDateRange.dateTo);
|
|
124
|
+
|
|
125
|
+
await expect(filterChangedListenerMock).toHaveBeenCalledWith(
|
|
126
|
+
expect.objectContaining({
|
|
127
|
+
detail: {
|
|
128
|
+
aDateColumnFrom: customDateRange.dateFrom,
|
|
129
|
+
aDateColumnTo: customDateRange.dateTo,
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
await expect(optionChangedListenerMock).toHaveBeenCalledWith(
|
|
135
|
+
expect.objectContaining({
|
|
136
|
+
detail: customDateRange.label,
|
|
137
|
+
}),
|
|
138
|
+
);
|
|
139
|
+
});
|
|
112
140
|
},
|
|
113
141
|
};
|
|
142
|
+
|
|
143
|
+
const dateToPicker = (canvas: ReturnType<typeof within>) => canvas.getByPlaceholderText('Date to');
|
|
144
|
+
const selectField = (canvas: ReturnType<typeof within>) => canvas.getByRole('combobox');
|
|
@@ -2,6 +2,7 @@ import { customElement, property } from 'lit/decorators.js';
|
|
|
2
2
|
import { type DetailedHTMLProps, type HTMLAttributes } from 'react';
|
|
3
3
|
|
|
4
4
|
import { DateRangeSelector, type DateRangeSelectorProps } from '../../preact/dateRangeSelector/date-range-selector';
|
|
5
|
+
import { type DateRangeOptionChangedEvent } from '../../preact/dateRangeSelector/dateRangeOption';
|
|
5
6
|
import { type Equals, type Expect } from '../../utils/typeAssertions';
|
|
6
7
|
import { PreactLitAdapter } from '../PreactLitAdapter';
|
|
7
8
|
|
|
@@ -18,7 +19,7 @@ import { PreactLitAdapter } from '../PreactLitAdapter';
|
|
|
18
19
|
* Setting a value in either of the date pickers will set the select field to "custom",
|
|
19
20
|
* which represents an arbitrary date range.
|
|
20
21
|
*
|
|
21
|
-
* @fires {CustomEvent<{ `${dateColumn}From`: string; `${dateColumn}To`: string; }>} gs-date-range-changed
|
|
22
|
+
* @fires {CustomEvent<{ `${dateColumn}From`: string; `${dateColumn}To`: string; }>} gs-date-range-filter-changed
|
|
22
23
|
* Fired when:
|
|
23
24
|
* - The select field is changed,
|
|
24
25
|
* - A date is selected in either of the date pickers,
|
|
@@ -33,6 +34,18 @@ import { PreactLitAdapter } from '../PreactLitAdapter';
|
|
|
33
34
|
* }
|
|
34
35
|
* ```
|
|
35
36
|
* will be fired.
|
|
37
|
+
*
|
|
38
|
+
* Use this event, when you want to use the filter directly as a LAPIS filter.
|
|
39
|
+
*
|
|
40
|
+
*
|
|
41
|
+
* @fires {CustomEvent<{ string | {dateFrom: string, dateTo: string}}>} gs-date-range-option-changed
|
|
42
|
+
* Fired when:
|
|
43
|
+
* - The select field is changed,
|
|
44
|
+
* - A date is selected in either of the date pickers,
|
|
45
|
+
* - A date was typed into either of the date input fields, and the input field loses focus ("on blur").
|
|
46
|
+
* Contains the selected dateRangeOption or when users select custom values it contains the selected dates.
|
|
47
|
+
*
|
|
48
|
+
* Use this event, when you want to control this component in your JS application.
|
|
36
49
|
*/
|
|
37
50
|
@customElement('gs-date-range-selector')
|
|
38
51
|
export class DateRangeSelectorComponent extends PreactLitAdapter {
|
|
@@ -116,7 +129,8 @@ declare global {
|
|
|
116
129
|
}
|
|
117
130
|
|
|
118
131
|
interface HTMLElementEventMap {
|
|
119
|
-
'gs-date-range-changed': CustomEvent<Record<string, string>>;
|
|
132
|
+
'gs-date-range-filter-changed': CustomEvent<Record<string, string>>;
|
|
133
|
+
'gs-date-range-option-changed': DateRangeOptionChangedEvent;
|
|
120
134
|
}
|
|
121
135
|
}
|
|
122
136
|
|