@genspectrum/dashboard-components 0.1.1

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 (186) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +109 -0
  3. package/custom-elements.json +1587 -0
  4. package/dist/dashboard-components.js +7322 -0
  5. package/dist/dashboard-components.js.map +1 -0
  6. package/dist/genspectrum-components.d.ts +298 -0
  7. package/dist/style.css +2930 -0
  8. package/package.json +109 -0
  9. package/src/constants.ts +6 -0
  10. package/src/index.ts +1 -0
  11. package/src/lapisApi/ReferenceGenome.ts +30 -0
  12. package/src/lapisApi/__mockData__/referenceGenome.json +58 -0
  13. package/src/lapisApi/lapisApi.ts +99 -0
  14. package/src/lapisApi/lapisTypes.ts +51 -0
  15. package/src/operator/Dataset.ts +3 -0
  16. package/src/operator/DivisionOperator.spec.ts +27 -0
  17. package/src/operator/DivisionOperator.ts +60 -0
  18. package/src/operator/FetchAggregatedOperator.ts +44 -0
  19. package/src/operator/FetchInsertionsOperator.ts +24 -0
  20. package/src/operator/FetchSubstitutionsOrDeletionsOperator.ts +49 -0
  21. package/src/operator/FillMissingOperator.spec.ts +26 -0
  22. package/src/operator/FillMissingOperator.ts +30 -0
  23. package/src/operator/GroupByAndSumOperator.spec.ts +26 -0
  24. package/src/operator/GroupByAndSumOperator.ts +26 -0
  25. package/src/operator/GroupByOperator.spec.ts +43 -0
  26. package/src/operator/GroupByOperator.ts +32 -0
  27. package/src/operator/MapOperator.spec.ts +13 -0
  28. package/src/operator/MapOperator.ts +16 -0
  29. package/src/operator/MockOperator.spec.ts +11 -0
  30. package/src/operator/MockOperator.ts +12 -0
  31. package/src/operator/Operator.ts +5 -0
  32. package/src/operator/SlidingOperator.spec.ts +52 -0
  33. package/src/operator/SlidingOperator.ts +23 -0
  34. package/src/operator/SortOperator.spec.ts +13 -0
  35. package/src/operator/SortOperator.ts +16 -0
  36. package/src/preact/LapisUrlContext.ts +3 -0
  37. package/src/preact/ReferenceGenomeContext.ts +5 -0
  38. package/src/preact/components/SegmentSelector.tsx +62 -0
  39. package/src/preact/components/chart.stories.tsx +42 -0
  40. package/src/preact/components/chart.tsx +32 -0
  41. package/src/preact/components/checkbox-selector.stories.tsx +56 -0
  42. package/src/preact/components/checkbox-selector.tsx +46 -0
  43. package/src/preact/components/confidence-interval-selector.tsx +45 -0
  44. package/src/preact/components/csv-download-button.stories.tsx +25 -0
  45. package/src/preact/components/csv-download-button.tsx +51 -0
  46. package/src/preact/components/error-display.stories.tsx +22 -0
  47. package/src/preact/components/error-display.tsx +5 -0
  48. package/src/preact/components/headline.stories.tsx +29 -0
  49. package/src/preact/components/headline.tsx +16 -0
  50. package/src/preact/components/info.stories.tsx +22 -0
  51. package/src/preact/components/info.tsx +16 -0
  52. package/src/preact/components/loading-display.stories.tsx +20 -0
  53. package/src/preact/components/loading-display.tsx +5 -0
  54. package/src/preact/components/min-max-percent-slider.css +40 -0
  55. package/src/preact/components/min-max-range-slider.tsx +95 -0
  56. package/src/preact/components/mutation-type-selector.tsx +30 -0
  57. package/src/preact/components/no-data-display.stories.tsx +20 -0
  58. package/src/preact/components/no-data-display.tsx +5 -0
  59. package/src/preact/components/percent-intput.tsx +49 -0
  60. package/src/preact/components/proportion-selector-dropdown.stories.tsx +66 -0
  61. package/src/preact/components/proportion-selector-dropdown.tsx +33 -0
  62. package/src/preact/components/proportion-selector.stories.tsx +81 -0
  63. package/src/preact/components/proportion-selector.tsx +43 -0
  64. package/src/preact/components/scaling-selector.stories.tsx +25 -0
  65. package/src/preact/components/scaling-selector.tsx +36 -0
  66. package/src/preact/components/select.stories.tsx +42 -0
  67. package/src/preact/components/select.tsx +21 -0
  68. package/src/preact/components/table.stories.tsx +24 -0
  69. package/src/preact/components/table.tsx +51 -0
  70. package/src/preact/components/tabs.stories.tsx +60 -0
  71. package/src/preact/components/tabs.tsx +49 -0
  72. package/src/preact/dateRangeSelector/date-range-selector.stories.tsx +32 -0
  73. package/src/preact/dateRangeSelector/date-range-selector.tsx +228 -0
  74. package/src/preact/dateRangeSelector/dateConversion.ts +8 -0
  75. package/src/preact/locationFilter/__mockData__/aggregated.json +775 -0
  76. package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +36 -0
  77. package/src/preact/locationFilter/fetchAutocompletionList.ts +43 -0
  78. package/src/preact/locationFilter/location-filter.stories.tsx +50 -0
  79. package/src/preact/locationFilter/location-filter.tsx +112 -0
  80. package/src/preact/mutationComparison/__mockData__/nucleotideMutationsOtherVariant.json +295 -0
  81. package/src/preact/mutationComparison/__mockData__/nucleotideMutationsSomeVariant.json +304 -0
  82. package/src/preact/mutationComparison/fetchMutationData.spec.ts +118 -0
  83. package/src/preact/mutationComparison/getMutationComparisonTableData.spec.ts +125 -0
  84. package/src/preact/mutationComparison/getMutationComparisonTableData.ts +40 -0
  85. package/src/preact/mutationComparison/mutation-comparison-table.tsx +43 -0
  86. package/src/preact/mutationComparison/mutation-comparison-venn.tsx +122 -0
  87. package/src/preact/mutationComparison/mutation-comparison.stories.tsx +152 -0
  88. package/src/preact/mutationComparison/mutation-comparison.tsx +179 -0
  89. package/src/preact/mutationComparison/queryMutationData.ts +53 -0
  90. package/src/preact/mutationFilter/mutation-filter.stories.tsx +164 -0
  91. package/src/preact/mutationFilter/mutation-filter.tsx +268 -0
  92. package/src/preact/mutationFilter/parseAndValidateMutation.ts +54 -0
  93. package/src/preact/mutationFilter/parseMutation.spec.ts +150 -0
  94. package/src/preact/mutationFilter/sequenceTypeFromSegment.spec.ts +66 -0
  95. package/src/preact/mutationFilter/sequenceTypeFromSegment.ts +20 -0
  96. package/src/preact/mutations/__mockData__/nucleotideInsertions.json +252 -0
  97. package/src/preact/mutations/__mockData__/nucleotideMutations.json +880 -0
  98. package/src/preact/mutations/getInsertionsTableData.spec.ts +36 -0
  99. package/src/preact/mutations/getInsertionsTableData.ts +10 -0
  100. package/src/preact/mutations/getMutationsGridData.spec.ts +135 -0
  101. package/src/preact/mutations/getMutationsGridData.ts +92 -0
  102. package/src/preact/mutations/getMutationsTableData.spec.ts +94 -0
  103. package/src/preact/mutations/getMutationsTableData.ts +17 -0
  104. package/src/preact/mutations/mutations-grid.tsx +84 -0
  105. package/src/preact/mutations/mutations-insertions-table.tsx +33 -0
  106. package/src/preact/mutations/mutations-table.tsx +47 -0
  107. package/src/preact/mutations/mutations.stories.tsx +95 -0
  108. package/src/preact/mutations/mutations.tsx +192 -0
  109. package/src/preact/mutations/queryMutations.ts +55 -0
  110. package/src/preact/prevalenceOverTime/__mockData__/denominator.json +1700 -0
  111. package/src/preact/prevalenceOverTime/__mockData__/denominatorOneVariant.json +608 -0
  112. package/src/preact/prevalenceOverTime/__mockData__/numeratorEG.json +1560 -0
  113. package/src/preact/prevalenceOverTime/__mockData__/numeratorJN1.json +592 -0
  114. package/src/preact/prevalenceOverTime/__mockData__/numeratorOneVariant.json +604 -0
  115. package/src/preact/prevalenceOverTime/getPrevalenceOverTimeTableData.spec.ts +67 -0
  116. package/src/preact/prevalenceOverTime/getPrevalenceOverTimeTableData.ts +18 -0
  117. package/src/preact/prevalenceOverTime/prevalence-over-time-bar-chart.tsx +105 -0
  118. package/src/preact/prevalenceOverTime/prevalence-over-time-bubble-chart.tsx +86 -0
  119. package/src/preact/prevalenceOverTime/prevalence-over-time-line-chart.tsx +141 -0
  120. package/src/preact/prevalenceOverTime/prevalence-over-time-table.tsx +46 -0
  121. package/src/preact/prevalenceOverTime/prevalence-over-time.stories.tsx +165 -0
  122. package/src/preact/prevalenceOverTime/prevalence-over-time.tsx +202 -0
  123. package/src/preact/relativeGrowthAdvantage/__mockData__/denominator.json +376 -0
  124. package/src/preact/relativeGrowthAdvantage/__mockData__/numerator.json +332 -0
  125. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage-chart.tsx +138 -0
  126. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.stories.tsx +71 -0
  127. package/src/preact/relativeGrowthAdvantage/relative-growth-advantage.tsx +136 -0
  128. package/src/preact/shared/charts/LogitScale.ts +48 -0
  129. package/src/preact/shared/charts/colors.ts +26 -0
  130. package/src/preact/shared/charts/confideceInterval.ts +29 -0
  131. package/src/preact/shared/charts/getYAxisScale.ts +16 -0
  132. package/src/preact/shared/charts/scales.ts +16 -0
  133. package/src/preact/shared/icons/DeleteIcon.tsx +17 -0
  134. package/src/preact/shared/sort/sortInsertions.spec.ts +47 -0
  135. package/src/preact/shared/sort/sortInsertions.ts +21 -0
  136. package/src/preact/shared/sort/sortMutationPositions.spec.ts +31 -0
  137. package/src/preact/shared/sort/sortMutationPositions.ts +14 -0
  138. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.spec.ts +47 -0
  139. package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +17 -0
  140. package/src/preact/shared/table/formatProportion.ts +3 -0
  141. package/src/preact/textInput/__mockData__/aggregated_hosts.json +24 -0
  142. package/src/preact/textInput/fetchAutocompleteList.ts +9 -0
  143. package/src/preact/textInput/text-input.stories.tsx +49 -0
  144. package/src/preact/textInput/text-input.tsx +73 -0
  145. package/src/preact/useQuery.ts +27 -0
  146. package/src/query/queryInsertions.ts +14 -0
  147. package/src/query/queryPrevalenceOverTime.ts +126 -0
  148. package/src/query/queryRelativeGrowthAdvantage.ts +131 -0
  149. package/src/query/querySubstitutionsOrDeletions.ts +19 -0
  150. package/src/styles/tailwind.css +3 -0
  151. package/src/styles/tailwind.d.ts +3 -0
  152. package/src/types.ts +23 -0
  153. package/src/utils/mutations.spec.ts +64 -0
  154. package/src/utils/mutations.ts +165 -0
  155. package/src/utils/temporal.spec.ts +97 -0
  156. package/src/utils/temporal.ts +348 -0
  157. package/src/utils/test-utils.ts +5 -0
  158. package/src/utils/type-utils.ts +15 -0
  159. package/src/utils/utils.spec.ts +16 -0
  160. package/src/utils/utils.ts +38 -0
  161. package/src/web-components/PreactLitAdapter.tsx +62 -0
  162. package/src/web-components/PreactLitAdapterWithGridJsStyles.tsx +12 -0
  163. package/src/web-components/app.ts +51 -0
  164. package/src/web-components/display/index.ts +4 -0
  165. package/src/web-components/display/mutation-comparison-component.stories.ts +138 -0
  166. package/src/web-components/display/mutation-comparison-component.tsx +31 -0
  167. package/src/web-components/display/mutations-component.stories.ts +107 -0
  168. package/src/web-components/display/mutations-component.tsx +27 -0
  169. package/src/web-components/display/prevalence-over-time-component.stories.ts +205 -0
  170. package/src/web-components/display/prevalence-over-time-component.tsx +46 -0
  171. package/src/web-components/display/relative-growth-advantage-component.stories.ts +89 -0
  172. package/src/web-components/display/relative-growth-advantage-component.tsx +37 -0
  173. package/src/web-components/index.ts +3 -0
  174. package/src/web-components/input/date-range-selector-component.stories.ts +53 -0
  175. package/src/web-components/input/date-range-selector-component.tsx +33 -0
  176. package/src/web-components/input/index.ts +4 -0
  177. package/src/web-components/input/location-filter-component.stories.ts +184 -0
  178. package/src/web-components/input/location-filter-component.tsx +68 -0
  179. package/src/web-components/input/location-filter.mdx +25 -0
  180. package/src/web-components/input/mutation-filter-component.stories.ts +97 -0
  181. package/src/web-components/input/mutation-filter-component.tsx +27 -0
  182. package/src/web-components/input/text-input-component.stories.ts +92 -0
  183. package/src/web-components/input/text-input-component.tsx +30 -0
  184. package/src/web-components/lapis-context.ts +3 -0
  185. package/src/web-components/reference-genome-context.ts +5 -0
  186. package/src/web-components/withinShadowRoot.story.ts +34 -0
@@ -0,0 +1,184 @@
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
+ import { ifDefined } from 'lit/directives/if-defined.js';
6
+
7
+ import { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
8
+ import '../app';
9
+ import './location-filter-component';
10
+ import data from '../../preact/locationFilter/__mockData__/aggregated.json';
11
+ import { type LocationFilterProps } from '../../preact/locationFilter/location-filter';
12
+ import { withinShadowRoot } from '../withinShadowRoot.story';
13
+
14
+ const meta: Meta = {
15
+ title: 'Input/Location filter',
16
+ component: 'gs-location-filter',
17
+ parameters: {
18
+ actions: {
19
+ handles: ['gs-location-changed'],
20
+ },
21
+ },
22
+ decorators: [withActions],
23
+ };
24
+
25
+ export default meta;
26
+
27
+ const Template: StoryObj<LocationFilterProps> = {
28
+ render: (args) => {
29
+ return html` <gs-app lapis="${LAPIS_URL}">
30
+ <div class="max-w-screen-lg">
31
+ <gs-location-filter .fields=${args.fields} value=${ifDefined(args.value)}></gs-location-filter>
32
+ </div>
33
+ </gs-app>`;
34
+ },
35
+ args: {
36
+ fields: ['region', 'country', 'division', 'location'],
37
+ value: '',
38
+ },
39
+ };
40
+
41
+ const aggregatedEndpointMatcher = {
42
+ name: 'numeratorEG',
43
+ url: AGGREGATED_ENDPOINT,
44
+ body: {
45
+ fields: ['region', 'country', 'division', 'location'],
46
+ },
47
+ };
48
+
49
+ export const LocationFilter: StoryObj<LocationFilterProps> = {
50
+ ...Template,
51
+ parameters: {
52
+ fetchMock: {
53
+ mocks: [
54
+ {
55
+ matcher: aggregatedEndpointMatcher,
56
+ response: {
57
+ status: 200,
58
+ body: data,
59
+ },
60
+ },
61
+ ],
62
+ },
63
+ },
64
+ play: async ({ canvasElement }) => {
65
+ const canvas = await withinShadowRoot(canvasElement, 'gs-location-filter');
66
+ await waitFor(() => {
67
+ return expect(canvas.getByRole('combobox')).toBeEnabled();
68
+ });
69
+ },
70
+ };
71
+
72
+ export const DelayToShowLoadingState: StoryObj<LocationFilterProps> = {
73
+ ...Template,
74
+ parameters: {
75
+ fetchMock: {
76
+ mocks: [
77
+ {
78
+ matcher: aggregatedEndpointMatcher,
79
+ response: {
80
+ status: 200,
81
+ body: data,
82
+ },
83
+ options: {
84
+ delay: 5000,
85
+ },
86
+ },
87
+ ],
88
+ },
89
+ },
90
+ };
91
+
92
+ export const FetchingLocationsFails: StoryObj<LocationFilterProps> = {
93
+ ...Template,
94
+ parameters: {
95
+ fetchMock: {
96
+ mocks: [
97
+ {
98
+ matcher: aggregatedEndpointMatcher,
99
+ response: {
100
+ status: 500,
101
+ body: { error: 'no data' },
102
+ },
103
+ },
104
+ ],
105
+ },
106
+ },
107
+ play: async ({ canvasElement }) => {
108
+ const canvas = await withinShadowRoot(canvasElement, 'gs-location-filter');
109
+
110
+ await waitFor(() =>
111
+ expect(
112
+ canvas.getByText('Internal Server Error: {"error":"no data"} ', { exact: false }),
113
+ ).toBeInTheDocument(),
114
+ );
115
+ },
116
+ };
117
+
118
+ export const FiresEvent: StoryObj<LocationFilterProps> = {
119
+ ...Template,
120
+ parameters: {
121
+ fetchMock: {
122
+ mocks: [
123
+ {
124
+ matcher: aggregatedEndpointMatcher,
125
+ response: {
126
+ status: 200,
127
+ body: data,
128
+ },
129
+ },
130
+ ],
131
+ },
132
+ },
133
+ play: async ({ canvasElement, step }) => {
134
+ const canvas = await withinShadowRoot(canvasElement, 'gs-location-filter');
135
+
136
+ const submitButton = () => canvas.getByRole('button', { name: 'Submit' });
137
+ const inputField = () => canvas.getByRole('combobox');
138
+
139
+ const listenerMock = fn();
140
+ await step('Setup event listener mock', async () => {
141
+ canvasElement.addEventListener('gs-location-changed', listenerMock);
142
+ });
143
+
144
+ await step('wait until data is loaded', async () => {
145
+ await waitFor(() => {
146
+ return expect(inputField()).toBeEnabled();
147
+ });
148
+ });
149
+
150
+ await step('Input invalid location', async () => {
151
+ await userEvent.type(inputField(), 'Not / A / Location');
152
+ await userEvent.click(submitButton());
153
+ await expect(listenerMock).not.toHaveBeenCalled();
154
+ await userEvent.type(inputField(), '{backspace>18/}');
155
+ });
156
+
157
+ await step('Select Asia', async () => {
158
+ await userEvent.type(inputField(), 'Asia');
159
+ await userEvent.click(submitButton());
160
+ await expect(listenerMock).toHaveBeenCalledWith(
161
+ expect.objectContaining({
162
+ detail: {
163
+ region: 'Asia',
164
+ },
165
+ }),
166
+ );
167
+ });
168
+
169
+ await step('Select Asia / Bangladesh / Rajshahi / Chapainawabgonj', async () => {
170
+ await userEvent.type(inputField(), ' / Bangladesh / Rajshahi / Chapainawabgonj');
171
+ await userEvent.click(submitButton());
172
+ await expect(listenerMock).toHaveBeenCalledWith(
173
+ expect.objectContaining({
174
+ detail: {
175
+ region: 'Asia',
176
+ country: 'Bangladesh',
177
+ division: 'Rajshahi',
178
+ location: 'Chapainawabgonj',
179
+ },
180
+ }),
181
+ );
182
+ });
183
+ },
184
+ };
@@ -0,0 +1,68 @@
1
+ import { customElement, property } from 'lit/decorators.js';
2
+
3
+ import { LocationFilter } from '../../preact/locationFilter/location-filter';
4
+ import { PreactLitAdapter } from '../PreactLitAdapter';
5
+
6
+ /**
7
+ * ## Tag
8
+ *
9
+ * `gs-location-filter`
10
+ *
11
+ * ## Context
12
+ *
13
+ * This component provides an input field to specify filters for locations.
14
+ *
15
+ * It expects a list of fields that form a strict hierarchical order, such as continent, country, and city.
16
+ * The component retrieves a list of all possible values for these fields from the Lapis instance.
17
+ * This list is then utilized to display autocomplete suggestions and to validate the input.
18
+ *
19
+ * Given `fields` are `['field1', 'field2', ..., 'fieldN']`,
20
+ * then valid values for the location filter must be in the form `valueForField1 / valueForField2 / ... / valueForFieldK`,
21
+ * where `1 <= K <= N`.
22
+ * Values for the fields `i > K` are considered `undefined`.
23
+ *
24
+ * @fires {CustomEvent<Record<string, string>>} gs-location-changed
25
+ * Fired when the field is submitted with a valid location value.
26
+ * The `details` of this event contain an object with all `fields` as keys
27
+ * and the corresponding values as values, if they are not `undefined`.
28
+ * Example:
29
+ * ```
30
+ * {
31
+ * continent: "Asia",
32
+ * country: "China",
33
+ * city: "Beijing"
34
+ * }
35
+ * ```
36
+ */
37
+ @customElement('gs-location-filter')
38
+ export class LocationFilterComponent extends PreactLitAdapter {
39
+ /**
40
+ * The initial value to use for this location filter.
41
+ * Must be of the form `valueForField1 / valueForField2 / ... / valueForFieldN`.
42
+ */
43
+ @property()
44
+ value = '';
45
+
46
+ /**
47
+ * The fields to display in the location filter, in hierarchical order.
48
+ * The top-level field should be the first entry in the array.
49
+ * This component assumes that the values for each field form a strict hierarchy
50
+ * (e.g., `fields = ['continent', 'country', 'city']`).
51
+ */
52
+ @property({ type: Array })
53
+ fields: string[] = [];
54
+
55
+ override render() {
56
+ return <LocationFilter value={this.value} fields={this.fields} />;
57
+ }
58
+ }
59
+
60
+ declare global {
61
+ interface HTMLElementTagNameMap {
62
+ 'gs-location-filter': LocationFilterComponent;
63
+ }
64
+
65
+ interface HTMLElementEventMap {
66
+ 'gs-location-changed': CustomEvent<Record<string, string>>;
67
+ }
68
+ }
@@ -0,0 +1,25 @@
1
+ import { ArgTypes, Description, Meta, Story, Title, Source } from '@storybook/blocks';
2
+
3
+ import * as LocationFilterStories from './location-filter-component.stories.ts';
4
+
5
+ <Meta of={LocationFilterStories} name='Docs' />
6
+
7
+ <Title of={LocationFilterStories} />
8
+
9
+ <Description of={LocationFilterStories} />
10
+
11
+ ## Specification
12
+
13
+ <ArgTypes of={LocationFilterStories} />
14
+
15
+ ## Example
16
+
17
+ <Source
18
+ code={`<gs-location-filter fields="['continent', 'country']" value='Europe / Switzerland'></gs-location-filter>`}
19
+ />
20
+
21
+ ### Live Example
22
+
23
+ [See here](?path=/story/input-location-filter--location-filter)
24
+
25
+ <Story of={LocationFilterStories.LocationFilter} />
@@ -0,0 +1,97 @@
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 { LAPIS_URL } from '../../constants';
7
+ import '../app';
8
+ import { withinShadowRoot } from '../withinShadowRoot.story';
9
+ import './mutation-filter-component';
10
+
11
+ const meta: Meta = {
12
+ title: 'Input/Mutation filter',
13
+ component: 'gs-mutation-filter',
14
+ parameters: {
15
+ actions: {
16
+ handles: ['gs-mutation-filter-changed', 'gs-mutation-filter-on-blur'],
17
+ },
18
+ fetchMock: {},
19
+ },
20
+ decorators: [withActions],
21
+ };
22
+
23
+ export default meta;
24
+
25
+ export const Default: StoryObj<{ lapisField: string; placeholderText: string }> = {
26
+ render: () => {
27
+ return html` <gs-app lapis="${LAPIS_URL}">
28
+ <div class="max-w-screen-lg">
29
+ <gs-mutation-filter></gs-mutation-filter>
30
+ </div>
31
+ </gs-app>`;
32
+ },
33
+ };
34
+
35
+ export const FiresFilterChangedEvent: StoryObj<{ lapisField: string; placeholderText: string }> = {
36
+ ...Default,
37
+ play: async ({ canvasElement, step }) => {
38
+ const canvas = await withinShadowRoot(canvasElement, 'gs-mutation-filter');
39
+
40
+ const inputField = () => canvas.getByPlaceholderText('Enter a mutation');
41
+ const submitButton = () => canvas.getByRole('button', { name: '+' });
42
+ const listenerMock = fn();
43
+ await step('Setup event listener mock', async () => {
44
+ canvasElement.addEventListener('gs-mutation-filter-changed', listenerMock);
45
+ });
46
+
47
+ await step('wait until data is loaded', async () => {
48
+ await waitFor(() => {
49
+ return expect(inputField()).toBeEnabled();
50
+ });
51
+ });
52
+
53
+ await step('Enter a valid mutation', async () => {
54
+ await userEvent.type(inputField(), 'A123T');
55
+ await waitFor(() => submitButton().click());
56
+
57
+ await waitFor(() =>
58
+ expect(listenerMock).toHaveBeenCalledWith(
59
+ expect.objectContaining({
60
+ detail: {
61
+ nucleotideMutations: ['A123T'],
62
+ aminoAcidMutations: [],
63
+ nucleotideInsertions: [],
64
+ aminoAcidInsertions: [],
65
+ },
66
+ }),
67
+ ),
68
+ );
69
+ });
70
+ },
71
+ };
72
+
73
+ export const FiresFilterOnBlurEvent: StoryObj<{ lapisField: string; placeholderText: string }> = {
74
+ ...Default,
75
+ play: async ({ canvasElement, step }) => {
76
+ const canvas = await withinShadowRoot(canvasElement, 'gs-mutation-filter');
77
+
78
+ const inputField = () => canvas.getByPlaceholderText('Enter a mutation');
79
+ const listenerMock = fn();
80
+ await step('Setup event listener mock', async () => {
81
+ canvasElement.addEventListener('gs-mutation-filter-on-blur', listenerMock);
82
+ });
83
+
84
+ await step('wait until data is loaded', async () => {
85
+ await waitFor(() => {
86
+ return expect(inputField()).toBeEnabled();
87
+ });
88
+ });
89
+
90
+ await step('Move outside of input', async () => {
91
+ await userEvent.type(inputField(), 'A123T');
92
+ await userEvent.tab();
93
+
94
+ await expect(listenerMock).toHaveBeenCalled();
95
+ });
96
+ },
97
+ };
@@ -0,0 +1,27 @@
1
+ import { customElement } from 'lit/decorators.js';
2
+
3
+ import { type TextInputComponent } from './text-input-component';
4
+ import { MutationFilter, type SelectedMutationFilterStrings } from '../../preact/mutationFilter/mutation-filter';
5
+ import { PreactLitAdapter } from '../PreactLitAdapter';
6
+
7
+ /**
8
+ * @fires {CustomEvent<SelectedMutationFilterStrings>} gs-mutation-filter-changed - When the mutation filter values have changed
9
+ * @fires {CustomEvent<SelectedMutationFilterStrings>} gs-mutation-filter-on-blur - When the mutation filter has lost focus
10
+ */
11
+ @customElement('gs-mutation-filter')
12
+ export class MutationFilterComponent extends PreactLitAdapter {
13
+ override render() {
14
+ return <MutationFilter />;
15
+ }
16
+ }
17
+
18
+ declare global {
19
+ interface HTMLElementTagNameMap {
20
+ 'gs-mutation-filter': TextInputComponent;
21
+ }
22
+
23
+ interface HTMLElementEventMap {
24
+ 'gs-mutation-filter-changed': CustomEvent<SelectedMutationFilterStrings>;
25
+ 'gs-mutation-filter-on-blur': CustomEvent<SelectedMutationFilterStrings>;
26
+ }
27
+ }
@@ -0,0 +1,92 @@
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 { AGGREGATED_ENDPOINT, LAPIS_URL } from '../../constants';
7
+ import '../app';
8
+ import './text-input-component';
9
+ import data from '../../preact/textInput/__mockData__/aggregated_hosts.json';
10
+ import { withinShadowRoot } from '../withinShadowRoot.story';
11
+
12
+ const meta: Meta = {
13
+ title: 'Input/Text input',
14
+ component: 'gs-text-input',
15
+ parameters: {
16
+ actions: {
17
+ handles: ['gs-text-input-changed'],
18
+ },
19
+ fetchMock: {
20
+ mocks: [
21
+ {
22
+ matcher: {
23
+ name: 'hosts',
24
+ url: AGGREGATED_ENDPOINT,
25
+ body: {
26
+ fields: ['host'],
27
+ },
28
+ },
29
+ response: {
30
+ status: 200,
31
+ body: data,
32
+ },
33
+ },
34
+ ],
35
+ },
36
+ },
37
+ decorators: [withActions],
38
+ tags: ['autodocs'],
39
+ };
40
+
41
+ export default meta;
42
+
43
+ export const Default: StoryObj<{ lapisField: string; placeholderText: string }> = {
44
+ render: (args) => {
45
+ return html` <gs-app lapis="${LAPIS_URL}">
46
+ <div class="max-w-screen-lg">
47
+ <gs-text-input .lapisField=${args.lapisField} .placeholderText=${args.placeholderText}></gs-text-input>
48
+ </div>
49
+ </gs-app>`;
50
+ },
51
+ args: {
52
+ lapisField: 'host',
53
+ placeholderText: 'Enter host name',
54
+ },
55
+ };
56
+
57
+ export const FiresEvent: StoryObj<{ lapisField: string; placeholderText: string }> = {
58
+ ...Default,
59
+ play: async ({ canvasElement, step }) => {
60
+ const canvas = await withinShadowRoot(canvasElement, 'gs-text-input');
61
+
62
+ const inputField = () => canvas.getByPlaceholderText('Enter host name');
63
+ const listenerMock = fn();
64
+ await step('Setup event listener mock', async () => {
65
+ canvasElement.addEventListener('gs-text-input-changed', listenerMock);
66
+ });
67
+
68
+ await step('wait until data is loaded', async () => {
69
+ await waitFor(() => {
70
+ return expect(inputField()).toBeEnabled();
71
+ });
72
+ });
73
+
74
+ await step('Enters an invalid host name', async () => {
75
+ await userEvent.type(inputField(), 'notInList');
76
+ await expect(listenerMock).not.toHaveBeenCalled();
77
+ await userEvent.type(inputField(), '{backspace>9/}');
78
+ });
79
+
80
+ await step('Enter a valid host name', async () => {
81
+ await userEvent.type(inputField(), 'Homo');
82
+
83
+ await expect(listenerMock).toHaveBeenCalledWith(
84
+ expect.objectContaining({
85
+ detail: {
86
+ host: 'Homo',
87
+ },
88
+ }),
89
+ );
90
+ });
91
+ },
92
+ };
@@ -0,0 +1,30 @@
1
+ import { customElement, property } from 'lit/decorators.js';
2
+
3
+ import { TextInput } from '../../preact/textInput/text-input';
4
+ import { PreactLitAdapter } from '../PreactLitAdapter';
5
+
6
+ /**
7
+ * @fires {CustomEvent<Record<string, string>>} gs-text-input-changed - When the text input has changed
8
+ */
9
+ @customElement('gs-text-input')
10
+ export class TextInputComponent extends PreactLitAdapter {
11
+ @property()
12
+ lapisField = '';
13
+
14
+ @property()
15
+ placeholderText = '';
16
+
17
+ override render() {
18
+ return <TextInput lapisField={this.lapisField} placeholderText={this.placeholderText} />;
19
+ }
20
+ }
21
+
22
+ declare global {
23
+ interface HTMLElementTagNameMap {
24
+ 'gs-text-input': TextInputComponent;
25
+ }
26
+
27
+ interface HTMLElementEventMap {
28
+ 'gs-text-input-changed': CustomEvent<Record<string, string>>;
29
+ }
30
+ }
@@ -0,0 +1,3 @@
1
+ import { createContext } from '@lit/context';
2
+
3
+ export const lapisContext = createContext<string>('lapis-context');
@@ -0,0 +1,5 @@
1
+ import { createContext } from '@lit/context';
2
+
3
+ import { type ReferenceGenome } from '../lapisApi/ReferenceGenome';
4
+
5
+ export const referenceGenomeContext = createContext<ReferenceGenome>('reference-genome-context');
@@ -0,0 +1,34 @@
1
+ import { expect, waitFor, within } from '@storybook/test';
2
+
3
+ export default {};
4
+
5
+ /**
6
+ * taken from https://github.com/storybookjs/testing-library/issues/24#issuecomment-1593709872
7
+ *
8
+ * Usage:
9
+ * ```ts
10
+ * export const MyStory: StoryObj = {
11
+ * render: () => html` <my-web-component />`,
12
+ * play: async ({ canvasElement, step }) => {
13
+ * const canvas = await withinShadowRoot(canvasElement, 'my-web-component');
14
+ *
15
+ * await userEvent.click(canvas.getByRole('button'));
16
+ * // ...
17
+ * },
18
+ * };
19
+ * ```
20
+ */
21
+ export async function withinShadowRoot(customElement: HTMLElement, selector: string) {
22
+ const webComponent = customElement.querySelector(selector);
23
+
24
+ await waitFor(
25
+ () => {
26
+ const shadowRootFirstEl = webComponent?.shadowRoot?.firstElementChild as HTMLElement;
27
+ return expect(shadowRootFirstEl).toContainElement(shadowRootFirstEl);
28
+ },
29
+ { timeout: 1000 },
30
+ );
31
+
32
+ // force type HTMLElement to ignore the type checking of the "within" function
33
+ return within(webComponent?.shadowRoot as unknown as HTMLElement);
34
+ }