Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ import {
*/
export function WasapPageStateSelector({
config,
resistanceSetNames,
pageStateHandler,
initialBaseFilterState,
initialAnalysisFilterState,
setPageState,
}: {
config: WasapPageConfig;
resistanceSetNames: string[];
pageStateHandler: PageStateHandler<WasapFilter>;
initialBaseFilterState: WasapBaseFilter;
initialAnalysisFilterState: WasapAnalysisFilter;
Expand Down Expand Up @@ -189,7 +191,7 @@ export function WasapPageStateSelector({
<ResistanceMutationsFilter
pageState={resistanceFilter}
setPageState={setResistanceFilter}
resistanceMutationSets={config.resistanceMutationSets}
resistanceSetNames={resistanceSetNames}
/>
);
case 'untracked':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { render } from 'vitest-browser-react';

import { ResistanceMutationsFilter } from './ResistanceMutationsFilter';
import { it } from '../../../../../test-extend';
import { covidResistanceMutations } from '../../../views/wasap/resistanceMutations';
import type { WasapResistanceFilter } from '../../../views/wasap/wasapPageConfig';

describe('ResistanceMutationsFilter', () => {
Expand All @@ -13,7 +12,7 @@ describe('ResistanceMutationsFilter', () => {
resistanceSet: '3CLpro',
};

const resistanceMutationSets = covidResistanceMutations;
const resistanceSetNames = ['3CLpro', 'RdRp', 'Spike'];

it('renders with initial resistance set', async () => {
const mockSetPageState = vi.fn();
Expand All @@ -22,7 +21,7 @@ describe('ResistanceMutationsFilter', () => {
<ResistanceMutationsFilter
pageState={defaultPageState}
setPageState={mockSetPageState}
resistanceMutationSets={resistanceMutationSets}
resistanceSetNames={resistanceSetNames}
/>,
);

Expand All @@ -37,7 +36,7 @@ describe('ResistanceMutationsFilter', () => {
<ResistanceMutationsFilter
pageState={defaultPageState}
setPageState={mockSetPageState}
resistanceMutationSets={resistanceMutationSets}
resistanceSetNames={resistanceSetNames}
/>,
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import type { ResistanceMutationSet } from '../../../views/wasap/resistanceMutations';
import type { WasapResistanceFilter } from '../../../views/wasap/wasapPageConfig';
import { LabeledField } from '../utils/LabeledField';

export function ResistanceMutationsFilter({
pageState,
setPageState,
resistanceMutationSets,
resistanceSetNames,
}: {
pageState: WasapResistanceFilter;
setPageState: (newState: WasapResistanceFilter) => void;
resistanceMutationSets: ResistanceMutationSet[];
resistanceSetNames: string[];
}) {
return (
<LabeledField label='Resistance mutation set'>
Expand All @@ -18,9 +17,9 @@ export function ResistanceMutationsFilter({
value={pageState.resistanceSet}
onChange={(e) => setPageState({ ...pageState, resistanceSet: e.target.value })}
>
{resistanceMutationSets.map((set) => (
<option key={set.name} value={set.name}>
{set.name}
{resistanceSetNames.map((name) => (
<option key={name} value={name}>
{name}
</option>
))}
</select>
Expand Down
26 changes: 22 additions & 4 deletions website/src/components/views/wasap/Wasap.astro
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
---
import { WasapPage } from './WasapPage';
import { fetchResistanceData, type ResistanceData } from './resistanceData';
import { BackendService } from '../../../backendApi/backendService.ts';
import { isStaging, getBackendHost } from '../../../config.ts';
import BaseLayout from '../../../layouts/base/BaseLayout.astro';
import { wastewaterOrganismConfigs, type WastewaterOrganismName } from '../../../types/wastewaterConfig';
import { getInstanceLogger } from '../../../logger.ts';
import {
wastewaterOrganismConfigs,
wastewaterOrganismStagingConfigs,
type WastewaterOrganismName,
} from '../../../types/wastewaterConfig';

type Props = {
wastewaterOrganism: WastewaterOrganismName;
};

const { wastewaterOrganism } = Astro.props;
const { name } = wastewaterOrganismConfigs[wastewaterOrganism];
const config = (isStaging() ? wastewaterOrganismStagingConfigs : wastewaterOrganismConfigs)[wastewaterOrganism];
const backendService = new BackendService(getBackendHost());
let resistanceData: ResistanceData = { mutationAnnotations: [], displayMutationsBySet: {} };

try {
resistanceData = await fetchResistanceData(config, backendService);
} catch (error) {
Comment on lines 14 to +25
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI: If fetchResistanceData fails, the page renders with empty resistance data — the resistance mode dropdown will show zero options, but the user can still select the "resistance" tab and see an empty dropdown. Should we show some kind of user-facing warning in this case, or hide the resistance mode tab entirely when the data is unavailable?

getInstanceLogger('WasapPage').error(
`Failed to fetch resistance data for WASAP page (organism: ${wastewaterOrganism}): ${error}`,
);
Comment on lines +25 to +28
}
---

<BaseLayout title={`Swiss wastewater - ${name}`}>
<WasapPage wastewaterOrganism={wastewaterOrganism} client:only='react' />
<BaseLayout title={`Swiss wastewater - ${config.name}`}>
<WasapPage config={config} resistanceData={resistanceData} client:only='react' />
</BaseLayout>
31 changes: 10 additions & 21 deletions website/src/components/views/wasap/WasapPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,14 @@ import { CollectionInfo } from './components/CollectionInfo';
import { NoDataHelperText } from './components/NoDataHelperText';
import { VariantFetchInfo } from './components/VariantFetchInfo';
import { WasapStats } from './components/WasapStats';
import { toMutationAnnotations } from './resistanceMutations';
import type { ResistanceData } from './resistanceData';
import { useWasapPageData } from './useWasapPageData';
import type { WasapPageConfig } from './wasapPageConfig';
import { withQueryProvider } from '../../../backendApi/withQueryProvider';
import { defaultBreadcrumbs } from '../../../layouts/Breadcrumbs.tsx';
import { DataPageLayout } from '../../../layouts/OrganismPage/DataPageLayout.tsx';
import { dataOrigins } from '../../../types/dataOrigins.ts';
import {
wastewaterBreadcrumb,
wastewaterOrganismConfigs,
type WastewaterOrganismName,
} from '../../../types/wastewaterConfig';
import { wastewaterBreadcrumb } from '../../../types/wastewaterConfig';
import { Loading } from '../../../util/Loading';
import { WasapPageStateHandler } from '../../../views/pageStateHandlers/WasapPageStateHandler';
import { GsMutationsOverTime } from '../../genspectrum/GsMutationsOverTime';
Expand All @@ -25,11 +22,11 @@ import { WasapPageStateSelector } from '../../pageStateSelectors/wasap/WasapPage
import { usePageState } from '../usePageState.ts';

export type WasapPageProps = {
wastewaterOrganism: WastewaterOrganismName;
config: WasapPageConfig;
resistanceData: ResistanceData;
};

export const WasapPageInner: FC<WasapPageProps> = ({ wastewaterOrganism }) => {
const config = wastewaterOrganismConfigs[wastewaterOrganism];
export const WasapPageInner: FC<WasapPageProps> = ({ config, resistanceData }) => {
// initialize page state from the URL
const pageStateHandler = useMemo(() => new WasapPageStateHandler(config), [config]);

Expand All @@ -38,8 +35,9 @@ export const WasapPageInner: FC<WasapPageProps> = ({ wastewaterOrganism }) => {
setPageState,
} = usePageState(pageStateHandler);

const { mutationAnnotations, displayMutationsBySet } = resistanceData;
// fetch which mutations should be analyzed
const { data, isPending, isError } = useWasapPageData(config, analysis);
const { data, isPending, isError } = useWasapPageData(config, displayMutationsBySet, analysis);

let initialMeanProportionInterval: MeanProportionInterval = { min: 0.0, max: 1.0 };
if (analysis.mode === 'manual' && analysis.mutations === undefined) {
Expand All @@ -52,16 +50,6 @@ export const WasapPageInner: FC<WasapPageProps> = ({ wastewaterOrganism }) => {
...(base.samplingDate?.dateTo && { samplingDateTo: base.samplingDate.dateTo }),
};

const memoizedMutationAnnotations = useMemo(
() =>
config.resistanceAnalysisModeEnabled
? config.resistanceMutationSets.flatMap((resistanceMutation) =>
toMutationAnnotations(resistanceMutation),
)
: [],
[config],
);

return (
<DataPageLayout
breadcrumbs={[
Expand All @@ -77,7 +65,7 @@ export const WasapPageInner: FC<WasapPageProps> = ({ wastewaterOrganism }) => {
>
<gs-app
lapis={config.lapisBaseUrl}
mutationAnnotations={memoizedMutationAnnotations}
mutationAnnotations={mutationAnnotations}
mutationLinkTemplate={config.linkTemplate}
>
<div className='grid-cols-[300px_1fr] gap-x-4 lg:grid'>
Expand All @@ -88,6 +76,7 @@ export const WasapPageInner: FC<WasapPageProps> = ({ wastewaterOrganism }) => {
initialBaseFilterState={base}
initialAnalysisFilterState={analysis}
setPageState={setPageState}
resistanceSetNames={Object.keys(displayMutationsBySet)}
/>
</div>
{isError ? (
Expand Down
58 changes: 58 additions & 0 deletions website/src/components/views/wasap/resistanceData.ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's maybe add a simple unit test to test the mapping here?

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { MutationAnnotations } from '@genspectrum/dashboard-components/util';

import type { ResistanceMutationCollectionConfig, WasapPageConfig } from './wasapPageConfig';
import type { BackendService } from '../../../backendApi/backendService';
import type { Collection } from '../../../types/Collection';

export type ResistanceData = {
/** Flat list of mutation annotations for the genome viewer. */
mutationAnnotations: MutationAnnotations;
/** Map from set name (e.g. "3CLpro") to the genomic amino acid mutations to display. */
displayMutationsBySet: Record<string, string[]>;
};

export async function fetchResistanceData(
config: WasapPageConfig,
backendService: BackendService,
): Promise<ResistanceData> {
if (!config.resistanceAnalysisModeEnabled) {
return { mutationAnnotations: [], displayMutationsBySet: {} };
}
const collections = await Promise.all(
config.resistanceMutationCollections.map((setConfig) =>
backendService.getCollection({ id: String(setConfig.collectionId) }),
),
);
return buildResistanceData(config.resistanceMutationCollections, collections);
}

function buildResistanceData(
setConfigs: ResistanceMutationCollectionConfig[],
collections: Collection[],
): ResistanceData {
const mutationAnnotations: MutationAnnotations = [];
const displayMutationsBySet: Record<string, string[]> = {};

setConfigs.forEach((setConfig, i) => {
const entries = collections[i].variants.flatMap((variant) => {
if (variant.type !== 'filterObject') return [];
return (variant.filterObject.aminoAcidMutations ?? []).map((aminoAcidMutation) => ({
displayName: variant.name,
aminoAcidMutation,
}));
});

displayMutationsBySet[setConfig.name] = entries.map((e) => e.aminoAcidMutation);

mutationAnnotations.push(
...entries.map(({ displayName, aminoAcidMutation }) => ({
name: displayName,
symbol: setConfig.annotationSymbol,
description: setConfig.description,
aminoAcidMutations: [aminoAcidMutation],
})),
);
});

return { mutationAnnotations, displayMutationsBySet };
}
Loading