From c98a52a8fd52e8cd1ccda120c1bd926880b3e851 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Jun 2025 14:42:26 +0000 Subject: [PATCH 1/6] chore(website): bump @types/node from 22.15.30 to 24.0.3 in /website (#728) --- website/package-lock.json | 16 ++++++++-------- website/package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/website/package-lock.json b/website/package-lock.json index 73d5de1f3..20ae2f823 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -40,7 +40,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", - "@types/node": "^22.15.30", + "@types/node": "^24.0.3", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/topojson-specification": "^1.0.5", @@ -3389,12 +3389,12 @@ } }, "node_modules/@types/node": { - "version": "22.15.30", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", - "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.8.0" } }, "node_modules/@types/prop-types": { @@ -13058,9 +13058,9 @@ "license": "MIT" }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "license": "MIT" }, "node_modules/unicode-properties": { diff --git a/website/package.json b/website/package.json index cdea3c3ea..5b968a411 100644 --- a/website/package.json +++ b/website/package.json @@ -50,7 +50,7 @@ "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", - "@types/node": "^22.15.30", + "@types/node": "^24.0.3", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/topojson-specification": "^1.0.5", From f988a4e8ee9b5b1781a70194d533b8559473e5d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Jul 2025 08:39:06 +0200 Subject: [PATCH 2/6] chore(github-actions): bump lewagon/wait-on-check-action (#742) Bumps [lewagon/wait-on-check-action](https://github.com/lewagon/wait-on-check-action) from 1.3.4 to 1.4.0. - [Release notes](https://github.com/lewagon/wait-on-check-action/releases) - [Changelog](https://github.com/lewagon/wait-on-check-action/blob/master/CHANGELOG.md) - [Commits](https://github.com/lewagon/wait-on-check-action/compare/v1.3.4...v1.4.0) --- updated-dependencies: - dependency-name: lewagon/wait-on-check-action dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/e2e.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e58ccb3d4..33c1bc5e2 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -37,14 +37,14 @@ jobs: run: echo "branchTag=${TAG##*:}" >> $GITHUB_OUTPUT - name: Wait for website Docker image - uses: lewagon/wait-on-check-action@v1.3.4 + uses: lewagon/wait-on-check-action@v1.4.0 with: ref: ${{ github.sha }} check-name: Build Website Docker Image repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 10 - name: Wait for backend Docker image - uses: lewagon/wait-on-check-action@v1.3.4 + uses: lewagon/wait-on-check-action@v1.4.0 with: ref: ${{ github.sha }} check-name: Build Backend Docker Image From 1217b908957b08f0ba134a6c09cb46bc3882bda1 Mon Sep 17 00:00:00 2001 From: "Anna (Anya) Parker" <50943381+anna-parker@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:06:43 +0200 Subject: [PATCH 3/6] feat(dashboards): add influenza info (#739) * feat(dashboards): add influenza A info * feat(dashboards): add rsv-b information * feat(dashboards): clean up * feat(dashboards): format * Update website/src/components/InfluenzaInformation.astro Co-authored-by: Chaoran Chen * Update website/src/components/InfluenzaInformation.astro Co-authored-by: Chaoran Chen * Update website/src/components/InfluenzaInformation.astro Co-authored-by: Chaoran Chen * feat(dashboards): format --------- Co-authored-by: Chaoran Chen --- .../src/components/InfluenzaInformation.astro | 114 ++++++++++++++++++ .../views/overview/LinkedOverview.astro | 4 + .../src/pages/influenza-a/h1n1pdm/index.astro | 23 +++- .../src/pages/influenza-a/h3n2/index.astro | 26 +++- .../src/pages/influenza-a/h5n1/index.astro | 24 +++- website/src/pages/influenza-a/index.astro | 21 +++- website/src/pages/influenza-b/index.astro | 25 +++- .../pages/influenza-b/victoria/index.astro | 17 ++- 8 files changed, 244 insertions(+), 10 deletions(-) create mode 100644 website/src/components/InfluenzaInformation.astro diff --git a/website/src/components/InfluenzaInformation.astro b/website/src/components/InfluenzaInformation.astro new file mode 100644 index 000000000..131e5d075 --- /dev/null +++ b/website/src/components/InfluenzaInformation.astro @@ -0,0 +1,114 @@ +--- +import Link from './Link.astro'; + +interface Props { + type: string; +} + +const { type } = Astro.props; +--- + +
+
+ How We Process Influenza Data + { + type === 'A' && ( +

+ Genspectrum uses all open influenza A data that is available on the INSDC (taxonid: 197911). To + classify influenza segments and subtypes we use nextclade sort (using half of all k-mers for each + subtype defined in{' '} + + https://github.com/anna-parker/InfluenzaAReferenceDB + + ) to improve classification). Where available we use the assembly information to group segments that + are from the same sample/isolate. For all remaining segments we use a heuristic grouping algorithm + to group all segments from the same sample/isolate using the metadata available from each segment. +

+ ) + } + { + type === 'B' && ( +

+ Genspectrum uses all open influenza B data that is available on the INSDC (taxonid: 11520). To + classify influenza segments and lineages we use nextclade sort (using half off all k-mers for each + lineage defined in + https://github.com/anna-parker/InfluenzaAReferenceDB/blob/main/influenza-b/config/config.yaml). + Where available we use the assembly information to group segments that are from the same + sample/isolate. For all remaining segments we use a heuristic grouping algorithm to group all + segments from the same sample/isolate using the metadata available from each segment. +

+ ) + } +
+
+ Protein Functions Explained + { + type === 'A' && ( +
+
    +
  • Segment 1: PB2 protein (part of the RNA polymerase subunit).
  • +
  • Segment 2: PB1 protein (part of the RNA polymerase subunit).
  • +
  • Segment 3: PA protein (part of the RNA polymerase subunit).
  • +
  • + Segment 4: HA protein (hemagglutinin, part of the envelope, functions as an attachment + factor and membrane fusion protein. It is responsible for binding influenza to sialic acid + on the surface of target cells in the upper respiratory tract.) +
  • +
  • + Segment 5: NP protein (nucleoprotein, at start of infection binds with RNA to enter host + cell nucleus where it is transcribed and replicated). +
  • +
  • + Segment 6: NA protein (Neuraminidase, part of the envelope, is an enzyme that breaks + glycosidic bonds in molecules called neuraminic acids (often found as sialic acids). This + helps the new virus particles leave the infected cell and spread to other cells). +
  • +
  • + Segment 7: M1 and M2 proteins (M1 forms the capsid - a layer between the nucleoprotein and + the envelope, M2 a proton channel protein). +
  • +
  • Segment 8: NS1 and NEP proteins (non-structural protein and nuclear export protein)
  • +
+

+ For each individual influenza subtype you can view the CDS of each protein in the genome data + viewer. +

+
+ ) + } + { + type === 'B' && ( +
+
    +
  • Segment 1: PB1 protein (part of the RNA polymerase subunit).
  • +
  • Segment 2: PB2 protein (part of the RNA polymerase subunit).
  • +
  • Segment 3: PA protein (part of the RNA polymerase subunit).
  • +
  • + Segment 4: HA protein (hemagglutinin, functions as an attachment factor and membrane fusion + protein. It is responsible for binding influenza to sialic acid on the surface of target + cells in the upper respiratory tract.) +
  • +
  • + Segment 5: NP protein (nucleoprotein, at start of infection binds with RNA to enter host + cell nucleus where it is transcribed and replicated). +
  • +
  • + Segment 6: NA protein (Neuraminidase is an enzyme that breaks glycosidic bonds in molecules + called neuraminic acids (often found as sialic acids). This helps the new virus particles + leave the infected cell and spread to other cells). +
  • +
  • + Segment 7: M1 and M2 proteins (M1 forms the capsid - a layer between the nucleoprotein and + the envelope, M2 a proton channel protein). +
  • +
  • Segment 8: NS1 and NEP proteins (non-structural protein and nuclear export protein)
  • +
+

+ For each individual influenza lineage you can view the CDS of each protein in the genome data + viewer. +

+
+ ) + } +
+
diff --git a/website/src/components/views/overview/LinkedOverview.astro b/website/src/components/views/overview/LinkedOverview.astro index 7ea8e7db7..d50185055 100644 --- a/website/src/components/views/overview/LinkedOverview.astro +++ b/website/src/components/views/overview/LinkedOverview.astro @@ -20,8 +20,12 @@ const subsections = linkedOrganisms.map((key) => allSections[key]); {section.headline} +
+ +

{linkedOrganismsTitle}

+
diff --git a/website/src/pages/influenza-a/h1n1pdm/index.astro b/website/src/pages/influenza-a/h1n1pdm/index.astro index 5c1e7ef1c..952b3201d 100644 --- a/website/src/pages/influenza-a/h1n1pdm/index.astro +++ b/website/src/pages/influenza-a/h1n1pdm/index.astro @@ -1,9 +1,30 @@ --- +import InfluenzaAInformation from '../../../components/InfluenzaInformation.astro'; +import Link from '../../../components/Link.astro'; import GenomeView from '../../../components/views/genomeView/GenomeView.astro'; import GenericOverview from '../../../components/views/overview/GenericOverview.astro'; import { Organisms } from '../../../types/Organism'; --- - +
+

+ The pandemic H1N1/09 virus (also called H1N1pdm) is a swine origin influenza A virus subtype H1N1 strain + that was responsible for the 2009 swine flu pandemic. It is an ancestor of most H1N1 viruses now in + circulation in humans. +

+

+ We use the official nextclade dataset for sequence alignment and HA and NA clade assignment: + nextstrain/flu/h1n1pdm + (more specifically we use the CY121680.1 + HA reference, the MW626056 Na reference and + otherwise the references from the GCF_001343785.1 assembly). +

+ + +
diff --git a/website/src/pages/influenza-a/h3n2/index.astro b/website/src/pages/influenza-a/h3n2/index.astro index 2b1b44001..fe51855bd 100644 --- a/website/src/pages/influenza-a/h3n2/index.astro +++ b/website/src/pages/influenza-a/h3n2/index.astro @@ -1,9 +1,33 @@ --- +import InfluenzaAInformation from '../../../components/InfluenzaInformation.astro'; +import Link from '../../../components/Link.astro'; import GenomeView from '../../../components/views/genomeView/GenomeView.astro'; import GenericOverview from '../../../components/views/overview/GenericOverview.astro'; import { Organisms } from '../../../types/Organism'; --- - +
+

+ H3N2 is one of the two major subtypes of influenza circulating in humans. Major outbreaks of A/H3N2 strains + in humans include Hong Kong Flu (1968-1969), and Fujian flu (2003-2004). +

+

+ We use the official nextclade dataset for sequence alignment and HA and NA clade assignment: + nextstrain/flu/h3n2 + (more specifically we use the CY121680.1 + HA reference and our custom dataset + genspectrum/h3n2/seg6/CY114383 for NA, for all other sequences we use the GCF_000865085.1. We have converted nextclade's clade assignments that are based on a non-open GISAID sequence to use a + similar INSDC-available reference sequence. +

+ + +
diff --git a/website/src/pages/influenza-a/h5n1/index.astro b/website/src/pages/influenza-a/h5n1/index.astro index f1396c403..01b13651f 100644 --- a/website/src/pages/influenza-a/h5n1/index.astro +++ b/website/src/pages/influenza-a/h5n1/index.astro @@ -1,9 +1,31 @@ --- +import InfluenzaAInformation from '../../../components/InfluenzaInformation.astro'; +import Link from '../../../components/Link.astro'; import GenomeView from '../../../components/views/genomeView/GenomeView.astro'; import GenericOverview from '../../../components/views/overview/GenericOverview.astro'; import { Organisms } from '../../../types/Organism'; --- - +
+

+ The H5N1 influenza subtype is enzootic in birds and can also infect other animals. Since 2018 the highly + pathogenic subtype HPAI A(H5N1) has become the dominant strain in bird populations worldwide and has also + caused widespread disease in cattle. +

+

+ We assign clades based on the AF144305.1 + HA segment, as defined in the community dataset: community/moncla-lab/iav-h5/ha/all-clades. For the other segments we use our own custom dataset based the + GCF_000864105.1 + assembly: + genspectrum/flu/h5n1. +

+ + +
diff --git a/website/src/pages/influenza-a/index.astro b/website/src/pages/influenza-a/index.astro index 250d2b252..6abfd1586 100644 --- a/website/src/pages/influenza-a/index.astro +++ b/website/src/pages/influenza-a/index.astro @@ -1,8 +1,27 @@ --- +import InfluenzaAInformation from '../../components/InfluenzaInformation.astro'; import LinkedOverview from '../../components/views/overview/LinkedOverview.astro'; import { Organisms } from '../../types/Organism'; const subtypes = [Organisms.h5n1, Organisms.h3n2, Organisms.h1n1pdm]; --- - + +
+

+ Influenza A viruses have segmented genomes consisting of eight negative-sense, single-stranded RNA segments. +

+

+ Influenza A subtypes are determined by the two viral envelope proteins HA and NA, on segments 4 and 6 + respectively. The two main influenza A subtypes circulating in humans are H1N1 and H3N2. The primary host + for most influenza A subtypes is birds, but they can also infect other animal species such as pigs and + cattle. For the major subtypes, more fine grained clades are also determined based on the HA (and sometimes + NA) segments. +

+

+ The HA protein (on segment 4) is the primary target of neutralizing antibodies, and NA is often used as an + antiviral drug target +

+ +
+
diff --git a/website/src/pages/influenza-b/index.astro b/website/src/pages/influenza-b/index.astro index ce6f08f20..9630a800f 100644 --- a/website/src/pages/influenza-b/index.astro +++ b/website/src/pages/influenza-b/index.astro @@ -1,12 +1,27 @@ --- +import InfluenzaInformation from '../../components/InfluenzaInformation.astro'; import LinkedOverview from '../../components/views/overview/LinkedOverview.astro'; import { Organisms } from '../../types/Organism'; const lineages = [Organisms.victoria]; --- - + +
+

+ Influenza B viruses have segmented genomes consisting of eight negative-sense, single-stranded RNA segments. +

+

+ Influenza B lineages are determined by the two viral envelope proteins HA and NA, on segments 4 and 6 + respectively. The two main influenza lineages circulating in humans are victoria and yamagata. However, the + B/Yamagata-lineage viruses have not been identified since the covid19 pandemic in 2020. The primary host for + most influenza B is mammals, including humans, ferrets, pigs, and seals. More fine grained clades are also + determined based on the HA (and sometimes NA) segments. +

+

+ The HA protein (on segment 4) is the primary target of neutralizing antibodies, and NA is often used as an + antiviral drug target. +

+ +
+
diff --git a/website/src/pages/influenza-b/victoria/index.astro b/website/src/pages/influenza-b/victoria/index.astro index 62fe3d8eb..5f5c7ea39 100644 --- a/website/src/pages/influenza-b/victoria/index.astro +++ b/website/src/pages/influenza-b/victoria/index.astro @@ -1,9 +1,24 @@ --- +import InfluenzaInformation from '../../../components/InfluenzaInformation.astro'; +import Link from '../../../components/Link.astro'; import GenomeView from '../../../components/views/genomeView/GenomeView.astro'; import GenericOverview from '../../../components/views/overview/GenericOverview.astro'; import { Organisms } from '../../../types/Organism'; --- - +
+

+ Victoria is only currently circulating influenza B lineage. Until 2020 both B/Yamagata and B/Victoria + circled in the population but now only Victoria is circulating. +

+

+ We use the official nextclade dataset for sequence alignment and HA and NA clade assignment: /nextstrain/flu/vic +

+ + +
From 4f600f49b718eb55aa997833b903fd63d16ca681 Mon Sep 17 00:00:00 2001 From: Fabian Engelniederhammer <92720311+fengelniederhammer@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:03:22 +0200 Subject: [PATCH 4/6] refactor(website): migrate generic single variant page to React (#715) resolves #714 --- ...GenericAnalyseSingleVariantDataDisplay.tsx | 119 ++++++++++++++++++ .../GenericAnalyzeSingleVariantPage.astro | 103 ++------------- .../QuickstartLinks.astro | 38 ------ .../analyzeSingleVariant/QuickstartLinks.tsx | 37 ++++++ website/src/views/routing.ts | 2 + 5 files changed, 168 insertions(+), 131 deletions(-) create mode 100644 website/src/components/views/analyzeSingleVariant/GenericAnalyseSingleVariantDataDisplay.tsx delete mode 100644 website/src/components/views/analyzeSingleVariant/QuickstartLinks.astro create mode 100644 website/src/components/views/analyzeSingleVariant/QuickstartLinks.tsx diff --git a/website/src/components/views/analyzeSingleVariant/GenericAnalyseSingleVariantDataDisplay.tsx b/website/src/components/views/analyzeSingleVariant/GenericAnalyseSingleVariantDataDisplay.tsx new file mode 100644 index 000000000..7020eb0d5 --- /dev/null +++ b/website/src/components/views/analyzeSingleVariant/GenericAnalyseSingleVariantDataDisplay.tsx @@ -0,0 +1,119 @@ +import { views } from '@genspectrum/dashboard-components/util'; +import { type FC, useMemo } from 'react'; + +import { QuickstartLinks } from './QuickstartLinks.tsx'; +import { SelectVariant } from './SelectVariant.tsx'; +import type { OrganismsConfig } from '../../../config.ts'; +import { Organisms } from '../../../types/Organism.ts'; +import { chooseGranularityBasedOnDateRange } from '../../../util/chooseGranularityBasedOnDateRange.ts'; +import { hasOnlyUndefinedValues } from '../../../util/hasOnlyUndefinedValues.ts'; +import type { DatasetAndVariantData } from '../../../views/View.ts'; +import { getLocationSubdivision } from '../../../views/locationHelpers.ts'; +import { locationFieldsToFilterIdentifier } from '../../../views/pageStateHandlers/locationFilterFromToUrl.ts'; +import { type OrganismViewKey, Routing, type SingleVariantOrganism } from '../../../views/routing.ts'; +import type { singleVariantViewKey } from '../../../views/viewKeys.ts'; +import { ComponentsGrid } from '../../ComponentsGrid.tsx'; +import { GsAggregate } from '../../genspectrum/GsAggregate.tsx'; +import { GsMutations } from '../../genspectrum/GsMutations.tsx'; +import { GsMutationsOverTime } from '../../genspectrum/GsMutationsOverTime.tsx'; +import { GsPrevalenceOverTime } from '../../genspectrum/GsPrevalenceOverTime.tsx'; +import { GsStatistics } from '../../genspectrum/GsStatistics.tsx'; + +export type GenericAnalyseSingleVariantDataDisplayProps = { + organismViewKey: OrganismViewKey & + `${Exclude}.${typeof singleVariantViewKey}`; + organismsConfig: OrganismsConfig; + pageState: DatasetAndVariantData; +}; + +export const GenericAnalyseSingleVariantDataDisplay: FC = ({ + organismViewKey, + organismsConfig, + pageState, +}) => { + const view = useMemo(() => new Routing(organismsConfig), [organismsConfig]).getOrganismView(organismViewKey); + + const variantLapisFilter = view.pageStateHandler.toLapisFilter(pageState); + const datasetLapisFilter = view.pageStateHandler.toLapisFilterWithoutVariant(pageState); + const timeGranularity = chooseGranularityBasedOnDateRange({ + earliestDate: new Date(view.organismConstants.earliestDate), + dateRange: pageState.datasetFilter.dateFilters[view.organismConstants.mainDateField], + }); + + const { label: subdivisionLabel, field: subdivisionField } = getLocationSubdivision( + view.organismConstants.locationFields, + pageState.datasetFilter.locationFilters[ + locationFieldsToFilterIdentifier(view.organismConstants.locationFields) + ], + ); + const noVariantSelected = hasOnlyUndefinedValues(pageState.variantFilter); + + return ( +
+ {noVariantSelected && ( + + + + )} + + + + + + {subdivisionField !== undefined && ( + + )} + + {view.organismConstants.aggregatedVisualizations.singleVariant.map(({ label, fields, views }) => ( + + ))} + + + +
+ ); +}; diff --git a/website/src/components/views/analyzeSingleVariant/GenericAnalyzeSingleVariantPage.astro b/website/src/components/views/analyzeSingleVariant/GenericAnalyzeSingleVariantPage.astro index 4eada60e1..b4096f103 100644 --- a/website/src/components/views/analyzeSingleVariant/GenericAnalyzeSingleVariantPage.astro +++ b/website/src/components/views/analyzeSingleVariant/GenericAnalyzeSingleVariantPage.astro @@ -1,25 +1,13 @@ --- -import { views } from '@genspectrum/dashboard-components/util'; - -import QuickstartLinks from './QuickstartLinks.astro'; -import { SelectVariant } from './SelectVariant'; +import { GenericAnalyseSingleVariantDataDisplay } from './GenericAnalyseSingleVariantDataDisplay'; import { getDashboardsConfig } from '../../../config'; import SingleVariantOrganismPageLayout from '../../../layouts/OrganismPage/SingleVariantOrganismPageLayout.astro'; import { Organisms } from '../../../types/Organism'; -import { chooseGranularityBasedOnDateRange } from '../../../util/chooseGranularityBasedOnDateRange'; import { hasOnlyUndefinedValues } from '../../../util/hasOnlyUndefinedValues'; -import { getLocationSubdivision } from '../../../views/locationHelpers'; import { toDisplayName } from '../../../views/pageStateHandlers/PageStateHandler'; -import { locationFieldsToFilterIdentifier } from '../../../views/pageStateHandlers/locationFilterFromToUrl'; import { type OrganismViewKey, type OrganismWithViewKey } from '../../../views/routing'; import { ServerSide } from '../../../views/serverSideRouting'; import { singleVariantViewKey } from '../../../views/viewKeys'; -import ComponentsGrid from '../../ComponentsGrid.astro'; -import GsAggregate from '../../genspectrum/GsAggregate.astro'; -import GsMutations from '../../genspectrum/GsMutations.astro'; -import GsMutationsOverTime from '../../genspectrum/GsMutationsOverTime.astro'; -import GsPrevalenceOverTime from '../../genspectrum/GsPrevalenceOverTime.astro'; -import GsStatistics from '../../genspectrum/GsStatistics.astro'; import { AnalyzeSingleVariantSelectorFallback } from '../../pageStateSelectors/FallbackElement'; import { SingleVariantPageStateSelector } from '../../pageStateSelectors/SingleVariantPageStateSelector'; import { sanitizeForFilename } from '../compareSideBySide/toDownloadLink'; @@ -36,19 +24,11 @@ const view = ServerSide.routing.getOrganismView(organismViewKey); const pageState = view.pageStateHandler.parsePageStateFromUrl(Astro.url); const variantLapisFilter = view.pageStateHandler.toLapisFilter(pageState); -const datasetLapisFilter = view.pageStateHandler.toLapisFilterWithoutVariant(pageState); -const timeGranularity = chooseGranularityBasedOnDateRange({ - earliestDate: new Date(view.organismConstants.earliestDate), - dateRange: pageState.datasetFilter.dateFilters[view.organismConstants.mainDateField], -}); - -const { label: subdivisionLabel, field: subdivisionField } = getLocationSubdivision( - view.organismConstants.locationFields, - pageState.datasetFilter.locationFilters[locationFieldsToFilterIdentifier(view.organismConstants.locationFields)], -); const noVariantSelected = hasOnlyUndefinedValues(pageState.variantFilter); +const organismConfig = getDashboardsConfig().dashboards.organisms; + const displayName = toDisplayName(pageState.variantFilter); const downloadLinks = noVariantSelected ? [] @@ -65,82 +45,19 @@ const downloadLinks = noVariantSelected -
- { - noVariantSelected && ( - - - - ) - } - - - - - - { - subdivisionField !== undefined && ( - - ) - } - - { - view.organismConstants.aggregatedVisualizations.singleVariant.map(({ label, fields, views }) => ( - - )) - } - - - +
diff --git a/website/src/components/views/analyzeSingleVariant/QuickstartLinks.astro b/website/src/components/views/analyzeSingleVariant/QuickstartLinks.astro deleted file mode 100644 index 826ba2560..000000000 --- a/website/src/components/views/analyzeSingleVariant/QuickstartLinks.astro +++ /dev/null @@ -1,38 +0,0 @@ ---- -import { collectionVariantClassName } from './CollectionsList'; -import { Organisms } from '../../../types/Organism'; -import { toDisplayName } from '../../../views/pageStateHandlers/PageStateHandler'; -import type { OrganismWithViewKey, ViewsMap } from '../../../views/routing'; -import { singleVariantViewKey } from '../../../views/viewKeys'; - -type SingleVariantOrganism = OrganismWithViewKey; -interface Props { - view: ViewsMap[Exclude][typeof singleVariantViewKey]; -} - -const { view } = Astro.props; - -const variants = view.organismConstants.predefinedVariants; - -const pageState = view.pageStateHandler.parsePageStateFromUrl(Astro.url); ---- - -
-

To get started you can choose one of these variants:

- -
- { - variants.map((variant) => ( - - {toDisplayName(variant)} - - )) - } -
-
diff --git a/website/src/components/views/analyzeSingleVariant/QuickstartLinks.tsx b/website/src/components/views/analyzeSingleVariant/QuickstartLinks.tsx new file mode 100644 index 000000000..9745d57d8 --- /dev/null +++ b/website/src/components/views/analyzeSingleVariant/QuickstartLinks.tsx @@ -0,0 +1,37 @@ +import type { FC } from 'react'; + +import { collectionVariantClassName } from './CollectionsList'; +import { Organisms } from '../../../types/Organism'; +import type { DatasetFilter } from '../../../views/View.ts'; +import { toDisplayName } from '../../../views/pageStateHandlers/PageStateHandler'; +import type { SingleVariantOrganism, ViewsMap } from '../../../views/routing'; +import { singleVariantViewKey } from '../../../views/viewKeys'; + +export type QuickstartLinksProps = { + view: ViewsMap[Exclude][typeof singleVariantViewKey]; + datasetFilter: DatasetFilter; +}; + +export const QuickstartLinks: FC = ({ view, datasetFilter }) => { + const variants = view.organismConstants.predefinedVariants; + + return ( +
+

To get started you can choose one of these variants:

+ +
+ {variants.map((variant) => { + const href = view.pageStateHandler.toUrl({ + datasetFilter: datasetFilter, + variantFilter: variant, + }); + return ( + + {toDisplayName(variant)} + + ); + })} +
+
+ ); +}; diff --git a/website/src/views/routing.ts b/website/src/views/routing.ts index 68eea580c..6e7c0136b 100644 --- a/website/src/views/routing.ts +++ b/website/src/views/routing.ts @@ -113,6 +113,8 @@ export type OrganismViewKey = { type OrganismExtractor = T extends `${infer O}.${ViewKey}` ? O : never; export type OrganismWithViewKey = OrganismExtractor; +export type SingleVariantOrganism = OrganismWithViewKey; + export class Routing { public readonly views; public readonly externalPages; From 94600724646fba3d1836fa5896d136543e73157e Mon Sep 17 00:00:00 2001 From: Fabian Engelniederhammer <92720311+fengelniederhammer@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:56:42 +0200 Subject: [PATCH 5/6] refactor(website): migrate generic sequencing efforts page to React (#719) * refactor(website): migrate generic sequencing efforts page to React resolves #716 * fixup! refactor(website): migrate generic sequencing efforts page to React * fixup! refactor(website): migrate generic sequencing efforts page to React --- .../genspectrum/GsNumberSequencesOverTime.tsx | 28 ++++--- .../genspectrum/GsSequencesByLocation.astro | 49 ----------- .../genspectrum/GsSequencesByLocation.tsx | 55 +++++++++++++ .../subscriptions/create/FilterDisplay.tsx | 2 +- .../sequencingEfforts/CountStatistics.astro | 32 -------- .../sequencingEfforts/CountStatistics.tsx | 46 +++++++++++ .../GenericSequencingEffortsDataDisplay.tsx | 82 +++++++++++++++++++ .../GenericSequencingEffortsPage.astro | 60 +++----------- website/src/lapis/getTotalCount.spec.ts | 20 +++-- website/src/lapis/getTotalCount.ts | 31 +++---- .../getSequencesByLocationMapData.spec.ts | 2 +- .../src/util/getSequencesByLocationMapData.ts | 80 +++++++++--------- website/vitest.setup.ts | 7 ++ 13 files changed, 289 insertions(+), 205 deletions(-) delete mode 100644 website/src/components/genspectrum/GsSequencesByLocation.astro create mode 100644 website/src/components/genspectrum/GsSequencesByLocation.tsx delete mode 100644 website/src/components/views/sequencingEfforts/CountStatistics.astro create mode 100644 website/src/components/views/sequencingEfforts/CountStatistics.tsx create mode 100644 website/src/components/views/sequencingEfforts/GenericSequencingEffortsDataDisplay.tsx diff --git a/website/src/components/genspectrum/GsNumberSequencesOverTime.tsx b/website/src/components/genspectrum/GsNumberSequencesOverTime.tsx index 9cdd4cf3c..ecbe70940 100644 --- a/website/src/components/genspectrum/GsNumberSequencesOverTime.tsx +++ b/website/src/components/genspectrum/GsNumberSequencesOverTime.tsx @@ -1,13 +1,15 @@ import '@genspectrum/dashboard-components/components'; import type { NamedLapisFilter } from '@genspectrum/dashboard-components/util'; +import { ComponentWrapper } from '../ComponentWrapper.tsx'; + type Views = 'bar' | 'line' | 'table'; type TemporalGranularity = 'day' | 'week' | 'month' | 'year'; export function GsNumberSequencesOverTime({ lapisDateField, - lapisFilter, + lapisFilters, views, width, height, @@ -16,7 +18,7 @@ export function GsNumberSequencesOverTime({ pageSize, }: { lapisDateField: string; - lapisFilter: NamedLapisFilter[]; + lapisFilters: NamedLapisFilter[]; views?: Views[]; width?: string; height?: string; @@ -25,15 +27,17 @@ export function GsNumberSequencesOverTime({ pageSize?: boolean | number; }) { return ( - + + + ); } diff --git a/website/src/components/genspectrum/GsSequencesByLocation.astro b/website/src/components/genspectrum/GsSequencesByLocation.astro deleted file mode 100644 index 11dae6106..000000000 --- a/website/src/components/genspectrum/GsSequencesByLocation.astro +++ /dev/null @@ -1,49 +0,0 @@ ---- -import { type LapisFilter } from '@genspectrum/dashboard-components/util'; - -import { getSequencesByLocationMapData } from '../../util/getSequencesByLocationMapData'; -import { defaultTablePageSize } from '../../views/View'; -import ComponentWrapper from '../ComponentWrapper.astro'; - -interface Props { - title: string; - lapisLocationField: string; - lapisFilter: LapisFilter; - height?: string; - mapName?: string; - pageSize?: number; -} - -const { title, height, lapisLocationField, lapisFilter, mapName, pageSize } = Astro.props; - -const mapData = getSequencesByLocationMapData(mapName, Astro.url); ---- - - - { - mapData === undefined ? ( - - ) : ( - - ) - } - diff --git a/website/src/components/genspectrum/GsSequencesByLocation.tsx b/website/src/components/genspectrum/GsSequencesByLocation.tsx new file mode 100644 index 000000000..54c51e378 --- /dev/null +++ b/website/src/components/genspectrum/GsSequencesByLocation.tsx @@ -0,0 +1,55 @@ +import { type LapisFilter } from '@genspectrum/dashboard-components/util'; +import type { FC } from 'react'; + +import { getSequencesByLocationMapData } from '../../util/getSequencesByLocationMapData'; +import { defaultTablePageSize } from '../../views/View'; +import { ComponentWrapper } from '../ComponentWrapper.tsx'; + +export type GsSequencesByLocationProps = { + title: string; + lapisLocationField: string; + lapisFilter: LapisFilter; + height?: string; + mapName?: string; + pageSize?: number; +}; + +export const GsSequencesByLocation: FC = ({ + title, + height, + lapisLocationField, + lapisFilter, + mapName, + pageSize, +}) => { + const mapData = getSequencesByLocationMapData(mapName, window.location.href); + + return ( + + {mapData === undefined ? ( + + ) : ( + + )} + + ); +}; diff --git a/website/src/components/subscriptions/create/FilterDisplay.tsx b/website/src/components/subscriptions/create/FilterDisplay.tsx index f9d165fa0..9da0bcaec 100644 --- a/website/src/components/subscriptions/create/FilterDisplay.tsx +++ b/website/src/components/subscriptions/create/FilterDisplay.tsx @@ -38,7 +38,7 @@ export function FilterDisplay({

Count over time

-
- { - count === undefined ? ( -

- Error - Sorry, we were unable to compute the total count of sequences. Please try again later. -

- ) : ( - <> -
Sequences
-
{count.toLocaleString('en-us')}
-
The total number of sequenced samples
- - ) - } -
-
diff --git a/website/src/components/views/sequencingEfforts/CountStatistics.tsx b/website/src/components/views/sequencingEfforts/CountStatistics.tsx new file mode 100644 index 000000000..08dead3d2 --- /dev/null +++ b/website/src/components/views/sequencingEfforts/CountStatistics.tsx @@ -0,0 +1,46 @@ +import type { LapisFilter } from '@genspectrum/dashboard-components/util'; +import { useQuery } from '@tanstack/react-query'; +import type { FC } from 'react'; + +import { getTotalCount } from '../../../lapis/getTotalCount'; +import { withQueryProvider } from '../../subscriptions/backendApi/withQueryProvider.tsx'; + +export type CountStatisticsProps = { + lapisFilter: LapisFilter; + lapisUrl: string; +}; + +const CountStatisticsInner: FC = ({ lapisFilter, lapisUrl }) => { + const { + data: count, + isPending, + isError, + } = useQuery({ + queryKey: ['countStatistics', lapisFilter, lapisUrl], + queryFn: () => getTotalCount(lapisUrl, lapisFilter), + refetchOnWindowFocus: false, + refetchOnReconnect: false, + }); + + return ( +
+
+ {isPending ? ( + + ) : isError ? ( +

+ Error - Sorry, we were unable to compute the total count of sequences. Please try again later. +

+ ) : ( + <> +
Sequences
+
{count.toLocaleString('en-us')}
+
The total number of sequenced samples
+ + )} +
+
+ ); +}; + +export const CountStatistics = withQueryProvider(CountStatisticsInner); diff --git a/website/src/components/views/sequencingEfforts/GenericSequencingEffortsDataDisplay.tsx b/website/src/components/views/sequencingEfforts/GenericSequencingEffortsDataDisplay.tsx new file mode 100644 index 000000000..e3c38984c --- /dev/null +++ b/website/src/components/views/sequencingEfforts/GenericSequencingEffortsDataDisplay.tsx @@ -0,0 +1,82 @@ +import { type FC, useMemo } from 'react'; + +import { CountStatistics } from './CountStatistics.tsx'; +import { type OrganismsConfig } from '../../../config.ts'; +import { chooseGranularityBasedOnDateRange } from '../../../util/chooseGranularityBasedOnDateRange.ts'; +import type { CovidVariantData } from '../../../views/covid.ts'; +import { getLocationDisplayConfig } from '../../../views/locationHelpers.ts'; +import { locationFieldsToFilterIdentifier } from '../../../views/pageStateHandlers/locationFilterFromToUrl.ts'; +import { type OrganismViewKey, Routing } from '../../../views/routing.ts'; +import { sequencingEffortsViewKey } from '../../../views/viewKeys.ts'; +import { ComponentsGrid } from '../../ComponentsGrid.tsx'; +import { GsAggregate } from '../../genspectrum/GsAggregate.tsx'; +import { GsNumberSequencesOverTime } from '../../genspectrum/GsNumberSequencesOverTime.tsx'; +import { GsSequencesByLocation } from '../../genspectrum/GsSequencesByLocation.tsx'; + +export type GenericSequencingEffortsDataDisplayProps = { + organismViewKey: OrganismViewKey & `${string}.${typeof sequencingEffortsViewKey}`; + organismsConfig: OrganismsConfig; + pageState: CovidVariantData; +}; + +export const GenericSequencingEffortsDataDisplay: FC = ({ + organismViewKey, + organismsConfig, + pageState, +}) => { + const view = useMemo(() => new Routing(organismsConfig), [organismsConfig]).getOrganismView(organismViewKey); + + const lapisFilter = view.pageStateHandler.toLapisFilter(pageState); + const timeGranularity = chooseGranularityBasedOnDateRange({ + earliestDate: new Date(view.organismConstants.earliestDate), + dateRange: pageState.datasetFilter.dateFilters[view.organismConstants.mainDateField], + }); + + const { locationField, mapName } = getLocationDisplayConfig( + view.organismConstants.locationFields, + pageState.datasetFilter.locationFilters[ + locationFieldsToFilterIdentifier(view.organismConstants.locationFields) + ], + ); + + return ( +
+ + + + {locationField !== undefined && ( + + )} + {view.organismConstants.aggregatedVisualizations.sequencingEfforts.map(({ label, fields, views }) => ( + + ))} + +
+ ); +}; diff --git a/website/src/components/views/sequencingEfforts/GenericSequencingEffortsPage.astro b/website/src/components/views/sequencingEfforts/GenericSequencingEffortsPage.astro index 717a0c920..474038d44 100644 --- a/website/src/components/views/sequencingEfforts/GenericSequencingEffortsPage.astro +++ b/website/src/components/views/sequencingEfforts/GenericSequencingEffortsPage.astro @@ -1,17 +1,10 @@ --- -import CountStatistics from './CountStatistics.astro'; -import { getDashboardsConfig, getLapisUrl } from '../../../config'; +import { GenericSequencingEffortsDataDisplay } from './GenericSequencingEffortsDataDisplay'; +import { getDashboardsConfig } from '../../../config'; import SingleVariantOrganismPageLayout from '../../../layouts/OrganismPage/SingleVariantOrganismPageLayout.astro'; -import { chooseGranularityBasedOnDateRange } from '../../../util/chooseGranularityBasedOnDateRange'; -import { getLocationDisplayConfig } from '../../../views/locationHelpers'; -import { locationFieldsToFilterIdentifier } from '../../../views/pageStateHandlers/locationFilterFromToUrl'; import { type OrganismViewKey, type OrganismWithViewKey } from '../../../views/routing'; import { ServerSide } from '../../../views/serverSideRouting'; import { sequencingEffortsViewKey } from '../../../views/viewKeys'; -import ComponentsGrid from '../../ComponentsGrid.astro'; -import GsAggregate from '../../genspectrum/GsAggregate.astro'; -import GsNumberSequencesOverTime from '../../genspectrum/GsNumberSequencesOverTime.astro'; -import GsSequencesByLocation from '../../genspectrum/GsSequencesByLocation.astro'; import { SequencingEffortsSelectorFallback } from '../../pageStateSelectors/FallbackElement'; import { SequencingEffortsPageStateSelector } from '../../pageStateSelectors/SequencingEffortsPageStateSelector'; @@ -27,15 +20,8 @@ const view = ServerSide.routing.getOrganismView(organismViewKey); const pageState = view.pageStateHandler.parsePageStateFromUrl(Astro.url); const lapisFilter = view.pageStateHandler.toLapisFilter(pageState); -const timeGranularity = chooseGranularityBasedOnDateRange({ - earliestDate: new Date(view.organismConstants.earliestDate), - dateRange: pageState.datasetFilter.dateFilters[view.organismConstants.mainDateField], -}); -const { locationField, mapName } = getLocationDisplayConfig( - view.organismConstants.locationFields, - pageState.datasetFilter.locationFilters[locationFieldsToFilterIdentifier(view.organismConstants.locationFields)], -); +const organismConfig = getDashboardsConfig().dashboards.organisms; --- -
- - - - { - locationField !== undefined && ( - - ) - } - { - view.organismConstants.aggregatedVisualizations.sequencingEfforts.map(({ label, fields, views }) => ( - - )) - } - +
+
diff --git a/website/src/lapis/getTotalCount.spec.ts b/website/src/lapis/getTotalCount.spec.ts index edab39e6c..e94dc8378 100644 --- a/website/src/lapis/getTotalCount.spec.ts +++ b/website/src/lapis/getTotalCount.spec.ts @@ -1,9 +1,13 @@ -import { describe, expect, test } from 'vitest'; +import { beforeEach, describe, expect, test } from 'vitest'; import { getTotalCount } from './getTotalCount.ts'; -import { DUMMY_LAPIS_URL, lapisRequestMocks } from '../../vitest.setup.ts'; +import { astroApiMocks, DUMMY_LAPIS_URL, lapisRequestMocks } from '../../vitest.setup.ts'; describe('getTotalCount', () => { + beforeEach(() => { + astroApiMocks.log(); + }); + test('should return the total count', async () => { const lapisFilter = { country: 'Germany' }; @@ -19,9 +23,9 @@ describe('getTotalCount', () => { lapisRequestMocks.postAggregated(lapisFilter, { data: [{ count: 42 }] }, 500); - const result = await getTotalCount(DUMMY_LAPIS_URL, lapisFilter); - - expect(result).toBeUndefined(); + await expect(getTotalCount(DUMMY_LAPIS_URL, lapisFilter)).rejects.toThrow( + /Failed to fetch lapis aggregated data/, + ); }); test('should return undefined when LAPIS returns unexpected data', async () => { @@ -30,8 +34,8 @@ describe('getTotalCount', () => { // @ts-expect-error -- intentionally passing wrong data lapisRequestMocks.postAggregated(lapisFilter, { data: 'something unexpected' }); - const result = await getTotalCount(DUMMY_LAPIS_URL, lapisFilter); - - expect(result).toBeUndefined(); + await expect(getTotalCount(DUMMY_LAPIS_URL, lapisFilter)).rejects.toThrow( + /Failed to parse lapis aggregated data/, + ); }); }); diff --git a/website/src/lapis/getTotalCount.ts b/website/src/lapis/getTotalCount.ts index 0d562f904..1ce914cd7 100644 --- a/website/src/lapis/getTotalCount.ts +++ b/website/src/lapis/getTotalCount.ts @@ -2,30 +2,31 @@ import type { LapisFilter } from '@genspectrum/dashboard-components/util'; import axios from 'axios'; import { z } from 'zod'; -import { getInstanceLogger } from '../logger.ts'; +import { getClientLogger } from '../clientLogger.ts'; const lapisTotalCountSchema = z.object({ data: z.tuple([z.object({ count: z.number() })]), }); export type LapisTotalCount = z.infer; -const logger = getInstanceLogger('getTotalCount'); +const logger = getClientLogger('getTotalCount'); export async function getTotalCount(lapisUrl: string, lapisFilter: LapisFilter) { + let response; try { - const response = await axios.post(`${lapisUrl}/sample/aggregated`, lapisFilter); - const parsedResponse = lapisTotalCountSchema.safeParse(response.data); - - if (parsedResponse.success) { - return parsedResponse.data.data[0].count; - } - - logger.error( - `Failed to parse lapis aggregated data: ${JSON.stringify(parsedResponse)} (was ${JSON.stringify(response.data)})`, - ); - return undefined; + response = await axios.post(`${lapisUrl}/sample/aggregated`, lapisFilter); } catch (error) { - logger.error(`Failed to fetch lapis aggregated data: ${JSON.stringify(error)}`); - return undefined; + const message = `Failed to fetch lapis aggregated data: ${JSON.stringify(error)}`; + logger.error(message); + throw new Error(message); } + + const parsedResponse = lapisTotalCountSchema.safeParse(response.data); + if (parsedResponse.success) { + return parsedResponse.data.data[0].count; + } + + const message = `Failed to parse lapis aggregated data: ${JSON.stringify(parsedResponse)} (was ${JSON.stringify(response.data)})`; + logger.error(message); + throw new Error(message); } diff --git a/website/src/util/getSequencesByLocationMapData.spec.ts b/website/src/util/getSequencesByLocationMapData.spec.ts index 2b9c2b914..361c45bd1 100644 --- a/website/src/util/getSequencesByLocationMapData.spec.ts +++ b/website/src/util/getSequencesByLocationMapData.spec.ts @@ -2,7 +2,7 @@ import { describe, expect, test } from 'vitest'; import { getSequencesByLocationMapData } from './getSequencesByLocationMapData'; -const testUrl = new URL('https://my.host/page?query=1'); +const testUrl = 'https://my.host/page?query=1'; describe('getSequencesByLocationMapData', () => { test('should return the correct map data for World', () => { diff --git a/website/src/util/getSequencesByLocationMapData.ts b/website/src/util/getSequencesByLocationMapData.ts index 50c04510b..7ac027131 100644 --- a/website/src/util/getSequencesByLocationMapData.ts +++ b/website/src/util/getSequencesByLocationMapData.ts @@ -1,67 +1,71 @@ -import path from 'path'; - import type { MapSource } from '@genspectrum/dashboard-components/util'; -import type { Topology } from 'topojson-specification'; let knownMapFiles: ReturnType | null = null; +type MapMetadatum = { + additionalInfo: Omit; + topologyObjectsKey: string; +}; + /* eslint-disable @typescript-eslint/naming-convention -- the keys must match the country names */ -const mapMetadata: Record> = { +const mapMetadata: Record = { 'Germany': { - zoom: 5.8, - offsetX: 10, - offsetY: 51.4, + additionalInfo: { + zoom: 5.8, + offsetX: 10, + offsetY: 51.4, + }, + topologyObjectsKey: 'deu', }, 'Switzerland': { - zoom: 7.7, - offsetX: 8.2, - offsetY: 46.8, + additionalInfo: { + zoom: 7.7, + offsetX: 8.2, + offsetY: 46.8, + }, + topologyObjectsKey: 'che', }, 'USA': { - zoom: 4.1, - offsetX: -98, - offsetY: 38, + additionalInfo: { + zoom: 4.1, + offsetX: -98, + offsetY: 38, + }, + topologyObjectsKey: 'usa', }, 'World': { - zoom: 1.5, - offsetX: 0, - offsetY: 10, + additionalInfo: { + zoom: 1.5, + offsetX: 0, + offsetY: 10, + }, + topologyObjectsKey: 'countries', }, 'Democratic Republic of the Congo': { - zoom: 5, - offsetX: 20, - offsetY: -5, + additionalInfo: { + zoom: 5, + offsetX: 20, + offsetY: -5, + }, + topologyObjectsKey: 'cd', }, }; /* eslint-enable @typescript-eslint/naming-convention */ function loadKnownMapFiles() { - const topoJsonFiles = import.meta.glob('../../public/mapData/*.topo.json', { eager: true }); // For reference: https://vite.dev/guide/features.html#glob-import - - const mapSpecs = Object.entries(topoJsonFiles).map(([relativeMapFilePath, map]) => { - const mapName = path.basename(relativeMapFilePath, '.topo.json'); - const topologyObjectsKey = Object.keys(map.objects)[0]; - - return { mapName, topologyObjectsKey }; - }); - return new Map( - mapSpecs.map(({ mapName, topologyObjectsKey }) => [mapName, getMapData(mapName, topologyObjectsKey)]), + Object.entries(mapMetadata).map(([mapName, mapMetadatum]) => [mapName, getMapData(mapName, mapMetadatum)]), ); } -function getMapData(mapName: string, topologyObjectsKey: string): MapData { - if (!(mapName in mapMetadata)) { - throw new Error(`Map for '${mapName}' is missing metadata configuration.`); - } - +function getMapData(mapName: string, mapMetadatum: MapMetadatum): MapData { return { mapSource: { type: 'topojson', url: `/mapData/${mapName}.topo.json`, - topologyObjectsKey: topologyObjectsKey, + topologyObjectsKey: mapMetadatum.topologyObjectsKey, }, - ...mapMetadata[mapName], + ...mapMetadatum.additionalInfo, }; } @@ -72,7 +76,7 @@ export type MapData = { readonly offsetY: number; }; -export function getSequencesByLocationMapData(mapName: string | undefined, currentUrl: URL): MapData | undefined { +export function getSequencesByLocationMapData(mapName: string | undefined, baseUrl: string): MapData | undefined { if (mapName === undefined) { return undefined; } @@ -85,7 +89,7 @@ export function getSequencesByLocationMapData(mapName: string | undefined, curre return undefined; } - const absoluteMapUrl = new URL(mapData.mapSource.url, currentUrl); + const absoluteMapUrl = new URL(mapData.mapSource.url, baseUrl); return { ...mapData, mapSource: { diff --git a/website/vitest.setup.ts b/website/vitest.setup.ts index 2fc26d778..89158f8c2 100755 --- a/website/vitest.setup.ts +++ b/website/vitest.setup.ts @@ -19,6 +19,7 @@ import setupDayjs from './src/util/setupDayjs.ts'; setupDayjs(); +export const ASTRO_SERVER_URL = '/api'; export const DUMMY_BACKEND_URL = 'http://backend.dummy'; export const DUMMY_LAPIS_URL = 'http://lapis.dummy'; @@ -31,6 +32,12 @@ function getError(assertionError: AssertionError) { type ReferenceSequence = { name: string; sequence: string }; type ReferenceGenome = { nucleotideSequences: ReferenceSequence[]; genes: ReferenceSequence[] }; +export const astroApiMocks = { + log: () => { + testServer.use(http.post(`${ASTRO_SERVER_URL}/log`, () => Response.json({}))); + }, +}; + export const lapisRequestMocks = { info: (response: LapisInfo, statusCode = 200) => { testServer.use(http.get(`${DUMMY_LAPIS_URL}/sample/info`, resolver({ statusCode, response }))); From 8b93ead6a700c5c80403539ecc594db4e635bbcd Mon Sep 17 00:00:00 2001 From: Fabian Engelniederhammer <92720311+fengelniederhammer@users.noreply.github.com> Date: Thu, 3 Jul 2025 07:56:54 +0200 Subject: [PATCH 6/6] fix(website): don't apply own field to lapis filter when filtering values of input components (#744) * fix(website): don't apply own field to lapis filter when filtering values of input components resolves #727 * fixup! fix(website): don't apply own field to lapis filter when filtering values of input components --- .../pageStateSelectors/BaselineSelector.tsx | 8 ++++++-- .../pageStateSelectors/VariantSelector.tsx | 2 +- website/tests/SequencingEffortsPage.ts | 7 ++++--- website/tests/sequencingEfforts.spec.ts | 20 +++++++++++++++++++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/website/src/components/pageStateSelectors/BaselineSelector.tsx b/website/src/components/pageStateSelectors/BaselineSelector.tsx index 5f14fe237..15ee65b1f 100644 --- a/website/src/components/pageStateSelectors/BaselineSelector.tsx +++ b/website/src/components/pageStateSelectors/BaselineSelector.tsx @@ -100,7 +100,7 @@ export function BaselineSelector({ }, }); }} - lapisFilter={lapisFilter} + lapisFilter={{ ...lapisFilter, [config.lapisField]: undefined }} /> ); @@ -108,6 +108,10 @@ export function BaselineSelector({ case 'location': { const filterIdentifier = locationFieldsToFilterIdentifier(config.locationFields); + const lapisFilerWithoutLocationFields = Object.fromEntries( + Object.entries(lapisFilter).filter(([key]) => !config.locationFields.includes(key)), + ); + return (