From 54c7b61fe44fe794e2d34f75ad62fb86ebe40e36 Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Wed, 27 May 2026 10:57:17 +0200 Subject: [PATCH 01/31] Mock the UI --- .../wasap/filters/VariantExplorerFilter.tsx | 187 ++++++++++++------ 1 file changed, 131 insertions(+), 56 deletions(-) diff --git a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx index 635f01fb7..dd4fc560f 100644 --- a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx +++ b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx @@ -1,3 +1,5 @@ +import { useState } from 'react'; + import { Inset } from '../../../../styles/Inset'; import { GsLineageFilter } from '../../../genspectrum/GsLineageFilter'; import { @@ -12,6 +14,8 @@ import { LabeledField } from '../utils/LabeledField'; import { NumericInput } from '../utils/NumericInput'; import { SequenceTypeSelector } from '../utils/SequenceTypeSelector'; +type SignatureType = 'computed' | 'nextclade'; + interface VariantExplorerFilterProps { pageState: WasapVariantFilter; setPageState: (newState: WasapVariantFilter) => void; @@ -29,68 +33,139 @@ export function VariantExplorerFilter({ clinicalSequenceLapisBaseUrl, clinicalSequenceLapisLineageField, }: VariantExplorerFilterProps) { + const [signatureType, setSignatureType] = useState('computed'); + return ( <> setPageState({ ...pageState, sequenceType })} /> - - }>Define Clinical Signature - - - { - setPageState({ ...pageState, variant: lineages[clinicalSequenceLapisLineageField] }); - }} - hideCounts={true} - /> - - - setPageState({ ...pageState, minProportion: v })} - /> - setPageState({ ...pageState, minCount: Math.round(v) })} - /> - setPageState({ ...pageState, minJaccard: v })} - /> - - - - + + + + {signatureType === 'nextclade' && } + {signatureType === 'computed' && ( + + }> + Define Clinical Signature + + + + { + setPageState({ + ...pageState, + variant: lineages[clinicalSequenceLapisLineageField], + }); + }} + hideCounts={true} + /> + + + setPageState({ ...pageState, minProportion: v })} + /> + setPageState({ ...pageState, minCount: Math.round(v) })} + /> + setPageState({ ...pageState, minJaccard: v })} + /> + + + + + )} ); } + +const DUMMY_NEXTCLADE_LINEAGES = [ + 'XEC', + 'KP.1.1', + 'KP.2', + 'KP.3', + 'JN.1', + 'XFG', + 'LP.8.1', + 'NB.1.8.1', + 'MC.10.1', + 'XEC.4', +]; + +function NextcladeSignature() { + const [variant, setVariant] = useState(''); + const [newMutationsOnly, setNewMutationsOnly] = useState(false); + + return ( + + + + +
+ setNewMutationsOnly(e.target.checked)} + /> + +
+
+ ); +} From 69d9f0f9e29803995a14114cf46c11cc3f65cd54 Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Fri, 29 May 2026 09:19:46 +0200 Subject: [PATCH 02/31] fix(website): resolve merge conflict in backendService getCollections Co-Authored-By: Claude Sonnet 4.6 --- website/src/backendApi/backendService.ts | 3 +- .../wasap/WasapPageStateSelector.tsx | 23 ++++++++ .../wasap/filters/VariantExplorerFilter.tsx | 59 ++++++++++--------- .../components/views/wasap/wasapPageConfig.ts | 4 ++ website/src/types/wastewaterConfig.ts | 4 ++ 5 files changed, 63 insertions(+), 30 deletions(-) diff --git a/website/src/backendApi/backendService.ts b/website/src/backendApi/backendService.ts index 8019e6d9d..e6219cfd9 100644 --- a/website/src/backendApi/backendService.ts +++ b/website/src/backendApi/backendService.ts @@ -181,9 +181,10 @@ export class BackendService extends ApiService { return this.get({ url: '/collections', requestParams, schema: z.array(collectionSummarySchema) }); } - public async getCollections({ organism }: { organism?: string } = {}) { + public async getCollections({ organism, userId }: { organism?: string; userId?: number } = {}) { const requestParams: Record = { includeVariants: 'true' }; if (organism !== undefined) requestParams.organism = organism; + if (userId !== undefined) requestParams.userId = String(userId); return this.get({ url: '/collections', requestParams, schema: z.array(collectionSchema) }); } diff --git a/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx b/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx index 5f5c483ff..62e9f29de 100644 --- a/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx +++ b/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx @@ -1,6 +1,8 @@ import { useQuery } from '@tanstack/react-query'; import { type Dispatch, type SetStateAction, useState } from 'react'; +import { getBackendServiceForClientside } from '../../../backendApi/backendService'; + import { ApplyFilterButton } from '../ApplyFilterButton'; import { DynamicDateFilter } from '../DynamicDateFilter'; import { SelectorHeadline } from '../SelectorHeadline'; @@ -101,6 +103,22 @@ export function WasapPageStateSelector({ }, }); + const predefinedVariantsQueryResult = useQuery({ + enabled: config.variantAnalysisModeEnabled && config.predefinedVariantsSource !== undefined, + queryKey: ['predefinedVariants', config.variantAnalysisModeEnabled && config.predefinedVariantsSource], + queryFn: () => { + if (!config.variantAnalysisModeEnabled || config.predefinedVariantsSource === undefined) { + throw Error( + "This predefined variants query was called despite it being disabled. This should not happen.", + ); + } + const { collectionsUserId, collectionsTag } = config.predefinedVariantsSource; + return getBackendServiceForClientside() + .getCollections({ userId: collectionsUserId, organism: config.name }) + .then((collections) => collections.filter((c) => c.description?.includes(collectionsTag) ?? false)); + }, + }); + return (
Filter dataset @@ -181,6 +199,11 @@ export function WasapPageStateSelector({ setPageState={setVariantFilter} clinicalSequenceLapisBaseUrl={config.clinicalLapis.lapisBaseUrl} clinicalSequenceLapisLineageField={config.clinicalLapis.lineageField} + predefinedVariantsQueryResult={ + config.predefinedVariantsSource !== undefined + ? predefinedVariantsQueryResult + : undefined + } /> ); case 'resistance': diff --git a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx index dd4fc560f..b0dc31cdd 100644 --- a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx +++ b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx @@ -1,5 +1,7 @@ +import { type UseQueryResult } from '@tanstack/react-query'; import { useState } from 'react'; +import { type Collection } from '../../../../types/Collection'; import { Inset } from '../../../../styles/Inset'; import { GsLineageFilter } from '../../../genspectrum/GsLineageFilter'; import { @@ -25,6 +27,7 @@ interface VariantExplorerFilterProps { */ clinicalSequenceLapisBaseUrl: string; clinicalSequenceLapisLineageField: string; + predefinedVariantsQueryResult?: UseQueryResult; } export function VariantExplorerFilter({ @@ -32,6 +35,7 @@ export function VariantExplorerFilter({ setPageState, clinicalSequenceLapisBaseUrl, clinicalSequenceLapisLineageField, + predefinedVariantsQueryResult, }: VariantExplorerFilterProps) { const [signatureType, setSignatureType] = useState('computed'); @@ -41,17 +45,21 @@ export function VariantExplorerFilter({ value={pageState.sequenceType} onChange={(sequenceType) => setPageState({ ...pageState, sequenceType })} /> - - - - {signatureType === 'nextclade' && } + {predefinedVariantsQueryResult !== undefined && ( + + + + )} + {signatureType === 'nextclade' && ( + + )} {signatureType === 'computed' && ( }> @@ -123,23 +131,16 @@ export function VariantExplorerFilter({ ); } -const DUMMY_NEXTCLADE_LINEAGES = [ - 'XEC', - 'KP.1.1', - 'KP.2', - 'KP.3', - 'JN.1', - 'XFG', - 'LP.8.1', - 'NB.1.8.1', - 'MC.10.1', - 'XEC.4', -]; - -function NextcladeSignature() { +function NextcladeSignature({ + predefinedVariantsQueryResult, +}: { + predefinedVariantsQueryResult: UseQueryResult | undefined; +}) { const [variant, setVariant] = useState(''); const [newMutationsOnly, setNewMutationsOnly] = useState(false); + const collections = predefinedVariantsQueryResult?.data ?? []; + return ( @@ -147,9 +148,9 @@ function NextcladeSignature() { - {DUMMY_NEXTCLADE_LINEAGES.map((lineage) => ( - ))} @@ -163,7 +164,7 @@ function NextcladeSignature() { onChange={(e) => setNewMutationsOnly(e.target.checked)} />
diff --git a/website/src/components/views/wasap/wasapPageConfig.ts b/website/src/components/views/wasap/wasapPageConfig.ts index 2c4cbfe69..e27250915 100644 --- a/website/src/components/views/wasap/wasapPageConfig.ts +++ b/website/src/components/views/wasap/wasapPageConfig.ts @@ -70,6 +70,10 @@ type VariantAnalysisModeConfig = } | { variantAnalysisModeEnabled: true; + predefinedVariantsSource?: { + collectionsUserId: number; + collectionsTag: string; + }; clinicalLapis: { lapisBaseUrl: string; dateField: string; diff --git a/website/src/types/wastewaterConfig.ts b/website/src/types/wastewaterConfig.ts index 8e10fa1d5..2f94a369e 100644 --- a/website/src/types/wastewaterConfig.ts +++ b/website/src/types/wastewaterConfig.ts @@ -56,6 +56,10 @@ export const wastewaterOrganismConfigs: Record Date: Fri, 29 May 2026 09:42:17 +0200 Subject: [PATCH 03/31] feat(website): use getCollectionSummaries with userId/organism for predefined variants Co-Authored-By: Claude Sonnet 4.6 --- website/src/backendApi/backendService.ts | 15 +++++++++++---- .../wasap/WasapPageStateSelector.tsx | 4 ++-- .../wasap/filters/VariantExplorerFilter.tsx | 6 +++--- .../src/components/views/wasap/wasapPageConfig.ts | 2 ++ website/src/types/wastewaterConfig.ts | 3 ++- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/website/src/backendApi/backendService.ts b/website/src/backendApi/backendService.ts index e6219cfd9..944dc4d23 100644 --- a/website/src/backendApi/backendService.ts +++ b/website/src/backendApi/backendService.ts @@ -177,14 +177,21 @@ export class BackendService extends ApiService { return this.get({ url: `/users/${id}`, schema: publicUserSchema }); } - public async getCollectionSummaries(requestParams: { organism?: string; excludeSystemCollections?: boolean } = {}) { - return this.get({ url: '/collections', requestParams, schema: z.array(collectionSummarySchema) }); + public async getCollectionSummaries({ organism, userId, excludeSystemCollections }: { organism?: string; userId?: number; excludeSystemCollections?: boolean } = {}) { + const requestParams: Record = {}; + if (organism !== undefined) requestParams.organism = organism; + if (userId !== undefined) requestParams.userId = String(userId); + if (excludeSystemCollections !== undefined) requestParams.excludeSystemCollections = String(excludeSystemCollections); + return this.get({ + url: '/collections', + requestParams: Object.keys(requestParams).length > 0 ? requestParams : undefined, + schema: z.array(collectionSummarySchema), + }); } - public async getCollections({ organism, userId }: { organism?: string; userId?: number } = {}) { + public async getCollections({ organism }: { organism?: string } = {}) { const requestParams: Record = { includeVariants: 'true' }; if (organism !== undefined) requestParams.organism = organism; - if (userId !== undefined) requestParams.userId = String(userId); return this.get({ url: '/collections', requestParams, schema: z.array(collectionSchema) }); } diff --git a/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx b/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx index 62e9f29de..d5e3183bd 100644 --- a/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx +++ b/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx @@ -112,9 +112,9 @@ export function WasapPageStateSelector({ "This predefined variants query was called despite it being disabled. This should not happen.", ); } - const { collectionsUserId, collectionsTag } = config.predefinedVariantsSource; + const { collectionsUserId, collectionsOrganism, collectionsTag } = config.predefinedVariantsSource; return getBackendServiceForClientside() - .getCollections({ userId: collectionsUserId, organism: config.name }) + .getCollectionSummaries({ userId: collectionsUserId, organism: collectionsOrganism }) .then((collections) => collections.filter((c) => c.description?.includes(collectionsTag) ?? false)); }, }); diff --git a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx index b0dc31cdd..b1581ddf5 100644 --- a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx +++ b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx @@ -1,7 +1,7 @@ import { type UseQueryResult } from '@tanstack/react-query'; import { useState } from 'react'; -import { type Collection } from '../../../../types/Collection'; +import { type CollectionSummary } from '../../../../types/Collection'; import { Inset } from '../../../../styles/Inset'; import { GsLineageFilter } from '../../../genspectrum/GsLineageFilter'; import { @@ -27,7 +27,7 @@ interface VariantExplorerFilterProps { */ clinicalSequenceLapisBaseUrl: string; clinicalSequenceLapisLineageField: string; - predefinedVariantsQueryResult?: UseQueryResult; + predefinedVariantsQueryResult?: UseQueryResult; } export function VariantExplorerFilter({ @@ -134,7 +134,7 @@ export function VariantExplorerFilter({ function NextcladeSignature({ predefinedVariantsQueryResult, }: { - predefinedVariantsQueryResult: UseQueryResult | undefined; + predefinedVariantsQueryResult: UseQueryResult | undefined; }) { const [variant, setVariant] = useState(''); const [newMutationsOnly, setNewMutationsOnly] = useState(false); diff --git a/website/src/components/views/wasap/wasapPageConfig.ts b/website/src/components/views/wasap/wasapPageConfig.ts index e27250915..1c518f04d 100644 --- a/website/src/components/views/wasap/wasapPageConfig.ts +++ b/website/src/components/views/wasap/wasapPageConfig.ts @@ -72,6 +72,8 @@ type VariantAnalysisModeConfig = variantAnalysisModeEnabled: true; predefinedVariantsSource?: { collectionsUserId: number; + // TODO: find a better source for this — the organism key should already be known from the page config + collectionsOrganism: string; collectionsTag: string; }; clinicalLapis: { diff --git a/website/src/types/wastewaterConfig.ts b/website/src/types/wastewaterConfig.ts index 2f94a369e..ac08c1a71 100644 --- a/website/src/types/wastewaterConfig.ts +++ b/website/src/types/wastewaterConfig.ts @@ -58,7 +58,8 @@ export const wastewaterOrganismConfigs: Record Date: Fri, 29 May 2026 10:29:28 +0200 Subject: [PATCH 04/31] feat(website): replace variant select with downshift autocomplete combobox Co-Authored-By: Claude Sonnet 4.6 --- website/package-lock.json | 14 ++- website/package.json | 1 + .../wasap/filters/VariantExplorerFilter.tsx | 14 +-- .../wasap/utils/CollectionCombobox.tsx | 103 ++++++++++++++++++ 4 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 website/src/components/pageStateSelectors/wasap/utils/CollectionCombobox.tsx diff --git a/website/package-lock.json b/website/package-lock.json index af0218637..9db2ea4ba 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -16,6 +16,7 @@ "better-auth": "^1.6.14", "cookie": "^1.1.1", "dayjs": "^1.11.21", + "downshift": "^9.3.3", "katex": "^0.17.0", "patch-package": "^8.0.1", "react": "^19.2.7", @@ -6495,9 +6496,9 @@ } }, "node_modules/downshift": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/downshift/-/downshift-9.3.2.tgz", - "integrity": "sha512-5VD0WZLQDhipWiDU+K5ili3VDhGrXwlvOlSaSG1Cb0eS4XpssxVuoD09JNgju+bAzxB2Wvlwx+FwTE/FNdrqow==", + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/downshift/-/downshift-9.3.3.tgz", + "integrity": "sha512-otjI/WtoFcIXwPOyIQYkbrAZJA2Gjz67F8ENOh1RvmJalOg6fk0KEx4ljJSFuJB54ryc6doymPkCM+GjAZ7zjA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.28.6", @@ -8225,6 +8226,13 @@ "set-cookie-parser": "^3.0.1" } }, + "node_modules/headers-polyfill/node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "devOptional": true, + "license": "MIT" + }, "node_modules/html-escaper": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", diff --git a/website/package.json b/website/package.json index 2dba7e144..8a0a94ab2 100644 --- a/website/package.json +++ b/website/package.json @@ -33,6 +33,7 @@ "cookie": "^1.1.1", "dayjs": "^1.11.21", "katex": "^0.17.0", + "downshift": "^9.3.3", "patch-package": "^8.0.1", "react": "^19.2.7", "react-dom": "^19.2.7", diff --git a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx index b1581ddf5..b94423951 100644 --- a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx +++ b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx @@ -2,6 +2,7 @@ import { type UseQueryResult } from '@tanstack/react-query'; import { useState } from 'react'; import { type CollectionSummary } from '../../../../types/Collection'; +import { CollectionCombobox } from '../utils/CollectionCombobox'; import { Inset } from '../../../../styles/Inset'; import { GsLineageFilter } from '../../../genspectrum/GsLineageFilter'; import { @@ -136,7 +137,7 @@ function NextcladeSignature({ }: { predefinedVariantsQueryResult: UseQueryResult | undefined; }) { - const [variant, setVariant] = useState(''); + const [selectedCollection, setSelectedCollection] = useState(null); const [newMutationsOnly, setNewMutationsOnly] = useState(false); const collections = predefinedVariantsQueryResult?.data ?? []; @@ -144,16 +145,7 @@ function NextcladeSignature({ return ( - +
void; + placeholderText?: string; +}) { + const [inputFilter, setInputFilter] = useState(() => value?.name ?? ''); + const [inputIsInvalid, setInputIsInvalid] = useState(false); + + const filteredCollections = useMemo( + () => + collections + .filter((c) => c.name.toLowerCase().includes(inputFilter.toLowerCase())) + .sort((a, b) => a.name.localeCompare(b.name)), + [collections, inputFilter], + ); + + const buttonRef = useRef(null); + + const { isOpen, getToggleButtonProps, getMenuProps, getInputProps, highlightedIndex, getItemProps, inputValue, closeMenu, reset } = + useCombobox({ + items: filteredCollections, + itemToString: (item) => item?.name ?? '', + selectedItem: value, + onInputValueChange({ inputValue }) { + setInputIsInvalid(false); + setInputFilter(inputValue.trim()); + }, + onSelectedItemChange({ selectedItem }) { + onChange(selectedItem ?? null); + }, + }); + + const onInputBlur = () => { + if (inputValue === '') { + onChange(null); + return; + } + const match = collections.find((c) => c.name === inputValue.trim()); + if (match !== undefined) { + onChange(match); + return; + } + setInputIsInvalid(true); + }; + + return ( +
+
{ + if (e.relatedTarget !== buttonRef.current) { + closeMenu(); + } + }} + > + + {inputValue !== '' && ( + + )} + +
+
    + {filteredCollections.length > 0 ? ( + filteredCollections.map((item, index) => ( +
  • + {item.name} +
  • + )) + ) : ( +
  • No variants found.
  • + )} +
+
+ ); +} From c01dbae21c0331e6c7f72fec6dbab14517503789 Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Fri, 29 May 2026 11:17:37 +0200 Subject: [PATCH 05/31] refactor(website): flatten WasapVariantFilter type and make SignatureType explicit Replace the WasapVariantComputedFilter | WasapVariantPredefinedFilter discriminated union with a single flat WasapVariantFilter type (consistent with WasapUntrackedFilter), removing the need to pass computedDefaults to VariantExplorerFilter. Also moves SignatureType to wasapPageConfig and makes newMutationsOnly optional (undefined = false). Co-Authored-By: Claude Sonnet 4.6 --- .../wasap/WasapPageStateSelector.tsx | 4 +- .../VariantExplorerFilter.browser.spec.tsx | 1 + .../wasap/filters/VariantExplorerFilter.tsx | 50 +++++++++------ .../wasap/utils/CollectionCombobox.tsx | 63 +++++++++++++------ .../src/components/views/wasap/WasapPage.tsx | 20 +++--- .../views/wasap/useWasapPageData.ts | 17 +++++ .../components/views/wasap/wasapPageConfig.ts | 8 +++ website/src/types/wastewaterConfig.ts | 4 ++ .../WasapPageStateHandler.spec.ts | 1 + .../WasapPageStateHandler.ts | 37 +++++++++-- 10 files changed, 149 insertions(+), 56 deletions(-) diff --git a/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx b/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx index d5e3183bd..8e4665009 100644 --- a/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx +++ b/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx @@ -2,7 +2,6 @@ import { useQuery } from '@tanstack/react-query'; import { type Dispatch, type SetStateAction, useState } from 'react'; import { getBackendServiceForClientside } from '../../../backendApi/backendService'; - import { ApplyFilterButton } from '../ApplyFilterButton'; import { DynamicDateFilter } from '../DynamicDateFilter'; import { SelectorHeadline } from '../SelectorHeadline'; @@ -109,7 +108,7 @@ export function WasapPageStateSelector({ queryFn: () => { if (!config.variantAnalysisModeEnabled || config.predefinedVariantsSource === undefined) { throw Error( - "This predefined variants query was called despite it being disabled. This should not happen.", + 'This predefined variants query was called despite it being disabled. This should not happen.', ); } const { collectionsUserId, collectionsOrganism, collectionsTag } = config.predefinedVariantsSource; @@ -204,6 +203,7 @@ export function WasapPageStateSelector({ ? predefinedVariantsQueryResult : undefined } + predefinedVariantsLabel={config.predefinedVariantsSource?.variantSourceLabel} /> ); case 'resistance': diff --git a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.browser.spec.tsx b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.browser.spec.tsx index dca132918..bf7d94410 100644 --- a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.browser.spec.tsx +++ b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.browser.spec.tsx @@ -12,6 +12,7 @@ const DUMMY_LAPIS_URL_2 = 'http://lapis2.dummy'; describe('VariantExplorerFilter', () => { const defaultPageState: WasapVariantFilter = { mode: 'variant', + signatureType: 'computed', sequenceType: 'nucleotide', variant: undefined, minProportion: 0.05, diff --git a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx index b94423951..d414b4ae9 100644 --- a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx +++ b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx @@ -1,24 +1,22 @@ import { type UseQueryResult } from '@tanstack/react-query'; -import { useState } from 'react'; -import { type CollectionSummary } from '../../../../types/Collection'; -import { CollectionCombobox } from '../utils/CollectionCombobox'; import { Inset } from '../../../../styles/Inset'; +import { type CollectionSummary } from '../../../../types/Collection'; import { GsLineageFilter } from '../../../genspectrum/GsLineageFilter'; import { VARIANT_TIME_FRAME, variantTimeFrameLabel, + type SignatureType, type VariantTimeFrame, type WasapVariantFilter, } from '../../../views/wasap/wasapPageConfig'; import { SelectorHeadline } from '../../SelectorHeadline'; import { DefineClinicalSignatureInfo } from '../InfoBlocks'; +import { CollectionCombobox } from '../utils/CollectionCombobox'; import { LabeledField } from '../utils/LabeledField'; import { NumericInput } from '../utils/NumericInput'; import { SequenceTypeSelector } from '../utils/SequenceTypeSelector'; -type SignatureType = 'computed' | 'nextclade'; - interface VariantExplorerFilterProps { pageState: WasapVariantFilter; setPageState: (newState: WasapVariantFilter) => void; @@ -29,6 +27,7 @@ interface VariantExplorerFilterProps { clinicalSequenceLapisBaseUrl: string; clinicalSequenceLapisLineageField: string; predefinedVariantsQueryResult?: UseQueryResult; + predefinedVariantsLabel?: string; } export function VariantExplorerFilter({ @@ -37,8 +36,11 @@ export function VariantExplorerFilter({ clinicalSequenceLapisBaseUrl, clinicalSequenceLapisLineageField, predefinedVariantsQueryResult, + predefinedVariantsLabel = 'Predefined', }: VariantExplorerFilterProps) { - const [signatureType, setSignatureType] = useState('computed'); + const handleSignatureTypeChange = (newType: SignatureType) => { + setPageState({ ...pageState, signatureType: newType }); + }; return ( <> @@ -50,18 +52,22 @@ export function VariantExplorerFilter({ )} - {signatureType === 'nextclade' && ( - + {pageState.signatureType === 'predefined' && ( + )} - {signatureType === 'computed' && ( + {pageState.signatureType === 'computed' && ( }> Define Clinical Signature @@ -132,28 +138,34 @@ export function VariantExplorerFilter({ ); } -function NextcladeSignature({ +function PredefinedSignature({ + pageState, + setPageState, predefinedVariantsQueryResult, }: { + pageState: WasapVariantFilter; + setPageState: (newState: WasapVariantFilter) => void; predefinedVariantsQueryResult: UseQueryResult | undefined; }) { - const [selectedCollection, setSelectedCollection] = useState(null); - const [newMutationsOnly, setNewMutationsOnly] = useState(false); - const collections = predefinedVariantsQueryResult?.data ?? []; + const selectedCollection = collections.find((c) => c.id === pageState.collectionId) ?? null; return ( - + setPageState({ ...pageState, collectionId: c?.id })} + />
setNewMutationsOnly(e.target.checked)} + checked={pageState.newMutationsOnly ?? false} + onChange={(e) => setPageState({ ...pageState, newMutationsOnly: e.target.checked })} />
); From b6c1893c9d292ef9c1cf3eceea3ef9a72edfdf0f Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Wed, 3 Jun 2026 16:27:29 +0200 Subject: [PATCH 25/31] fix(website): always serialize signatureType in URL, not only for predefined signatures Co-Authored-By: Claude Sonnet 4.6 --- website/src/views/pageStateHandlers/WasapPageStateHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/views/pageStateHandlers/WasapPageStateHandler.ts b/website/src/views/pageStateHandlers/WasapPageStateHandler.ts index 5c05ab533..f4b4569ad 100644 --- a/website/src/views/pageStateHandlers/WasapPageStateHandler.ts +++ b/website/src/views/pageStateHandlers/WasapPageStateHandler.ts @@ -144,8 +144,8 @@ export class WasapPageStateHandler implements PageStateHandler { break; case 'variant': setSearchFromString(search, 'sequenceType', analysis.sequenceType); + setSearchFromString(search, 'signatureType', analysis.signatureType); if (analysis.signatureType === 'predefined') { - setSearchFromString(search, 'signatureType', 'predefined'); setSearchFromString( search, 'collectionId', From 6edc0d427c04e27c669f44fa1b378a38b048ec22 Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Thu, 4 Jun 2026 11:52:31 +0200 Subject: [PATCH 26/31] fix check --- website/src/components/views/wasap/useWasapPageData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/src/components/views/wasap/useWasapPageData.ts b/website/src/components/views/wasap/useWasapPageData.ts index 77db1ff21..e5cad1309 100644 --- a/website/src/components/views/wasap/useWasapPageData.ts +++ b/website/src/components/views/wasap/useWasapPageData.ts @@ -114,7 +114,7 @@ async function fetchVariantComputedModeData( } async function fetchVariantPredefinedModeData(analysis: WasapVariantFilter): Promise { - if (!analysis.collectionId) { + if (!analysis.collectionId === undefined) { throw new Error('No collection selected for predefined variant mode.'); } const collection = await getBackendServiceForClientside().getCollection({ id: String(analysis.collectionId) }); From 69682725d29f6ff5bbce6f6d93a647178d7a9ffc Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Thu, 4 Jun 2026 11:55:11 +0200 Subject: [PATCH 27/31] fm --- .../wasap/filters/VariantExplorerFilter.tsx | 5 ++++- website/src/components/views/wasap/useWasapPageData.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx index 4fdfeae81..3d911f234 100644 --- a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx +++ b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx @@ -167,7 +167,10 @@ function PredefinedSignature({ checked={pageState.newMutationsOnly ?? false} onChange={(e) => setPageState({ ...pageState, newMutationsOnly: e.target.checked })} /> -
+
diff --git a/website/src/components/views/wasap/useWasapPageData.ts b/website/src/components/views/wasap/useWasapPageData.ts index e5cad1309..9d69bde1e 100644 --- a/website/src/components/views/wasap/useWasapPageData.ts +++ b/website/src/components/views/wasap/useWasapPageData.ts @@ -114,7 +114,7 @@ async function fetchVariantComputedModeData( } async function fetchVariantPredefinedModeData(analysis: WasapVariantFilter): Promise { - if (!analysis.collectionId === undefined) { + if (analysis.collectionId === undefined) { throw new Error('No collection selected for predefined variant mode.'); } const collection = await getBackendServiceForClientside().getCollection({ id: String(analysis.collectionId) }); From bd6193e7659d07cb09655a551f06a8092c9acb7c Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Thu, 4 Jun 2026 11:00:01 +0100 Subject: [PATCH 28/31] Update website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx Co-authored-by: Fabian Engelniederhammer <92720311+fengelniederhammer@users.noreply.github.com> --- .../wasap/WasapPageStateSelector.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx b/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx index 1694e173a..f968b786d 100644 --- a/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx +++ b/website/src/components/pageStateSelectors/wasap/WasapPageStateSelector.tsx @@ -105,16 +105,18 @@ export function WasapPageStateSelector({ const predefinedVariantsQueryResult = useQuery({ enabled: config.variantAnalysisModeEnabled && config.predefinedVariantsSource !== undefined, queryKey: ['predefinedVariants', config.variantAnalysisModeEnabled && config.predefinedVariantsSource], - queryFn: () => { + queryFn: async () => { if (!config.variantAnalysisModeEnabled || config.predefinedVariantsSource === undefined) { throw Error( 'This predefined variants query was called despite it being disabled. This should not happen.', ); } const { collectionsUserId, collectionsTag } = config.predefinedVariantsSource; - return getBackendServiceForClientside() - .getCollectionSummaries({ userId: collectionsUserId, organism: config.internalName }) - .then((collections) => collections.filter((c) => c.description?.includes(collectionsTag) ?? false)); + const collections = await getBackendServiceForClientside().getCollectionSummaries({ + userId: collectionsUserId, + organism: config.internalName, + }); + return collections.filter((c) => c.description?.includes(collectionsTag) ?? false); }, }); From 33d1a227e9885ea686888d9922516ec0ac822000 Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Thu, 4 Jun 2026 12:16:24 +0200 Subject: [PATCH 29/31] test fix --- .../src/views/pageStateHandlers/WasapPageStateHandler.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/website/src/views/pageStateHandlers/WasapPageStateHandler.spec.ts b/website/src/views/pageStateHandlers/WasapPageStateHandler.spec.ts index b905aa66e..34b3f5381 100644 --- a/website/src/views/pageStateHandlers/WasapPageStateHandler.spec.ts +++ b/website/src/views/pageStateHandlers/WasapPageStateHandler.spec.ts @@ -220,6 +220,7 @@ describe('WasapPageStateHandler', () => { 'granularity=week&' + 'analysisMode=variant&' + 'sequenceType=nucleotide&' + + 'signatureType=computed&' + 'variant=BA.2*&' + 'minProportion=0.5&' + 'minCount=10&' + From a67a3f8909a058778926fcab2bcbf52c8f1ed43d Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Mon, 8 Jun 2026 09:02:17 +0200 Subject: [PATCH 30/31] useId() --- .../wasap/filters/VariantExplorerFilter.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx index 3d911f234..6739cf1b5 100644 --- a/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx +++ b/website/src/components/pageStateSelectors/wasap/filters/VariantExplorerFilter.tsx @@ -1,4 +1,5 @@ import { type UseQueryResult } from '@tanstack/react-query'; +import { useId } from 'react'; import { Inset } from '../../../../styles/Inset'; import { type CollectionSummary } from '../../../../types/Collection'; @@ -149,6 +150,7 @@ function PredefinedSignature({ }) { const collections = predefinedVariantsQueryResult?.data ?? []; const selectedCollection = collections.find((c) => c.id === pageState.collectionId) ?? null; + const newMutationsCheckBoxId = useId(); return ( @@ -163,7 +165,7 @@ function PredefinedSignature({ setPageState({ ...pageState, newMutationsOnly: e.target.checked })} /> @@ -171,7 +173,7 @@ function PredefinedSignature({ className='tooltip tooltip-right inline' data-tip='Only show mutations that were not observed in the parent variant' > -
From e267af071e4ef0a1302f9b324f5e20258e989179 Mon Sep 17 00:00:00 2001 From: Felix Hennig Date: Mon, 8 Jun 2026 10:30:35 +0200 Subject: [PATCH 31/31] better error msg --- website/src/components/views/wasap/WasapPage.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/website/src/components/views/wasap/WasapPage.tsx b/website/src/components/views/wasap/WasapPage.tsx index 50a1950da..f7de97c3e 100644 --- a/website/src/components/views/wasap/WasapPage.tsx +++ b/website/src/components/views/wasap/WasapPage.tsx @@ -86,7 +86,16 @@ export const WasapPageInner: FC = ({ config, resistanceData }) = />
{isError ? ( - There was an error fetching the data to display. + analysis.mode === 'variant' && + analysis.signatureType === 'predefined' && + analysis.collectionId === undefined ? ( +
+

No variant selected

+

Please select a variant from the filter panel.

+
+ ) : ( + There was an error fetching the data to display. + ) ) : isPending ? ( ) : (