@genspectrum/dashboard-components 0.12.1 → 0.13.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.
- package/custom-elements.json +292 -25
- package/dist/{LocationChangedEvent-CORvQvXv.js → LineageFilterChangedEvent-GedKNGFI.js} +25 -3
- package/dist/LineageFilterChangedEvent-GedKNGFI.js.map +1 -0
- package/dist/assets/mutationOverTimeWorker-B1-WrM4b.js.map +1 -0
- package/dist/components.d.ts +124 -25
- package/dist/components.js +765 -572
- package/dist/components.js.map +1 -1
- package/dist/style.css +3 -0
- package/dist/util.d.ts +48 -18
- package/dist/util.js +3 -1
- package/package.json +2 -2
- package/src/constants.ts +6 -0
- package/src/lapisApi/__mockData__/wiseReferenceGenome.json +9 -0
- package/src/lapisApi/lapisApi.ts +17 -0
- package/src/lapisApi/lapisTypes.ts +7 -1
- package/src/operator/FetchDetailsOperator.ts +28 -0
- package/src/preact/components/downshift-combobox.tsx +145 -0
- package/src/preact/components/tabs.tsx +1 -1
- package/src/preact/lineageFilter/LineageFilterChangedEvent.ts +11 -0
- package/src/preact/lineageFilter/fetchLineageAutocompleteList.spec.ts +16 -2
- package/src/preact/lineageFilter/fetchLineageAutocompleteList.ts +13 -2
- package/src/preact/lineageFilter/lineage-filter.stories.tsx +110 -9
- package/src/preact/lineageFilter/lineage-filter.tsx +40 -50
- package/src/preact/locationFilter/LocationChangedEvent.ts +1 -1
- package/src/preact/locationFilter/fetchAutocompletionList.spec.ts +6 -2
- package/src/preact/locationFilter/fetchAutocompletionList.ts +16 -6
- package/src/preact/locationFilter/location-filter.stories.tsx +33 -30
- package/src/preact/locationFilter/location-filter.tsx +47 -144
- package/src/preact/mutationsOverTime/MutationOverTimeData.ts +9 -5
- package/src/preact/mutationsOverTime/mutations-over-time-grid.tsx +5 -3
- package/src/preact/shared/sort/sortSubstitutionsAndDeletions.ts +4 -7
- package/src/preact/textInput/TextInputChangedEvent.ts +1 -1
- package/src/preact/textInput/fetchStringAutocompleteList.ts +20 -0
- package/src/preact/textInput/text-input.stories.tsx +14 -11
- package/src/preact/textInput/text-input.tsx +39 -140
- package/src/preact/wastewater/mutationsOverTime/__mockData__/details.json +88 -0
- package/src/preact/wastewater/mutationsOverTime/computeWastewaterMutationsOverTimeDataPerLocation.spec.ts +159 -0
- package/src/preact/wastewater/mutationsOverTime/computeWastewaterMutationsOverTimeDataPerLocation.ts +51 -0
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.stories.tsx +71 -0
- package/src/preact/wastewater/mutationsOverTime/wastewater-mutations-over-time.tsx +151 -0
- package/src/query/queryMutationsOverTime.ts +6 -14
- package/src/query/queryWastewaterMutationsOverTime.spec.ts +94 -0
- package/src/query/queryWastewaterMutationsOverTime.ts +55 -0
- package/src/types.ts +3 -0
- package/src/utilEntrypoint.ts +2 -0
- package/src/utils/map2d.ts +39 -0
- package/src/web-components/index.ts +1 -0
- package/src/web-components/input/gs-lineage-filter.stories.ts +120 -31
- package/src/web-components/input/gs-lineage-filter.tsx +24 -8
- package/src/web-components/input/gs-location-filter.stories.ts +9 -0
- package/src/web-components/input/gs-location-filter.tsx +21 -3
- package/src/web-components/input/gs-text-input.stories.ts +14 -5
- package/src/web-components/input/gs-text-input.tsx +23 -7
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.stories.ts +82 -0
- package/src/web-components/wastewaterVisualization/gs-wastewater-mutations-over-time.tsx +112 -0
- package/src/web-components/wastewaterVisualization/index.ts +1 -0
- package/standalone-bundle/assets/{mutationOverTimeWorker-DEybsZ5r.js.map → mutationOverTimeWorker-Cls1J0cl.js.map} +1 -1
- package/standalone-bundle/dashboard-components.js +6972 -6796
- package/standalone-bundle/dashboard-components.js.map +1 -1
- package/standalone-bundle/style.css +1 -1
- package/dist/LocationChangedEvent-CORvQvXv.js.map +0 -1
- package/dist/assets/mutationOverTimeWorker-DTv93Ere.js.map +0 -1
- package/src/preact/textInput/fetchAutocompleteList.ts +0 -9
package/dist/style.css
CHANGED
package/dist/util.d.ts
CHANGED
|
@@ -160,7 +160,17 @@ declare const lapisFilterSchema: default_2.ZodIntersection<default_2.ZodRecord<d
|
|
|
160
160
|
aminoAcidInsertions?: string[] | undefined;
|
|
161
161
|
}>>;
|
|
162
162
|
|
|
163
|
-
declare type
|
|
163
|
+
declare type LapisLineageFilter = Record<string, string | undefined>;
|
|
164
|
+
|
|
165
|
+
declare type LapisLocationFilter = default_2.infer<typeof lapisLocationFilterSchema>;
|
|
166
|
+
|
|
167
|
+
declare const lapisLocationFilterSchema: default_2.ZodRecord<default_2.ZodString, default_2.ZodUnion<[default_2.ZodString, default_2.ZodUndefined]>>;
|
|
168
|
+
|
|
169
|
+
declare type LapisTextFilter = Record<string, string | undefined>;
|
|
170
|
+
|
|
171
|
+
export declare class LineageFilterChangedEvent extends CustomEvent<LapisLineageFilter> {
|
|
172
|
+
constructor(detail: LapisLineageFilter);
|
|
173
|
+
}
|
|
164
174
|
|
|
165
175
|
export declare class LocationChangedEvent extends CustomEvent<LapisLocationFilter> {
|
|
166
176
|
constructor(detail: LapisLocationFilter);
|
|
@@ -772,6 +782,10 @@ export declare type TemporalGranularity = default_2.infer<typeof temporalGranula
|
|
|
772
782
|
|
|
773
783
|
declare const temporalGranularitySchema: default_2.ZodUnion<[default_2.ZodLiteral<"day">, default_2.ZodLiteral<"week">, default_2.ZodLiteral<"month">, default_2.ZodLiteral<"year">]>;
|
|
774
784
|
|
|
785
|
+
export declare class TextInputChangedEvent extends CustomEvent<LapisTextFilter> {
|
|
786
|
+
constructor(detail: LapisTextFilter);
|
|
787
|
+
}
|
|
788
|
+
|
|
775
789
|
export declare const views: {
|
|
776
790
|
readonly table: "table";
|
|
777
791
|
readonly venn: "venn";
|
|
@@ -822,6 +836,22 @@ declare global {
|
|
|
822
836
|
}
|
|
823
837
|
|
|
824
838
|
|
|
839
|
+
declare global {
|
|
840
|
+
interface HTMLElementTagNameMap {
|
|
841
|
+
'gs-wastewater-mutations-over-time': WastewaterMutationsOverTimeComponent;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
declare global {
|
|
847
|
+
namespace JSX {
|
|
848
|
+
interface IntrinsicElements {
|
|
849
|
+
'gs-wastewater-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
|
|
825
855
|
declare global {
|
|
826
856
|
interface HTMLElementTagNameMap {
|
|
827
857
|
'gs-mutation-comparison-component': MutationComparisonComponent;
|
|
@@ -888,7 +918,7 @@ declare global {
|
|
|
888
918
|
|
|
889
919
|
declare global {
|
|
890
920
|
interface HTMLElementTagNameMap {
|
|
891
|
-
'gs-
|
|
921
|
+
'gs-aggregate': AggregateComponent;
|
|
892
922
|
}
|
|
893
923
|
}
|
|
894
924
|
|
|
@@ -896,7 +926,7 @@ declare global {
|
|
|
896
926
|
declare global {
|
|
897
927
|
namespace JSX {
|
|
898
928
|
interface IntrinsicElements {
|
|
899
|
-
'gs-
|
|
929
|
+
'gs-aggregate': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
900
930
|
}
|
|
901
931
|
}
|
|
902
932
|
}
|
|
@@ -904,7 +934,7 @@ declare global {
|
|
|
904
934
|
|
|
905
935
|
declare global {
|
|
906
936
|
interface HTMLElementTagNameMap {
|
|
907
|
-
'gs-
|
|
937
|
+
'gs-number-sequences-over-time': NumberSequencesOverTimeComponent;
|
|
908
938
|
}
|
|
909
939
|
}
|
|
910
940
|
|
|
@@ -912,7 +942,7 @@ declare global {
|
|
|
912
942
|
declare global {
|
|
913
943
|
namespace JSX {
|
|
914
944
|
interface IntrinsicElements {
|
|
915
|
-
'gs-
|
|
945
|
+
'gs-number-sequences-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
916
946
|
}
|
|
917
947
|
}
|
|
918
948
|
}
|
|
@@ -920,7 +950,7 @@ declare global {
|
|
|
920
950
|
|
|
921
951
|
declare global {
|
|
922
952
|
interface HTMLElementTagNameMap {
|
|
923
|
-
'gs-
|
|
953
|
+
'gs-sequences-by-location': SequencesByLocationComponent;
|
|
924
954
|
}
|
|
925
955
|
}
|
|
926
956
|
|
|
@@ -928,7 +958,7 @@ declare global {
|
|
|
928
958
|
declare global {
|
|
929
959
|
namespace JSX {
|
|
930
960
|
interface IntrinsicElements {
|
|
931
|
-
'gs-
|
|
961
|
+
'gs-sequences-by-location': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
932
962
|
}
|
|
933
963
|
}
|
|
934
964
|
}
|
|
@@ -936,7 +966,7 @@ declare global {
|
|
|
936
966
|
|
|
937
967
|
declare global {
|
|
938
968
|
interface HTMLElementTagNameMap {
|
|
939
|
-
'gs-
|
|
969
|
+
'gs-mutations-over-time': MutationsOverTimeComponent;
|
|
940
970
|
}
|
|
941
971
|
}
|
|
942
972
|
|
|
@@ -944,7 +974,7 @@ declare global {
|
|
|
944
974
|
declare global {
|
|
945
975
|
namespace JSX {
|
|
946
976
|
interface IntrinsicElements {
|
|
947
|
-
'gs-
|
|
977
|
+
'gs-mutations-over-time': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
948
978
|
}
|
|
949
979
|
}
|
|
950
980
|
}
|
|
@@ -1007,10 +1037,10 @@ declare global {
|
|
|
1007
1037
|
|
|
1008
1038
|
declare global {
|
|
1009
1039
|
interface HTMLElementTagNameMap {
|
|
1010
|
-
'gs-
|
|
1040
|
+
'gs-text-input': TextInputComponent;
|
|
1011
1041
|
}
|
|
1012
1042
|
interface HTMLElementEventMap {
|
|
1013
|
-
'gs-
|
|
1043
|
+
'gs-text-input-changed': TextInputChangedEvent;
|
|
1014
1044
|
}
|
|
1015
1045
|
}
|
|
1016
1046
|
|
|
@@ -1018,7 +1048,7 @@ declare global {
|
|
|
1018
1048
|
declare global {
|
|
1019
1049
|
namespace JSX {
|
|
1020
1050
|
interface IntrinsicElements {
|
|
1021
|
-
'gs-
|
|
1051
|
+
'gs-text-input': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1022
1052
|
}
|
|
1023
1053
|
}
|
|
1024
1054
|
}
|
|
@@ -1026,10 +1056,10 @@ declare global {
|
|
|
1026
1056
|
|
|
1027
1057
|
declare global {
|
|
1028
1058
|
interface HTMLElementTagNameMap {
|
|
1029
|
-
'gs-
|
|
1059
|
+
'gs-mutation-filter': MutationFilterComponent;
|
|
1030
1060
|
}
|
|
1031
1061
|
interface HTMLElementEventMap {
|
|
1032
|
-
'gs-
|
|
1062
|
+
'gs-mutation-filter-changed': CustomEvent<MutationsFilter>;
|
|
1033
1063
|
}
|
|
1034
1064
|
}
|
|
1035
1065
|
|
|
@@ -1037,7 +1067,7 @@ declare global {
|
|
|
1037
1067
|
declare global {
|
|
1038
1068
|
namespace JSX {
|
|
1039
1069
|
interface IntrinsicElements {
|
|
1040
|
-
'gs-
|
|
1070
|
+
'gs-mutation-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1041
1071
|
}
|
|
1042
1072
|
}
|
|
1043
1073
|
}
|
|
@@ -1045,10 +1075,10 @@ declare global {
|
|
|
1045
1075
|
|
|
1046
1076
|
declare global {
|
|
1047
1077
|
interface HTMLElementTagNameMap {
|
|
1048
|
-
'gs-
|
|
1078
|
+
'gs-lineage-filter': LineageFilterComponent;
|
|
1049
1079
|
}
|
|
1050
1080
|
interface HTMLElementEventMap {
|
|
1051
|
-
'gs-
|
|
1081
|
+
'gs-lineage-filter-changed': LineageFilterChangedEvent;
|
|
1052
1082
|
}
|
|
1053
1083
|
}
|
|
1054
1084
|
|
|
@@ -1056,7 +1086,7 @@ declare global {
|
|
|
1056
1086
|
declare global {
|
|
1057
1087
|
namespace JSX {
|
|
1058
1088
|
interface IntrinsicElements {
|
|
1059
|
-
'gs-
|
|
1089
|
+
'gs-lineage-filter': DetailedHTMLProps<HTMLAttributes<HTMLElement>, HTMLElement>;
|
|
1060
1090
|
}
|
|
1061
1091
|
}
|
|
1062
1092
|
}
|
package/dist/util.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { D, L, d, v } from "./
|
|
1
|
+
import { D, a, L, T, d, v } from "./LineageFilterChangedEvent-GedKNGFI.js";
|
|
2
2
|
export {
|
|
3
3
|
D as DateRangeOptionChangedEvent,
|
|
4
|
+
a as LineageFilterChangedEvent,
|
|
4
5
|
L as LocationChangedEvent,
|
|
6
|
+
T as TextInputChangedEvent,
|
|
5
7
|
d as dateRangeOptionPresets,
|
|
6
8
|
v as views
|
|
7
9
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genspectrum/dashboard-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "GenSpectrum web components for building dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
@@ -142,6 +142,6 @@
|
|
|
142
142
|
"typescript": "~5.7.2",
|
|
143
143
|
"vite": "^6.0.3",
|
|
144
144
|
"vite-plugin-dts": "^4.0.3",
|
|
145
|
-
"vitest": "^
|
|
145
|
+
"vitest": "^3.0.2"
|
|
146
146
|
}
|
|
147
147
|
}
|
package/src/constants.ts
CHANGED
|
@@ -5,3 +5,9 @@ export const NUCLEOTIDE_MUTATIONS_ENDPOINT = `${LAPIS_URL}/sample/nucleotideMuta
|
|
|
5
5
|
export const AMINO_ACID_MUTATIONS_ENDPOINT = `${LAPIS_URL}/sample/aminoAcidMutations`;
|
|
6
6
|
export const NUCLEOTIDE_INSERTIONS_ENDPOINT = `${LAPIS_URL}/sample/nucleotideInsertions`;
|
|
7
7
|
export const REFERENCE_GENOME_ENDPOINT = `${LAPIS_URL}/sample/referenceGenome`;
|
|
8
|
+
|
|
9
|
+
// WISE Wastewater
|
|
10
|
+
// This is a special instance for storing Swiss wastewater data generated by the WISE consortium
|
|
11
|
+
export const WISE_LAPIS_URL = 'https://api.wise-loculus.genspectrum.org/rsv';
|
|
12
|
+
export const WISE_DETAILS_ENDPOINT = `${WISE_LAPIS_URL}/sample/details`;
|
|
13
|
+
export const WISE_REFERENCE_GENOME_ENDPOINT = `${WISE_LAPIS_URL}/sample/referenceGenome`;
|
package/src/lapisApi/lapisApi.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { referenceGenomeResponse } from './ReferenceGenome';
|
|
2
2
|
import {
|
|
3
3
|
aggregatedResponse,
|
|
4
|
+
detailsResponse,
|
|
4
5
|
insertionsResponse,
|
|
5
6
|
type LapisBaseRequest,
|
|
6
7
|
lapisError,
|
|
@@ -51,6 +52,21 @@ export async function fetchAggregated(lapisUrl: string, body: LapisBaseRequest,
|
|
|
51
52
|
return aggregatedResponse.parse(await response.json());
|
|
52
53
|
}
|
|
53
54
|
|
|
55
|
+
export async function fetchDetails(lapisUrl: string, body: LapisBaseRequest, signal?: AbortSignal) {
|
|
56
|
+
const response = await fetch(detailsEndpoint(lapisUrl), {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: {
|
|
59
|
+
'Content-Type': 'application/json',
|
|
60
|
+
},
|
|
61
|
+
body: JSON.stringify(body),
|
|
62
|
+
signal,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await handleErrors(response, 'aggregated data');
|
|
66
|
+
|
|
67
|
+
return detailsResponse.parse(await response.json());
|
|
68
|
+
}
|
|
69
|
+
|
|
54
70
|
export async function fetchInsertions(
|
|
55
71
|
lapisUrl: string,
|
|
56
72
|
body: LapisBaseRequest,
|
|
@@ -163,6 +179,7 @@ const handleErrors = async (response: Response, requestedData: string) => {
|
|
|
163
179
|
};
|
|
164
180
|
|
|
165
181
|
export const aggregatedEndpoint = (lapisUrl: string) => `${lapisUrl}/sample/aggregated`;
|
|
182
|
+
export const detailsEndpoint = (lapisUrl: string) => `${lapisUrl}/sample/details`;
|
|
166
183
|
export const insertionsEndpoint = (lapisUrl: string, sequenceType: SequenceType) => {
|
|
167
184
|
return sequenceType === 'amino acid'
|
|
168
185
|
? `${lapisUrl}/sample/aminoAcidInsertions`
|
|
@@ -41,10 +41,16 @@ const insertionCount = z.object({
|
|
|
41
41
|
});
|
|
42
42
|
export const insertionsResponse = makeLapisResponse(z.array(insertionCount));
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
const baseResponseValueSchema = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
45
|
+
|
|
46
|
+
export const aggregatedItem = z.object({ count: z.number() }).catchall(baseResponseValueSchema);
|
|
45
47
|
export const aggregatedResponse = makeLapisResponse(z.array(aggregatedItem));
|
|
46
48
|
export type AggregatedItem = z.infer<typeof aggregatedItem>;
|
|
47
49
|
|
|
50
|
+
export const detailsItem = z.object({}).catchall(baseResponseValueSchema);
|
|
51
|
+
export const detailsResponse = makeLapisResponse(z.array(detailsItem));
|
|
52
|
+
export type DetailsItem = z.infer<typeof detailsItem>;
|
|
53
|
+
|
|
48
54
|
function makeLapisResponse<T extends ZodTypeAny>(data: T) {
|
|
49
55
|
return z.object({
|
|
50
56
|
data,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Dataset } from './Dataset';
|
|
2
|
+
import { type Operator } from './Operator';
|
|
3
|
+
import { fetchDetails } from '../lapisApi/lapisApi';
|
|
4
|
+
import { type LapisFilter } from '../types';
|
|
5
|
+
|
|
6
|
+
type Details<Fields extends string> = { [field in Fields]: string | number | boolean | null };
|
|
7
|
+
|
|
8
|
+
export class FetchDetailsOperator<Fields extends string> implements Operator<Details<Fields>> {
|
|
9
|
+
constructor(
|
|
10
|
+
private filter: LapisFilter,
|
|
11
|
+
private fields: Fields[] = [],
|
|
12
|
+
) {}
|
|
13
|
+
|
|
14
|
+
async evaluate(lapisUrl: string, signal?: AbortSignal): Promise<Dataset<Details<Fields>>> {
|
|
15
|
+
const detailsResponse = (
|
|
16
|
+
await fetchDetails(
|
|
17
|
+
lapisUrl,
|
|
18
|
+
{
|
|
19
|
+
...this.filter,
|
|
20
|
+
fields: this.fields,
|
|
21
|
+
},
|
|
22
|
+
signal,
|
|
23
|
+
)
|
|
24
|
+
).data;
|
|
25
|
+
|
|
26
|
+
return { content: detailsResponse as Details<Fields>[] };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { useCombobox } from 'downshift/preact';
|
|
2
|
+
import { type ComponentChild } from 'preact';
|
|
3
|
+
import { useRef, useState } from 'preact/hooks';
|
|
4
|
+
|
|
5
|
+
export function DownshiftCombobox<Item>({
|
|
6
|
+
allItems,
|
|
7
|
+
value,
|
|
8
|
+
filterItemsByInputValue,
|
|
9
|
+
createEvent,
|
|
10
|
+
itemToString,
|
|
11
|
+
placeholderText,
|
|
12
|
+
formatItemInList,
|
|
13
|
+
}: {
|
|
14
|
+
allItems: Item[];
|
|
15
|
+
value?: Item;
|
|
16
|
+
filterItemsByInputValue: (item: Item, value: string) => boolean;
|
|
17
|
+
createEvent: (item: Item | null) => CustomEvent;
|
|
18
|
+
itemToString: (item: Item | undefined | null) => string;
|
|
19
|
+
placeholderText?: string;
|
|
20
|
+
formatItemInList: (item: Item) => ComponentChild;
|
|
21
|
+
}) {
|
|
22
|
+
const initialSelectedItem = value ?? null;
|
|
23
|
+
|
|
24
|
+
const [items, setItems] = useState(
|
|
25
|
+
allItems.filter((item) => filterItemsByInputValue(item, itemToString(initialSelectedItem))),
|
|
26
|
+
);
|
|
27
|
+
const divRef = useRef<HTMLDivElement>(null);
|
|
28
|
+
|
|
29
|
+
const shadowRoot = divRef.current?.shadowRoot ?? undefined;
|
|
30
|
+
|
|
31
|
+
const environment =
|
|
32
|
+
shadowRoot !== undefined
|
|
33
|
+
? {
|
|
34
|
+
addEventListener: window.addEventListener.bind(window),
|
|
35
|
+
removeEventListener: window.removeEventListener.bind(window),
|
|
36
|
+
document: shadowRoot.ownerDocument,
|
|
37
|
+
Node: window.Node,
|
|
38
|
+
}
|
|
39
|
+
: undefined;
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
isOpen,
|
|
43
|
+
getToggleButtonProps,
|
|
44
|
+
getMenuProps,
|
|
45
|
+
getInputProps,
|
|
46
|
+
highlightedIndex,
|
|
47
|
+
getItemProps,
|
|
48
|
+
selectedItem,
|
|
49
|
+
inputValue,
|
|
50
|
+
selectItem,
|
|
51
|
+
setInputValue,
|
|
52
|
+
closeMenu,
|
|
53
|
+
} = useCombobox({
|
|
54
|
+
onInputValueChange({ inputValue }) {
|
|
55
|
+
setItems(allItems.filter((item) => filterItemsByInputValue(item, inputValue)));
|
|
56
|
+
},
|
|
57
|
+
onSelectedItemChange({ selectedItem }) {
|
|
58
|
+
if (selectedItem !== null) {
|
|
59
|
+
divRef.current?.dispatchEvent(createEvent(selectedItem));
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
items,
|
|
63
|
+
itemToString(item) {
|
|
64
|
+
return itemToString(item);
|
|
65
|
+
},
|
|
66
|
+
initialSelectedItem,
|
|
67
|
+
environment,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const onInputBlur = () => {
|
|
71
|
+
if (inputValue === '') {
|
|
72
|
+
divRef.current?.dispatchEvent(createEvent(null));
|
|
73
|
+
selectItem(null);
|
|
74
|
+
} else if (inputValue !== itemToString(selectedItem)) {
|
|
75
|
+
setInputValue(itemToString(selectedItem) || '');
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const clearInput = () => {
|
|
80
|
+
divRef.current?.dispatchEvent(createEvent(null));
|
|
81
|
+
selectItem(null);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const buttonRef = useRef(null);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div ref={divRef} className={'relative w-full'}>
|
|
88
|
+
<div className='w-full flex flex-col gap-1'>
|
|
89
|
+
<div
|
|
90
|
+
className='flex gap-0.5 input input-bordered min-w-32'
|
|
91
|
+
onBlur={(event) => {
|
|
92
|
+
if (event.relatedTarget != buttonRef.current) {
|
|
93
|
+
closeMenu();
|
|
94
|
+
}
|
|
95
|
+
}}
|
|
96
|
+
>
|
|
97
|
+
<input
|
|
98
|
+
placeholder={placeholderText}
|
|
99
|
+
className='w-full p-1.5'
|
|
100
|
+
{...getInputProps()}
|
|
101
|
+
onBlur={onInputBlur}
|
|
102
|
+
/>
|
|
103
|
+
<button
|
|
104
|
+
aria-label='clear selection'
|
|
105
|
+
className={`px-2 ${inputValue === '' && 'hidden'}`}
|
|
106
|
+
type='button'
|
|
107
|
+
onClick={clearInput}
|
|
108
|
+
tabIndex={-1}
|
|
109
|
+
>
|
|
110
|
+
×
|
|
111
|
+
</button>
|
|
112
|
+
<button
|
|
113
|
+
aria-label='toggle menu'
|
|
114
|
+
className='px-2'
|
|
115
|
+
type='button'
|
|
116
|
+
{...getToggleButtonProps()}
|
|
117
|
+
ref={buttonRef}
|
|
118
|
+
>
|
|
119
|
+
{isOpen ? <>↑</> : <>↓</>}
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
{isOpen && (
|
|
124
|
+
<ul
|
|
125
|
+
className='absolute bg-white mt-1 shadow-md max-h-80 overflow-scroll z-10 w-full min-w-32'
|
|
126
|
+
{...getMenuProps()}
|
|
127
|
+
>
|
|
128
|
+
{items.length > 0 ? (
|
|
129
|
+
items.map((item, index) => (
|
|
130
|
+
<li
|
|
131
|
+
className={`${highlightedIndex === index ? 'bg-blue-300' : ''} ${selectedItem !== null && itemToString(selectedItem) === itemToString(item) ? 'font-bold' : ''} py-2 px-3 shadow-sm flex flex-col`}
|
|
132
|
+
key={itemToString(item)}
|
|
133
|
+
{...getItemProps({ item, index })}
|
|
134
|
+
>
|
|
135
|
+
{formatItemInList(item)}
|
|
136
|
+
</li>
|
|
137
|
+
))
|
|
138
|
+
) : (
|
|
139
|
+
<li className='py-2 px-3 shadow-sm flex flex-col'>No elements to select.</li>
|
|
140
|
+
)}
|
|
141
|
+
</ul>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -16,7 +16,7 @@ const Tabs: FunctionComponent<ComponentTabsProps> = ({ tabs, toolbar }) => {
|
|
|
16
16
|
const [activeTab, setActiveTab] = useState(tabs[0]?.title);
|
|
17
17
|
|
|
18
18
|
const tabElements = (
|
|
19
|
-
<div className='flex flex-row'>
|
|
19
|
+
<div className='flex flex-row flex-wrap'>
|
|
20
20
|
{tabs.map((tab) => {
|
|
21
21
|
return (
|
|
22
22
|
<button
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type LapisLineageFilter = Record<string, string | undefined>;
|
|
2
|
+
|
|
3
|
+
export class LineageFilterChangedEvent extends CustomEvent<LapisLineageFilter> {
|
|
4
|
+
constructor(detail: LapisLineageFilter) {
|
|
5
|
+
super('gs-lineage-filter-changed', {
|
|
6
|
+
detail,
|
|
7
|
+
bubbles: true,
|
|
8
|
+
composed: true,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -5,9 +5,23 @@ import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../../vitest.setup';
|
|
|
5
5
|
|
|
6
6
|
describe('fetchLineageAutocompleteList', () => {
|
|
7
7
|
test('should add sublineage values', async () => {
|
|
8
|
-
lapisRequestMocks.aggregated(
|
|
8
|
+
lapisRequestMocks.aggregated(
|
|
9
|
+
{ fields: ['lineageField'], country: 'Germany' },
|
|
10
|
+
{
|
|
11
|
+
data: [
|
|
12
|
+
{
|
|
13
|
+
lineageField: 'B.1.1.7',
|
|
14
|
+
count: 1,
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
);
|
|
9
19
|
|
|
10
|
-
const result = await fetchLineageAutocompleteList(
|
|
20
|
+
const result = await fetchLineageAutocompleteList({
|
|
21
|
+
lapis: DUMMY_LAPIS_URL,
|
|
22
|
+
field: 'lineageField',
|
|
23
|
+
lapisFilter: { country: 'Germany' },
|
|
24
|
+
});
|
|
11
25
|
|
|
12
26
|
expect(result).to.deep.equal(['B.1.1.7', 'B.1.1.7*']);
|
|
13
27
|
});
|
|
@@ -1,7 +1,18 @@
|
|
|
1
1
|
import { FetchAggregatedOperator } from '../../operator/FetchAggregatedOperator';
|
|
2
|
+
import type { LapisFilter } from '../../types';
|
|
2
3
|
|
|
3
|
-
export async function fetchLineageAutocompleteList(
|
|
4
|
-
|
|
4
|
+
export async function fetchLineageAutocompleteList({
|
|
5
|
+
lapis,
|
|
6
|
+
field,
|
|
7
|
+
signal,
|
|
8
|
+
lapisFilter,
|
|
9
|
+
}: {
|
|
10
|
+
lapis: string;
|
|
11
|
+
field: string;
|
|
12
|
+
lapisFilter?: LapisFilter;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}) {
|
|
15
|
+
const fetchAggregatedOperator = new FetchAggregatedOperator<Record<string, string>>(lapisFilter ?? {}, [field]);
|
|
5
16
|
|
|
6
17
|
const data = (await fetchAggregatedOperator.evaluate(lapis, signal)).content;
|
|
7
18
|
|