From 14ea3f7cc29e7059ab0be159966640da92248e24 Mon Sep 17 00:00:00 2001 From: Fran McDade <18710366+frano-m@users.noreply.github.com> Date: Fri, 30 Jan 2026 14:24:17 +1000 Subject: [PATCH 1/6] feat: export cohort to biodata catalyst (#4640) --- .../platform/ExportMethod/exportMethod.tsx | 21 ++++ .../ExportToPlatform/exportToPlatform.tsx | 107 ++++++++++++++++++ .../platform/ExportToPlatform/types.ts | 15 +++ app/components/index.tsx | 1 + app/shared/entities.ts | 4 +- .../anvil-cmg/common/viewModelBuilders.ts | 86 ++++++++++++-- e2e/anvil/anvil-dataset.spec.ts | 4 +- pages/export/biodata-catalyst.tsx | 28 +++++ .../dev/detail/dataset/export/export.ts | 9 +- site-config/anvil-cmg/dev/export/constants.ts | 37 +++++- site-config/anvil-cmg/dev/export/export.ts | 41 ++++++- site-config/anvil-cmg/dev/export/routes.ts | 5 + 12 files changed, 335 insertions(+), 23 deletions(-) create mode 100644 app/components/Export/components/AnVILExplorer/platform/ExportMethod/exportMethod.tsx create mode 100644 app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/exportToPlatform.tsx create mode 100644 app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/types.ts create mode 100644 pages/export/biodata-catalyst.tsx create mode 100644 site-config/anvil-cmg/dev/export/routes.ts diff --git a/app/components/Export/components/AnVILExplorer/platform/ExportMethod/exportMethod.tsx b/app/components/Export/components/AnVILExplorer/platform/ExportMethod/exportMethod.tsx new file mode 100644 index 000000000..6ff470cb8 --- /dev/null +++ b/app/components/Export/components/AnVILExplorer/platform/ExportMethod/exportMethod.tsx @@ -0,0 +1,21 @@ +import { JSX } from "react"; +import { ComponentProps } from "react"; +import { ExportMethod as DXExportMethod } from "@databiosphere/findable-ui/lib/components/Export/components/ExportMethod/exportMethod"; +import { useFeatureFlag } from "@databiosphere/findable-ui/lib/hooks/useFeatureFlag/useFeatureFlag"; +import { FEATURES } from "app/shared/entities"; + +/** + * Export method component for platform based export. + * Hidden if the platform based export feature is not enabled (NCPI Export). + * @param props - Export method component props. + * @returns Export method component. + */ +export const ExportMethod = ( + props: ComponentProps +): JSX.Element | null => { + const isEnabled = useFeatureFlag(FEATURES.NCPI_EXPORT); + + if (!isEnabled) return null; + + return ; +}; diff --git a/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/exportToPlatform.tsx b/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/exportToPlatform.tsx new file mode 100644 index 000000000..d6135d32a --- /dev/null +++ b/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/exportToPlatform.tsx @@ -0,0 +1,107 @@ +import { JSX } from "react"; +import { BUTTON_PROPS } from "@databiosphere/findable-ui/lib/components/common/Button/constants"; +import { useFileManifest } from "@databiosphere/findable-ui/lib/hooks/useFileManifest/useFileManifest"; +import { useFileManifestFileCount } from "@databiosphere/findable-ui/lib/hooks/useFileManifest/useFileManifestFileCount"; +import { useFileManifestFormat } from "@databiosphere/findable-ui/lib/hooks/useFileManifest/useFileManifestFormat"; +import { useRequestFileLocation } from "@databiosphere/findable-ui/lib/hooks/useRequestFileLocation"; +import { useRequestManifest } from "@databiosphere/findable-ui/lib/hooks/useRequestManifest/useRequestManifest"; +import { FluidPaper } from "@databiosphere/findable-ui/lib/components/common/Paper/components/FluidPaper/fluidPaper"; +import { Loading } from "@databiosphere/findable-ui/lib/components/Loading/loading"; +import { ExportManifestDownloadFormatForm } from "@databiosphere/findable-ui/lib/components/Export/components/ExportForm/components/ExportManifestDownloadFormatForm/exportManifestDownloadFormatForm"; +import { ExportButton } from "@databiosphere/findable-ui/lib/components/Export/components/ExportForm/components/ExportButton/exportButton"; +import { ExportForm } from "@databiosphere/findable-ui/lib/components/Export/components/ExportForm/exportForm"; +import { + Section, + SectionActions, + SectionContent, +} from "@databiosphere/findable-ui/lib/components/Export/export.styles"; +import { Button } from "@mui/material"; +import { + REL_ATTRIBUTE, + ANCHOR_TARGET, +} from "@databiosphere/findable-ui/lib/components/Links/common/entities"; +import { PAPER_PANEL_STYLE } from "@databiosphere/findable-ui/lib/components/common/Paper/paper"; +import { MANIFEST_DOWNLOAD_FORMAT } from "@databiosphere/findable-ui/lib/apis/azul/common/entities"; +import { Props } from "./types"; + +export const ExportToPlatform = ({ + buttonLabel, + description, + fileManifestState, + fileSummaryFacetName, + filters, + formFacet, + speciesFacetName, + successTitle, + title, +}: Props): JSX.Element => { + useFileManifest(filters, fileSummaryFacetName); + useFileManifestFileCount(filters, speciesFacetName, fileSummaryFacetName); + + const fileManifestFormatState = useFileManifestFormat( + MANIFEST_DOWNLOAD_FORMAT.VERBATIM_PFB + ); + const { fileManifestFormat } = fileManifestFormatState; + + const requestManifest = useRequestManifest(fileManifestFormat, formFacet); + const { requestMethod, requestUrl } = requestManifest; + + const response = useRequestFileLocation(requestUrl, requestMethod); + const url = ""; + + return url ? ( + +
+ +

{successTitle}

+
+ + + +
+
+ ) : ( +
+ + +
+ +

{title}

+

{description}

+
+ response.run()} + > + + +
+
+
+ ); +}; + +/** + * Build the export button. + * @param props - Button props e.g. "onClick" to request manifest. + * @returns button element. + */ +function renderButton({ ...props }): JSX.Element { + return Request Link; +} diff --git a/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/types.ts b/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/types.ts new file mode 100644 index 000000000..f82d77276 --- /dev/null +++ b/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/types.ts @@ -0,0 +1,15 @@ +import { FormFacet } from "@databiosphere/findable-ui/lib/components/Export/common/entities"; +import { FileManifestState } from "@databiosphere/findable-ui/lib/providers/fileManifestState"; +import { Filters } from "@databiosphere/findable-ui/lib/common/entities"; + +export interface Props { + buttonLabel: string; + description: string; + fileManifestState: FileManifestState; + fileSummaryFacetName: string; + filters: Filters; + formFacet: FormFacet; + speciesFacetName: string; + successTitle: string; + title: string; +} diff --git a/app/components/index.tsx b/app/components/index.tsx index 858bc122b..3183607e8 100644 --- a/app/components/index.tsx +++ b/app/components/index.tsx @@ -13,6 +13,7 @@ export { DownloadIconSmall, InventoryIconSmall, } from "@databiosphere/findable-ui/lib/components/common/CustomIcon/common/constants"; +export { ExportToPlatform } from "./Export/components/AnVILExplorer/platform/ExportToPlatform/exportToPlatform"; export { DiscourseIcon } from "@databiosphere/findable-ui/lib/components/common/CustomIcon/components/DiscourseIcon/discourseIcon"; export { GitHubIcon } from "@databiosphere/findable-ui/lib/components/common/CustomIcon/components/GitHubIcon/gitHubIcon"; export { OpenInNewIcon } from "@databiosphere/findable-ui/lib/components/common/CustomIcon/components/OpenInNewIcon/openInNewIcon"; diff --git a/app/shared/entities.ts b/app/shared/entities.ts index 27dfc0bf2..854471865 100644 --- a/app/shared/entities.ts +++ b/app/shared/entities.ts @@ -1,4 +1,6 @@ /** * Set of possible feature flags. */ -export enum FEATURES {} +export enum FEATURES { + NCPI_EXPORT = "ncpiexport", +} diff --git a/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts b/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts index 286aa397f..241409f50 100644 --- a/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts +++ b/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts @@ -39,16 +39,13 @@ import { ChipProps as MChipProps, FadeProps as MFadeProps, } from "@mui/material"; -import React, { ReactNode } from "react"; +import React, { ComponentProps, ReactNode } from "react"; import { ANVIL_CMG_CATEGORY_KEY, ANVIL_CMG_CATEGORY_LABEL, DATASET_RESPONSE, } from "../../../../../site-config/anvil-cmg/category"; -import { - ROUTE_EXPORT_TO_TERRA, - ROUTE_MANIFEST_DOWNLOAD, -} from "../../../../../site-config/anvil-cmg/dev/export/constants"; +import { ROUTES } from "../../../../../site-config/anvil-cmg/dev/export/routes"; import { AggregatedBioSampleResponse, AggregatedDatasetResponse, @@ -452,7 +449,7 @@ export const buildDatasetExportMethodManifestDownload = ( buttonLabel: "Request File Manifest", description: "Request a file manifest suitable for downloading this dataset to your HPC cluster or local machine.", - route: `${datasetPath}${ROUTE_MANIFEST_DOWNLOAD}`, + route: `${datasetPath}${ROUTES.MANIFEST_DOWNLOAD}`, title: "Download a File Manifest with Metadata", }; }; @@ -486,7 +483,7 @@ export const buildDatasetExportMethodTerra = ( buttonLabel: "Analyze in Terra", description: "Terra is a biomedical research platform to analyze data using workflows, Jupyter Notebooks, RStudio, and Galaxy.", - route: `${datasetPath}${ROUTE_EXPORT_TO_TERRA}`, + route: `${datasetPath}${ROUTES.TERRA}`, title: "Export Dataset Data and Metadata to Terra Workspace", }; }; @@ -744,7 +741,7 @@ export const buildExportMethodManifestDownload = ( buttonLabel: "Request File Manifest", description: "Request a file manifest for the current query containing the full list of selected files and the metadata for each file.", - route: ROUTE_MANIFEST_DOWNLOAD, + route: ROUTES.MANIFEST_DOWNLOAD, title: "Download a File Manifest with Metadata for the Selected Data", }; }; @@ -764,7 +761,7 @@ export const buildExportMethodTerra = ( buttonLabel: "Analyze in Terra", description: "Terra is a biomedical research platform to analyze data using workflows, Jupyter Notebooks, RStudio, and Galaxy.", - route: ROUTE_EXPORT_TO_TERRA, + route: ROUTES.TERRA, title: "Export Study Data and Metadata to Terra Workspace", }; }; @@ -793,6 +790,77 @@ export const buildExportSelectedDataSummary = ( }; }; +/** + * Build props for ExportToPlatform component. + * @param props - Props to pass to the ExportToPlatform component. + * @returns model to be used as props for the ExportToPlatform component. + */ +export const buildExportToPlatform = ( + props: Pick< + ComponentProps, + "buttonLabel" | "description" | "successTitle" | "title" + > +): (( + _: unknown, + viewContext: ViewContext +) => ComponentProps) => { + return (_: unknown, viewContext: ViewContext) => { + const { + exploreState: { filterState }, + fileManifestState, + } = viewContext; + return { + ...props, + fileManifestState, + fileSummaryFacetName: ANVIL_CMG_CATEGORY_KEY.FILE_FILE_FORMAT, + filters: filterState, + formFacet: getFormFacets(fileManifestState), + speciesFacetName: ANVIL_CMG_CATEGORY_KEY.DONOR_ORGANISM_TYPE, + }; + }; +}; + +/** + * Build props for ExportToPlatform BackPageHero component. + * @param title - Title of the export method. + * @returns model to be used as props for the BackPageHero component. + */ +export const buildExportToPlatformHero = ( + title: string +): (( + _: unknown, + viewContext: ViewContext +) => React.ComponentProps) => { + return (_, viewContext) => { + const { + exploreState: { tabValue }, + } = viewContext; + return getExportMethodHero(tabValue, title); + }; +}; + +/** + * Build props for ExportMethod component for display of the export to [platform] metadata section. + * @param props - Props to pass to the ExportMethod component. + * @returns model to be used as props for the ExportMethod component. + */ +export const buildExportToPlatformMethod = ( + props: Pick< + ComponentProps, + "buttonLabel" | "description" | "route" | "title" + > +): (( + _: unknown, + viewContext: ViewContext +) => ComponentProps) => { + return (_: unknown, viewContext: ViewContext) => { + return { + ...props, + ...getExportMethodAccessibility(viewContext), + }; + }; +}; + /** * Build props for ExportToTerra component. * @param _ - Unused. diff --git a/e2e/anvil/anvil-dataset.spec.ts b/e2e/anvil/anvil-dataset.spec.ts index 1120431a0..ad84ef18e 100644 --- a/e2e/anvil/anvil-dataset.spec.ts +++ b/e2e/anvil/anvil-dataset.spec.ts @@ -10,7 +10,7 @@ import { DatasetAccess, } from "./common/constants"; import { MUI_CLASSES } from "../features/common/constants"; -import { ROUTE_MANIFEST_DOWNLOAD } from "../../site-config/anvil-cmg/dev/export/constants"; +import { ROUTES } from "../../site-config/anvil-cmg/dev/export/routes"; import { ANVIL_CMG_CATEGORY_KEY } from "../../site-config/anvil-cmg/category"; const { describe } = test; @@ -88,7 +88,7 @@ describe("Dataset", () => { // Navigate to the export file manifest page. const currentUrl = page.url(); - await page.goto(`${currentUrl}${ROUTE_MANIFEST_DOWNLOAD}`); + await page.goto(`${currentUrl}${ROUTES.MANIFEST_DOWNLOAD}`); // Confirm the login alert is displayed. await expect( diff --git a/pages/export/biodata-catalyst.tsx b/pages/export/biodata-catalyst.tsx new file mode 100644 index 000000000..5ba041464 --- /dev/null +++ b/pages/export/biodata-catalyst.tsx @@ -0,0 +1,28 @@ +import { JSX } from "react"; +import { ExportMethodView } from "@databiosphere/findable-ui/lib/views/ExportMethodView/exportMethodView"; +import { GetStaticProps } from "next"; +import { useFeatureFlag } from "@databiosphere/findable-ui/lib/hooks/useFeatureFlag/useFeatureFlag"; +import { FEATURES } from "../../app/shared/entities"; +import Error from "next/error"; + +export const getStaticProps: GetStaticProps = async () => { + return { + props: { + pageTitle: "Export to NHLBI BioData Catalyst", + }, + }; +}; + +/** + * Export method page for BioData Catalyst. + * @returns export method view component. + */ +const ExportMethodPage = (): JSX.Element => { + const isEnabled = useFeatureFlag(FEATURES.NCPI_EXPORT); + + if (!isEnabled) return ; + + return ; +}; + +export default ExportMethodPage; diff --git a/site-config/anvil-cmg/dev/detail/dataset/export/export.ts b/site-config/anvil-cmg/dev/detail/dataset/export/export.ts index eb12eb170..98729284b 100644 --- a/site-config/anvil-cmg/dev/detail/dataset/export/export.ts +++ b/site-config/anvil-cmg/dev/detail/dataset/export/export.ts @@ -6,10 +6,7 @@ import { sideColumn as exportSideColumn } from "../../../export/exportSideColumn import * as V from "../../../../../../app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders"; import * as C from "../../../../../../app/components"; import { DatasetsResponse } from "app/apis/azul/anvil-cmg/common/responses"; -import { - ROUTE_EXPORT_TO_TERRA, - ROUTE_MANIFEST_DOWNLOAD, -} from "../../../export/constants"; +import { ROUTES } from "../../../export/routes"; import * as MDX from "../../../../../../app/components/common/MDXContent/anvil-cmg"; /** @@ -66,7 +63,7 @@ export const exportConfig: ExportConfig = { viewBuilder: V.renderDatasetExport, } as ComponentConfig, ], - route: ROUTE_EXPORT_TO_TERRA, + route: ROUTES.TERRA, top: [ { children: [DATASET_ACCESSIBILITY_BADGE], @@ -119,7 +116,7 @@ export const exportConfig: ExportConfig = { viewBuilder: V.renderDatasetExport, } as ComponentConfig, ], - route: ROUTE_MANIFEST_DOWNLOAD, + route: ROUTES.MANIFEST_DOWNLOAD, top: [ { children: [DATASET_ACCESSIBILITY_BADGE], diff --git a/site-config/anvil-cmg/dev/export/constants.ts b/site-config/anvil-cmg/dev/export/constants.ts index 1af8b4659..c6cec8685 100644 --- a/site-config/anvil-cmg/dev/export/constants.ts +++ b/site-config/anvil-cmg/dev/export/constants.ts @@ -1,2 +1,35 @@ -export const ROUTE_EXPORT_TO_TERRA = "/export/export-to-terra"; -export const ROUTE_MANIFEST_DOWNLOAD = "/export/download-manifest"; +import { ROUTES } from "./routes"; +import { ComponentProps } from "react"; +import { ExportMethod } from "@databiosphere/findable-ui/lib/components/Export/components/ExportMethod/exportMethod"; +import { ExportToPlatform } from "../../../../app/components"; + +export const EXPORTS: Record< + string, + Pick< + ComponentProps, + "buttonLabel" | "description" | "successTitle" | "title" + > +> = { + BIO_DATA_CATALYST: { + buttonLabel: "Open BioData Catalyst", + description: + "BDC-SB is a cloud workspace for analysis, storage, and computation using workflows, Jupyter Notebooks, and RStudio.", + successTitle: "Your BioData Catalyst Workspace Link is Ready", + title: "Analyze in BioData Catalyst", + }, +}; + +export const EXPORT_METHODS: Record< + string, + Pick< + ComponentProps, + "buttonLabel" | "description" | "route" | "title" + > +> = { + BIO_DATA_CATALYST: { + buttonLabel: "Analyze in NHLBI BioData Catalyst", + description: EXPORTS.BIO_DATA_CATALYST.description, + route: ROUTES.BIO_DATA_CATALYST, + title: "Export to BioData Catalyst Powered by Seven Bridges (BDC-SB)", + }, +}; diff --git a/site-config/anvil-cmg/dev/export/export.ts b/site-config/anvil-cmg/dev/export/export.ts index fcd1209b8..7a666313b 100644 --- a/site-config/anvil-cmg/dev/export/export.ts +++ b/site-config/anvil-cmg/dev/export/export.ts @@ -4,9 +4,11 @@ import { } from "@databiosphere/findable-ui/lib/config/entities"; import * as C from "../../../../app/components"; import * as V from "../../../../app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders"; -import { ROUTE_EXPORT_TO_TERRA, ROUTE_MANIFEST_DOWNLOAD } from "./constants"; +import { ROUTES } from "./routes"; import { mainColumn as exportMainColumn } from "./exportMainColumn"; import { sideColumn as exportSideColumn } from "./exportSideColumn"; +import { ExportMethod } from "../../../../app/components/Export/components/AnVILExplorer/platform/ExportMethod/exportMethod"; +import { EXPORT_METHODS, EXPORTS } from "./constants"; export const exportConfig: ExportConfig = { exportMethods: [ @@ -27,7 +29,7 @@ export const exportConfig: ExportConfig = { /* sideColumn */ ...exportSideColumn, ], - route: ROUTE_EXPORT_TO_TERRA, + route: ROUTES.TERRA, top: [ { component: C.BackPageHero, @@ -35,6 +37,33 @@ export const exportConfig: ExportConfig = { } as ComponentConfig, ], }, + { + mainColumn: [ + /* mainColumn - top section - warning - some datasets are not available */ + ...exportMainColumn, + /* mainColumn */ + { + children: [ + { + component: C.ExportToPlatform, + viewBuilder: V.buildExportToPlatform(EXPORTS.BIO_DATA_CATALYST), + } as ComponentConfig, + ], + component: C.BackPageContentMainColumn, + } as ComponentConfig, + /* sideColumn */ + ...exportSideColumn, + ], + route: ROUTES.BIO_DATA_CATALYST, + top: [ + { + component: C.BackPageHero, + viewBuilder: V.buildExportToPlatformHero( + "Export to BioData Catalyst" + ), + } as ComponentConfig, + ], + }, { mainColumn: [ /* mainColumn - top section - warning - some datasets are not available */ @@ -52,7 +81,7 @@ export const exportConfig: ExportConfig = { /* sideColumn */ ...exportSideColumn, ], - route: ROUTE_MANIFEST_DOWNLOAD, + route: ROUTES.MANIFEST_DOWNLOAD, top: [ { component: C.BackPageHero, @@ -75,6 +104,12 @@ export const exportConfig: ExportConfig = { component: C.ExportMethod, viewBuilder: V.buildExportMethodTerra, } as ComponentConfig, + { + component: ExportMethod, + viewBuilder: V.buildExportToPlatformMethod( + EXPORT_METHODS.BIO_DATA_CATALYST + ), + } as ComponentConfig, { component: C.ExportMethod, viewBuilder: V.buildExportMethodManifestDownload, diff --git a/site-config/anvil-cmg/dev/export/routes.ts b/site-config/anvil-cmg/dev/export/routes.ts new file mode 100644 index 000000000..b7c69a5d1 --- /dev/null +++ b/site-config/anvil-cmg/dev/export/routes.ts @@ -0,0 +1,5 @@ +export const ROUTES = { + BIO_DATA_CATALYST: "/export/biodata-catalyst", + MANIFEST_DOWNLOAD: "/export/download-manifest", + TERRA: "/export/export-to-terra", +} as const; From a13b78ea6717096ccd4a99fbd7f35ba48450a589 Mon Sep 17 00:00:00 2001 From: Fran McDade <18710366+frano-m@users.noreply.github.com> Date: Fri, 30 Jan 2026 15:39:42 +1000 Subject: [PATCH 2/6] feat: export cohot to cavatica (#4642) --- pages/export/cavatica.tsx | 28 +++++++++++++++++ site-config/anvil-cmg/dev/export/constants.ts | 13 ++++++++ site-config/anvil-cmg/dev/export/export.ts | 31 +++++++++++++++++++ site-config/anvil-cmg/dev/export/routes.ts | 1 + 4 files changed, 73 insertions(+) create mode 100644 pages/export/cavatica.tsx diff --git a/pages/export/cavatica.tsx b/pages/export/cavatica.tsx new file mode 100644 index 000000000..580ab3c2c --- /dev/null +++ b/pages/export/cavatica.tsx @@ -0,0 +1,28 @@ +import { JSX } from "react"; +import { ExportMethodView } from "@databiosphere/findable-ui/lib/views/ExportMethodView/exportMethodView"; +import { GetStaticProps } from "next"; +import { useFeatureFlag } from "@databiosphere/findable-ui/lib/hooks/useFeatureFlag/useFeatureFlag"; +import { FEATURES } from "../../app/shared/entities"; +import Error from "next/error"; + +export const getStaticProps: GetStaticProps = async () => { + return { + props: { + pageTitle: "Export to CAVATICA", + }, + }; +}; + +/** + * Export method page for CAVATICA. + * @returns export method view component. + */ +const ExportMethodPage = (): JSX.Element => { + const isEnabled = useFeatureFlag(FEATURES.NCPI_EXPORT); + + if (!isEnabled) return ; + + return ; +}; + +export default ExportMethodPage; diff --git a/site-config/anvil-cmg/dev/export/constants.ts b/site-config/anvil-cmg/dev/export/constants.ts index c6cec8685..1cd1f1855 100644 --- a/site-config/anvil-cmg/dev/export/constants.ts +++ b/site-config/anvil-cmg/dev/export/constants.ts @@ -17,6 +17,13 @@ export const EXPORTS: Record< successTitle: "Your BioData Catalyst Workspace Link is Ready", title: "Analyze in BioData Catalyst", }, + CAVATICA: { + buttonLabel: "Open CAVATICA", + description: + "CAVATICA is a cloud workspace for analysis, storage, and computation using workflows, Jupyter Notebooks, and RStudio.", + successTitle: "Your CAVATICA Workspace Link is Ready", + title: "Analyze in CAVATICA", + }, }; export const EXPORT_METHODS: Record< @@ -32,4 +39,10 @@ export const EXPORT_METHODS: Record< route: ROUTES.BIO_DATA_CATALYST, title: "Export to BioData Catalyst Powered by Seven Bridges (BDC-SB)", }, + CAVATICA: { + buttonLabel: "Analyze in CAVATICA", + description: EXPORTS.CAVATICA.description, + route: ROUTES.CAVATICA, + title: "Export to CAVATICA", + }, }; diff --git a/site-config/anvil-cmg/dev/export/export.ts b/site-config/anvil-cmg/dev/export/export.ts index 7a666313b..d05f6180a 100644 --- a/site-config/anvil-cmg/dev/export/export.ts +++ b/site-config/anvil-cmg/dev/export/export.ts @@ -64,6 +64,31 @@ export const exportConfig: ExportConfig = { } as ComponentConfig, ], }, + { + mainColumn: [ + /* mainColumn - top section - warning - some datasets are not available */ + ...exportMainColumn, + /* mainColumn */ + { + children: [ + { + component: C.ExportToPlatform, + viewBuilder: V.buildExportToPlatform(EXPORTS.CAVATICA), + } as ComponentConfig, + ], + component: C.BackPageContentMainColumn, + } as ComponentConfig, + /* sideColumn */ + ...exportSideColumn, + ], + route: ROUTES.CAVATICA, + top: [ + { + component: C.BackPageHero, + viewBuilder: V.buildExportToPlatformHero("Export to CAVATICA"), + } as ComponentConfig, + ], + }, { mainColumn: [ /* mainColumn - top section - warning - some datasets are not available */ @@ -110,6 +135,12 @@ export const exportConfig: ExportConfig = { EXPORT_METHODS.BIO_DATA_CATALYST ), } as ComponentConfig, + { + component: ExportMethod, + viewBuilder: V.buildExportToPlatformMethod( + EXPORT_METHODS.CAVATICA + ), + } as ComponentConfig, { component: C.ExportMethod, viewBuilder: V.buildExportMethodManifestDownload, diff --git a/site-config/anvil-cmg/dev/export/routes.ts b/site-config/anvil-cmg/dev/export/routes.ts index b7c69a5d1..ec3daf379 100644 --- a/site-config/anvil-cmg/dev/export/routes.ts +++ b/site-config/anvil-cmg/dev/export/routes.ts @@ -1,5 +1,6 @@ export const ROUTES = { BIO_DATA_CATALYST: "/export/biodata-catalyst", + CAVATICA: "/export/cavatica", MANIFEST_DOWNLOAD: "/export/download-manifest", TERRA: "/export/export-to-terra", } as const; From 166f30c56730c8a7f2749e503f48beafcd0c40b0 Mon Sep 17 00:00:00 2001 From: Fran McDade <18710366+frano-m@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:15:17 +1000 Subject: [PATCH 3/6] feat: export cohort to cancel genomics cloud (#4644) --- __mocks__/ky.ts | 31 ++ __mocks__/mdx.ts | 6 + __mocks__/next-mdx-remote.tsx | 13 + .../site-config/export-constants.test.ts | 259 +++++++++++++ __tests__/viewModelBuilders/export.test.ts | 354 ++++++++++++++++++ jest.config.js | 4 + pages/export/cancer-genomics-cloud.tsx | 28 ++ site-config/anvil-cmg/dev/export/constants.ts | 13 + site-config/anvil-cmg/dev/export/export.ts | 35 ++ site-config/anvil-cmg/dev/export/routes.ts | 1 + 10 files changed, 744 insertions(+) create mode 100644 __mocks__/ky.ts create mode 100644 __mocks__/mdx.ts create mode 100644 __mocks__/next-mdx-remote.tsx create mode 100644 __tests__/site-config/export-constants.test.ts create mode 100644 __tests__/viewModelBuilders/export.test.ts create mode 100644 pages/export/cancer-genomics-cloud.tsx diff --git a/__mocks__/ky.ts b/__mocks__/ky.ts new file mode 100644 index 000000000..17854ddeb --- /dev/null +++ b/__mocks__/ky.ts @@ -0,0 +1,31 @@ +/** + * Mock implementation of ky HTTP client for testing. + * + * This mock provides the minimal structure needed to allow imports to resolve. + * The tests don't make actual HTTP calls - they test view model builder functions. + * @returns Mock ky instance. + */ +const mockKyInstance = { + get: jest.fn().mockReturnValue({ + json: jest.fn().mockResolvedValue({}), + text: jest.fn().mockResolvedValue(""), + }), + post: jest.fn().mockReturnValue({ + json: jest.fn().mockResolvedValue({}), + text: jest.fn().mockResolvedValue(""), + }), +}; + +const mockKy = Object.assign( + jest.fn().mockReturnValue({ + json: jest.fn().mockResolvedValue({}), + text: jest.fn().mockResolvedValue(""), + }), + { + create: jest.fn().mockReturnValue(mockKyInstance), + get: mockKyInstance.get, + post: mockKyInstance.post, + } +); + +export default mockKy; diff --git a/__mocks__/mdx.ts b/__mocks__/mdx.ts new file mode 100644 index 000000000..194f47a33 --- /dev/null +++ b/__mocks__/mdx.ts @@ -0,0 +1,6 @@ +/** + * Mock for MDX files in tests. + * @returns Null component. + */ +const MockMDXComponent = (): null => null; +export default MockMDXComponent; diff --git a/__mocks__/next-mdx-remote.tsx b/__mocks__/next-mdx-remote.tsx new file mode 100644 index 000000000..ce722fec3 --- /dev/null +++ b/__mocks__/next-mdx-remote.tsx @@ -0,0 +1,13 @@ +import React from "react"; + +/** + * Mock implementation of next-mdx-remote for testing. + */ + +export const MDXRemote = jest.fn(({ children }) =>
{children}
); + +export const serialize = jest.fn().mockResolvedValue({ + compiledSource: "", + frontmatter: {}, + scope: {}, +}); diff --git a/__tests__/site-config/export-constants.test.ts b/__tests__/site-config/export-constants.test.ts new file mode 100644 index 000000000..fdfe8582e --- /dev/null +++ b/__tests__/site-config/export-constants.test.ts @@ -0,0 +1,259 @@ +import { + EXPORTS, + EXPORT_METHODS, +} from "../../site-config/anvil-cmg/dev/export/constants"; +import { ROUTES } from "../../site-config/anvil-cmg/dev/export/routes"; + +describe("ROUTES constants", () => { + it("has all required export routes defined", () => { + expect(ROUTES.BIO_DATA_CATALYST).toBeDefined(); + expect(ROUTES.CAVATICA).toBeDefined(); + expect(ROUTES.CANCER_GENOMICS_CLOUD).toBeDefined(); + expect(ROUTES.MANIFEST_DOWNLOAD).toBeDefined(); + expect(ROUTES.TERRA).toBeDefined(); + }); + + it("all routes are valid URL paths starting with /export/", () => { + Object.values(ROUTES).forEach((route) => { + expect(route).toMatch(/^\/export\/.+$/); + }); + }); + + it("no duplicate routes exist", () => { + const routes = Object.values(ROUTES); + const uniqueRoutes = new Set(routes); + expect(uniqueRoutes.size).toBe(routes.length); + }); + + it("has correct route for BioData Catalyst", () => { + expect(ROUTES.BIO_DATA_CATALYST).toBe("/export/biodata-catalyst"); + }); + + it("has correct route for CAVATICA", () => { + expect(ROUTES.CAVATICA).toBe("/export/cavatica"); + }); + + it("has correct route for Cancer Genomics Cloud", () => { + expect(ROUTES.CANCER_GENOMICS_CLOUD).toBe("/export/cancer-genomics-cloud"); + }); +}); + +describe("EXPORTS constants", () => { + const REQUIRED_EXPORT_KEYS = [ + "buttonLabel", + "description", + "successTitle", + "title", + ] as const; + + describe("structure validation", () => { + it("has all three platform exports defined", () => { + expect(EXPORTS.BIO_DATA_CATALYST).toBeDefined(); + expect(EXPORTS.CAVATICA).toBeDefined(); + expect(EXPORTS.CANCER_GENOMICS_CLOUD).toBeDefined(); + }); + + it.each(Object.entries(EXPORTS))( + "%s has all required keys", + (_, config) => { + for (const key of REQUIRED_EXPORT_KEYS) { + expect(config).toHaveProperty(key); + expect(config[key]).toBeTruthy(); + } + } + ); + + it.each(Object.entries(EXPORTS))( + "%s has all non-empty string values", + (_, config) => { + for (const [, value] of Object.entries(config)) { + expect(typeof value).toBe("string"); + expect(value.trim().length).toBeGreaterThan(0); + } + } + ); + }); + + describe("BioData Catalyst export config", () => { + it("has correct buttonLabel", () => { + expect(EXPORTS.BIO_DATA_CATALYST.buttonLabel).toBe( + "Open BioData Catalyst" + ); + }); + + it("has correct title", () => { + expect(EXPORTS.BIO_DATA_CATALYST.title).toBe( + "Analyze in BioData Catalyst" + ); + }); + + it("has correct successTitle", () => { + expect(EXPORTS.BIO_DATA_CATALYST.successTitle).toBe( + "Your BioData Catalyst Workspace Link is Ready" + ); + }); + + it("description mentions BDC-SB", () => { + expect(EXPORTS.BIO_DATA_CATALYST.description).toContain("BDC-SB"); + }); + }); + + describe("CAVATICA export config", () => { + it("has correct buttonLabel", () => { + expect(EXPORTS.CAVATICA.buttonLabel).toBe("Open CAVATICA"); + }); + + it("has correct title", () => { + expect(EXPORTS.CAVATICA.title).toBe("Analyze in CAVATICA"); + }); + + it("has correct successTitle", () => { + expect(EXPORTS.CAVATICA.successTitle).toBe( + "Your CAVATICA Workspace Link is Ready" + ); + }); + + it("description mentions CAVATICA", () => { + expect(EXPORTS.CAVATICA.description).toContain("CAVATICA"); + }); + }); + + describe("Cancer Genomics Cloud export config", () => { + it("has correct buttonLabel", () => { + expect(EXPORTS.CANCER_GENOMICS_CLOUD.buttonLabel).toBe( + "Open Cancer Genomics Cloud" + ); + }); + + it("has correct title", () => { + expect(EXPORTS.CANCER_GENOMICS_CLOUD.title).toBe( + "Analyze in Cancer Genomics Cloud" + ); + }); + + it("has correct successTitle", () => { + expect(EXPORTS.CANCER_GENOMICS_CLOUD.successTitle).toBe( + "Your Cancer Genomics Cloud Workspace Link is Ready" + ); + }); + + it("description mentions CGC", () => { + expect(EXPORTS.CANCER_GENOMICS_CLOUD.description).toContain("CGC"); + }); + }); +}); + +describe("EXPORT_METHODS constants", () => { + const REQUIRED_METHOD_KEYS = [ + "buttonLabel", + "description", + "route", + "title", + ] as const; + + describe("structure validation", () => { + it("has all three platform methods defined", () => { + expect(EXPORT_METHODS.BIO_DATA_CATALYST).toBeDefined(); + expect(EXPORT_METHODS.CAVATICA).toBeDefined(); + expect(EXPORT_METHODS.CANCER_GENOMICS_CLOUD).toBeDefined(); + }); + + it.each(Object.entries(EXPORT_METHODS))( + "%s has all required keys", + (_, config) => { + for (const key of REQUIRED_METHOD_KEYS) { + expect(config).toHaveProperty(key); + expect(config[key]).toBeTruthy(); + } + } + ); + }); + + describe("route references", () => { + it("BIO_DATA_CATALYST method references correct route", () => { + expect(EXPORT_METHODS.BIO_DATA_CATALYST.route).toBe( + ROUTES.BIO_DATA_CATALYST + ); + }); + + it("CAVATICA method references correct route", () => { + expect(EXPORT_METHODS.CAVATICA.route).toBe(ROUTES.CAVATICA); + }); + + it("CANCER_GENOMICS_CLOUD method references correct route", () => { + expect(EXPORT_METHODS.CANCER_GENOMICS_CLOUD.route).toBe( + ROUTES.CANCER_GENOMICS_CLOUD + ); + }); + }); + + describe("description consistency", () => { + it("BIO_DATA_CATALYST method description matches EXPORTS description", () => { + expect(EXPORT_METHODS.BIO_DATA_CATALYST.description).toBe( + EXPORTS.BIO_DATA_CATALYST.description + ); + }); + + it("CAVATICA method description matches EXPORTS description", () => { + expect(EXPORT_METHODS.CAVATICA.description).toBe( + EXPORTS.CAVATICA.description + ); + }); + + it("CANCER_GENOMICS_CLOUD method description matches EXPORTS description", () => { + expect(EXPORT_METHODS.CANCER_GENOMICS_CLOUD.description).toBe( + EXPORTS.CANCER_GENOMICS_CLOUD.description + ); + }); + }); + + describe("BioData Catalyst method config", () => { + it("has correct buttonLabel", () => { + expect(EXPORT_METHODS.BIO_DATA_CATALYST.buttonLabel).toBe( + "Analyze in NHLBI BioData Catalyst" + ); + }); + + it("has correct title mentioning Seven Bridges", () => { + expect(EXPORT_METHODS.BIO_DATA_CATALYST.title).toContain("Seven Bridges"); + expect(EXPORT_METHODS.BIO_DATA_CATALYST.title).toContain("BDC-SB"); + }); + }); + + describe("CAVATICA method config", () => { + it("has correct buttonLabel", () => { + expect(EXPORT_METHODS.CAVATICA.buttonLabel).toBe("Analyze in CAVATICA"); + }); + + it("has correct title", () => { + expect(EXPORT_METHODS.CAVATICA.title).toBe("Export to CAVATICA"); + }); + }); + + describe("Cancer Genomics Cloud method config", () => { + it("has correct buttonLabel", () => { + expect(EXPORT_METHODS.CANCER_GENOMICS_CLOUD.buttonLabel).toBe( + "Analyze in Cancer Genomics Cloud" + ); + }); + + it("has correct title mentioning CGC", () => { + expect(EXPORT_METHODS.CANCER_GENOMICS_CLOUD.title).toContain("CGC"); + }); + }); +}); + +describe("Cross-validation between EXPORTS and EXPORT_METHODS", () => { + it("both have the same platforms defined", () => { + const exportPlatforms = Object.keys(EXPORTS).sort(); + const methodPlatforms = Object.keys(EXPORT_METHODS).sort(); + expect(exportPlatforms).toEqual(methodPlatforms); + }); + + it("all method routes are valid paths", () => { + const routeValues = Object.values(ROUTES); + Object.values(EXPORT_METHODS).forEach((method) => { + expect(routeValues).toContain(method.route); + }); + }); +}); diff --git a/__tests__/viewModelBuilders/export.test.ts b/__tests__/viewModelBuilders/export.test.ts new file mode 100644 index 000000000..d0b27ed5b --- /dev/null +++ b/__tests__/viewModelBuilders/export.test.ts @@ -0,0 +1,354 @@ +import { Breadcrumb } from "@databiosphere/findable-ui/lib/components/common/Breadcrumbs/breadcrumbs"; +import { ViewContext } from "@databiosphere/findable-ui/lib/config/entities"; +import { FileFacet } from "@databiosphere/findable-ui/lib/hooks/useFileManifest/common/entities"; +import { FileManifestState } from "@databiosphere/findable-ui/lib/providers/fileManifestState"; +import { + buildExportToPlatform, + buildExportToPlatformHero, + buildExportToPlatformMethod, +} from "../../app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders"; +import { ANVIL_CMG_CATEGORY_KEY } from "../../site-config/anvil-cmg/category"; +import { + EXPORTS, + EXPORT_METHODS, +} from "../../site-config/anvil-cmg/dev/export/constants"; + +// Mock file manifest state +const createMockFileManifestState = ( + overrides: Partial = {} +): FileManifestState => ({ + fileCount: undefined, + fileSummary: undefined, + fileSummaryFacetName: undefined, + fileSummaryFilters: [], + filesFacets: [], + filters: [], + isEnabled: true, + isFacetsLoading: false, + isFacetsSuccess: true, + isFileSummaryLoading: false, + isLoading: false, + isSummaryLoading: false, + summary: undefined, + ...overrides, +}); + +// Mock FileFacet +const createMockFileFacet = ( + overrides: Partial = {} +): FileFacet => ({ + name: "files.file_format", + selected: false, + selectedTermCount: 0, + selectedTerms: [], + termCount: 5, + terms: [{ count: 10, name: "bam", selected: false }], + termsByName: new Map(), + total: 10, + ...overrides, +}); + +// Mock view context +const createMockViewContext = ( + overrides: Partial> = {} +): ViewContext => + ({ + cellContext: undefined, + entityConfig: undefined, + exploreState: { + categoryViews: [], + filterState: [{ categoryKey: "test.filter", value: ["value1"] }], + tabValue: "datasets", + }, + fileManifestState: createMockFileManifestState(), + ...overrides, + }) as ViewContext; + +describe("buildExportToPlatform", () => { + describe("returns correct props structure", () => { + it("spreads provided props into result", () => { + const props = EXPORTS.BIO_DATA_CATALYST; + const builder = buildExportToPlatform(props); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.buttonLabel).toBe(props.buttonLabel); + expect(result.description).toBe(props.description); + expect(result.successTitle).toBe(props.successTitle); + expect(result.title).toBe(props.title); + }); + + it("includes fileManifestState from viewContext", () => { + const props = EXPORTS.CAVATICA; + const builder = buildExportToPlatform(props); + const fileManifestState = createMockFileManifestState({ + isFacetsSuccess: true, + }); + const viewContext = createMockViewContext({ fileManifestState }); + + const result = builder(undefined, viewContext); + + expect(result.fileManifestState).toBe(fileManifestState); + }); + + it("includes correct fileSummaryFacetName", () => { + const props = EXPORTS.CANCER_GENOMICS_CLOUD; + const builder = buildExportToPlatform(props); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.fileSummaryFacetName).toBe( + ANVIL_CMG_CATEGORY_KEY.FILE_FILE_FORMAT + ); + }); + + it("includes filters from exploreState", () => { + const props = EXPORTS.BIO_DATA_CATALYST; + const builder = buildExportToPlatform(props); + const filterState = [ + { categoryKey: "datasets.title", value: ["Test Dataset"] }, + ]; + const viewContext = createMockViewContext({ + exploreState: { + categoryViews: [], + filterState, + tabValue: "datasets", + }, + } as unknown as Partial>); + + const result = builder(undefined, viewContext); + + expect(result.filters).toBe(filterState); + }); + + it("includes correct speciesFacetName", () => { + const props = EXPORTS.CAVATICA; + const builder = buildExportToPlatform(props); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.speciesFacetName).toBe( + ANVIL_CMG_CATEGORY_KEY.DONOR_ORGANISM_TYPE + ); + }); + }); + + describe("works correctly for each platform", () => { + it("returns correct props for BioData Catalyst config", () => { + const builder = buildExportToPlatform(EXPORTS.BIO_DATA_CATALYST); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.buttonLabel).toBe("Open BioData Catalyst"); + expect(result.title).toBe("Analyze in BioData Catalyst"); + }); + + it("returns correct props for CAVATICA config", () => { + const builder = buildExportToPlatform(EXPORTS.CAVATICA); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.buttonLabel).toBe("Open CAVATICA"); + expect(result.title).toBe("Analyze in CAVATICA"); + }); + + it("returns correct props for Cancer Genomics Cloud config", () => { + const builder = buildExportToPlatform(EXPORTS.CANCER_GENOMICS_CLOUD); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.buttonLabel).toBe("Open Cancer Genomics Cloud"); + expect(result.title).toBe("Analyze in Cancer Genomics Cloud"); + }); + }); +}); + +describe("buildExportToPlatformHero", () => { + it("returns provided title", () => { + const title = "Export to BioData Catalyst"; + const builder = buildExportToPlatformHero(title); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.title).toBe(title); + }); + + it("builds correct breadcrumb structure", () => { + const title = "Export to CAVATICA"; + const builder = buildExportToPlatformHero(title); + const viewContext = createMockViewContext({ + exploreState: { + categoryViews: [], + filterState: [], + tabValue: "files", + }, + } as unknown as Partial>); + + const result = builder(undefined, viewContext); + const breadcrumbs = result.breadcrumbs as Breadcrumb[]; + + expect(result.breadcrumbs).toBeDefined(); + expect(Array.isArray(result.breadcrumbs)).toBe(true); + expect(breadcrumbs.length).toBe(3); + + // First breadcrumb should link to explore + expect(breadcrumbs[0].text).toBe("Explore"); + expect(breadcrumbs[0].path).toContain("files"); + + // Second breadcrumb should link to export + expect(breadcrumbs[1].text).toBe("Export Selected Data"); + expect(breadcrumbs[1].path).toBe("/export"); + + // Third breadcrumb should be the current page title + expect(breadcrumbs[2].text).toBe(title); + }); + + it("uses tabValue from exploreState for explore breadcrumb path", () => { + const title = "Export to Cancer Genomics Cloud"; + const builder = buildExportToPlatformHero(title); + const viewContext = createMockViewContext({ + exploreState: { + categoryViews: [], + filterState: [], + tabValue: "datasets", + }, + } as unknown as Partial>); + + const result = builder(undefined, viewContext); + const breadcrumbs = result.breadcrumbs as Breadcrumb[]; + + expect(breadcrumbs[0].path).toBe("/datasets"); + }); +}); + +describe("buildExportToPlatformMethod", () => { + it("returns route from props", () => { + const props = EXPORT_METHODS.BIO_DATA_CATALYST; + const builder = buildExportToPlatformMethod(props); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.route).toBe(props.route); + }); + + it("returns buttonLabel from props", () => { + const props = EXPORT_METHODS.CAVATICA; + const builder = buildExportToPlatformMethod(props); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.buttonLabel).toBe(props.buttonLabel); + }); + + it("returns description from props", () => { + const props = EXPORT_METHODS.CANCER_GENOMICS_CLOUD; + const builder = buildExportToPlatformMethod(props); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.description).toBe(props.description); + }); + + it("returns title from props", () => { + const props = EXPORT_METHODS.BIO_DATA_CATALYST; + const builder = buildExportToPlatformMethod(props); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.title).toBe(props.title); + }); + + describe("accessibility", () => { + it("returns isAccessible true when facets loaded and files available", () => { + const props = EXPORT_METHODS.CAVATICA; + const builder = buildExportToPlatformMethod(props); + const fileManifestState = createMockFileManifestState({ + filesFacets: [createMockFileFacet()], + isFacetsSuccess: true, + summary: { fileCount: 10 }, + }); + const viewContext = createMockViewContext({ fileManifestState }); + + const result = builder(undefined, viewContext); + + expect(result.isAccessible).toBe(true); + }); + + it("returns isAccessible false when facets not yet loaded", () => { + const props = EXPORT_METHODS.CANCER_GENOMICS_CLOUD; + const builder = buildExportToPlatformMethod(props); + const fileManifestState = createMockFileManifestState({ + isFacetsSuccess: false, + }); + const viewContext = createMockViewContext({ fileManifestState }); + + const result = builder(undefined, viewContext); + + expect(result.isAccessible).toBe(false); + }); + + it("returns footnote when files not accessible", () => { + const props = EXPORT_METHODS.BIO_DATA_CATALYST; + const builder = buildExportToPlatformMethod(props); + const fileManifestState = createMockFileManifestState({ + filesFacets: [], + isFacetsSuccess: true, + }); + const viewContext = createMockViewContext({ fileManifestState }); + + const result = builder(undefined, viewContext); + + // When not accessible, footnote should explain why + if (!result.isAccessible) { + expect(result.footnote).toBeDefined(); + } + }); + }); + + describe("works correctly for each platform method", () => { + it("returns correct props for BioData Catalyst method", () => { + const builder = buildExportToPlatformMethod( + EXPORT_METHODS.BIO_DATA_CATALYST + ); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.buttonLabel).toBe("Analyze in NHLBI BioData Catalyst"); + expect(result.route).toBe("/export/biodata-catalyst"); + }); + + it("returns correct props for CAVATICA method", () => { + const builder = buildExportToPlatformMethod(EXPORT_METHODS.CAVATICA); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.buttonLabel).toBe("Analyze in CAVATICA"); + expect(result.route).toBe("/export/cavatica"); + }); + + it("returns correct props for Cancer Genomics Cloud method", () => { + const builder = buildExportToPlatformMethod( + EXPORT_METHODS.CANCER_GENOMICS_CLOUD + ); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.buttonLabel).toBe("Analyze in Cancer Genomics Cloud"); + expect(result.route).toBe("/export/cancer-genomics-cloud"); + }); + }); +}); diff --git a/jest.config.js b/jest.config.js index e518e23db..11df161b2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -14,6 +14,10 @@ const customJestConfig = { // setupFilesAfterEnv: ['/jest.setup.js'], // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work moduleDirectories: ["node_modules", "/"], + moduleNameMapper: { + "\\.mdx$": "/__mocks__/mdx.ts", + "^ky$": "/__mocks__/ky.ts", + }, testEnvironment: "jest-environment-jsdom", testPathIgnorePatterns: ["/node_modules/", "/e2e/", "/analytics/"], }; diff --git a/pages/export/cancer-genomics-cloud.tsx b/pages/export/cancer-genomics-cloud.tsx new file mode 100644 index 000000000..70d347c9f --- /dev/null +++ b/pages/export/cancer-genomics-cloud.tsx @@ -0,0 +1,28 @@ +import { JSX } from "react"; +import { ExportMethodView } from "@databiosphere/findable-ui/lib/views/ExportMethodView/exportMethodView"; +import { GetStaticProps } from "next"; +import { useFeatureFlag } from "@databiosphere/findable-ui/lib/hooks/useFeatureFlag/useFeatureFlag"; +import { FEATURES } from "../../app/shared/entities"; +import Error from "next/error"; + +export const getStaticProps: GetStaticProps = async () => { + return { + props: { + pageTitle: "Export to Cancer Genomics Cloud", + }, + }; +}; + +/** + * Export method page for Cancer Genomics Cloud. + * @returns export method view component. + */ +const ExportMethodPage = (): JSX.Element => { + const isEnabled = useFeatureFlag(FEATURES.NCPI_EXPORT); + + if (!isEnabled) return ; + + return ; +}; + +export default ExportMethodPage; diff --git a/site-config/anvil-cmg/dev/export/constants.ts b/site-config/anvil-cmg/dev/export/constants.ts index 1cd1f1855..7d82a8fb9 100644 --- a/site-config/anvil-cmg/dev/export/constants.ts +++ b/site-config/anvil-cmg/dev/export/constants.ts @@ -17,6 +17,13 @@ export const EXPORTS: Record< successTitle: "Your BioData Catalyst Workspace Link is Ready", title: "Analyze in BioData Catalyst", }, + CANCER_GENOMICS_CLOUD: { + buttonLabel: "Open Cancer Genomics Cloud", + description: + "CGC is a cloud workspace for analysis, storage, and computation using workflows, Jupyter Notebooks, and RStudio.", + successTitle: "Your Cancer Genomics Cloud Workspace Link is Ready", + title: "Analyze in Cancer Genomics Cloud", + }, CAVATICA: { buttonLabel: "Open CAVATICA", description: @@ -39,6 +46,12 @@ export const EXPORT_METHODS: Record< route: ROUTES.BIO_DATA_CATALYST, title: "Export to BioData Catalyst Powered by Seven Bridges (BDC-SB)", }, + CANCER_GENOMICS_CLOUD: { + buttonLabel: "Analyze in Cancer Genomics Cloud", + description: EXPORTS.CANCER_GENOMICS_CLOUD.description, + route: ROUTES.CANCER_GENOMICS_CLOUD, + title: "Export to Cancer Genomics Cloud (CGC)", + }, CAVATICA: { buttonLabel: "Analyze in CAVATICA", description: EXPORTS.CAVATICA.description, diff --git a/site-config/anvil-cmg/dev/export/export.ts b/site-config/anvil-cmg/dev/export/export.ts index d05f6180a..4e856a180 100644 --- a/site-config/anvil-cmg/dev/export/export.ts +++ b/site-config/anvil-cmg/dev/export/export.ts @@ -89,6 +89,35 @@ export const exportConfig: ExportConfig = { } as ComponentConfig, ], }, + { + mainColumn: [ + /* mainColumn - top section - warning - some datasets are not available */ + ...exportMainColumn, + /* mainColumn */ + { + children: [ + { + component: C.ExportToPlatform, + viewBuilder: V.buildExportToPlatform( + EXPORTS.CANCER_GENOMICS_CLOUD + ), + } as ComponentConfig, + ], + component: C.BackPageContentMainColumn, + } as ComponentConfig, + /* sideColumn */ + ...exportSideColumn, + ], + route: ROUTES.CANCER_GENOMICS_CLOUD, + top: [ + { + component: C.BackPageHero, + viewBuilder: V.buildExportToPlatformHero( + "Export to Cancer Genomics Cloud" + ), + } as ComponentConfig, + ], + }, { mainColumn: [ /* mainColumn - top section - warning - some datasets are not available */ @@ -141,6 +170,12 @@ export const exportConfig: ExportConfig = { EXPORT_METHODS.CAVATICA ), } as ComponentConfig, + { + component: ExportMethod, + viewBuilder: V.buildExportToPlatformMethod( + EXPORT_METHODS.CANCER_GENOMICS_CLOUD + ), + } as ComponentConfig, { component: C.ExportMethod, viewBuilder: V.buildExportMethodManifestDownload, diff --git a/site-config/anvil-cmg/dev/export/routes.ts b/site-config/anvil-cmg/dev/export/routes.ts index ec3daf379..844340d8f 100644 --- a/site-config/anvil-cmg/dev/export/routes.ts +++ b/site-config/anvil-cmg/dev/export/routes.ts @@ -1,5 +1,6 @@ export const ROUTES = { BIO_DATA_CATALYST: "/export/biodata-catalyst", + CANCER_GENOMICS_CLOUD: "/export/cancer-genomics-cloud", CAVATICA: "/export/cavatica", MANIFEST_DOWNLOAD: "/export/download-manifest", TERRA: "/export/export-to-terra", From e14d0597d87ec77095a5ca015e2f7b05762d5925 Mon Sep 17 00:00:00 2001 From: Fran McDade <18710366+frano-m@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:41:34 +1000 Subject: [PATCH 4/6] feat: export dataset to biodata catalyst (#4641) --- .../viewModelBuilders/dataset-export.test.ts | 396 ++++++++++++++++++ .../anvil-cmg/common/viewModelBuilders.ts | 69 +++ pages/[entityListType]/[...params].tsx | 26 ++ .../dev/detail/dataset/export/export.ts | 63 +++ 4 files changed, 554 insertions(+) create mode 100644 __tests__/viewModelBuilders/dataset-export.test.ts diff --git a/__tests__/viewModelBuilders/dataset-export.test.ts b/__tests__/viewModelBuilders/dataset-export.test.ts new file mode 100644 index 000000000..0c268ec37 --- /dev/null +++ b/__tests__/viewModelBuilders/dataset-export.test.ts @@ -0,0 +1,396 @@ +import { Breadcrumb } from "@databiosphere/findable-ui/lib/components/common/Breadcrumbs/breadcrumbs"; +import { ViewContext } from "@databiosphere/findable-ui/lib/config/entities"; +import { FileFacet } from "@databiosphere/findable-ui/lib/hooks/useFileManifest/common/entities"; +import { FileManifestState } from "@databiosphere/findable-ui/lib/providers/fileManifestState"; +import { + buildDatasetExportToPlatform, + buildDatasetExportToPlatformHero, + buildDatasetExportToPlatformMethod, +} from "../../app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders"; +import { DatasetsResponse } from "../../app/apis/azul/anvil-cmg/common/responses"; +import { ANVIL_CMG_CATEGORY_KEY } from "../../site-config/anvil-cmg/category"; +import { + EXPORTS, + EXPORT_METHODS, +} from "../../site-config/anvil-cmg/dev/export/constants"; + +/** + * Creates a mock FileManifestState for testing. + * @param overrides - Properties to override in the mock. + * @returns Mock FileManifestState. + */ +const createMockFileManifestState = ( + overrides: Partial = {} +): FileManifestState => ({ + fileCount: undefined, + fileSummary: undefined, + fileSummaryFacetName: undefined, + fileSummaryFilters: [], + filesFacets: [], + filters: [], + isEnabled: true, + isFacetsLoading: false, + isFacetsSuccess: true, + isFileSummaryLoading: false, + isLoading: false, + isSummaryLoading: false, + summary: undefined, + ...overrides, +}); + +/** + * Creates a mock FileFacet for testing. + * @param overrides - Properties to override in the mock. + * @returns Mock FileFacet. + */ +const createMockFileFacet = ( + overrides: Partial = {} +): FileFacet => ({ + name: "files.file_format", + selected: false, + selectedTermCount: 0, + selectedTerms: [], + termCount: 5, + terms: [{ count: 10, name: "bam", selected: false }], + termsByName: new Map(), + total: 10, + ...overrides, +}); + +/** + * Creates a mock DatasetsResponse for testing. + * @param overrides - Properties to override in the mock. + * @returns Mock DatasetsResponse. + */ +const createMockDatasetsResponse = ( + overrides: Partial = {} +): DatasetsResponse => + ({ + datasets: [ + { + accessible: true, + dataset_id: "test-dataset-id", + title: "Test Dataset Title", + }, + ], + donors: [{ organism_type: ["Homo sapiens"] }], + entryId: "test-dataset-id", + files: [{ file_format: ["bam", "fastq"] }], + ...overrides, + }) as DatasetsResponse; + +/** + * Creates a mock ViewContext for testing. + * @param overrides - Properties to override in the mock. + * @returns Mock ViewContext. + */ +const createMockViewContext = ( + overrides: Partial> = {} +): ViewContext => + ({ + cellContext: undefined, + entityConfig: undefined, + exploreState: { + categoryViews: [], + filterState: [{ categoryKey: "test.filter", value: ["value1"] }], + tabValue: "datasets", + }, + fileManifestState: createMockFileManifestState(), + ...overrides, + }) as ViewContext; + +describe("buildDatasetExportToPlatform", () => { + describe("returns correct props structure", () => { + it("spreads provided props into result", () => { + const props = EXPORTS.BIO_DATA_CATALYST; + const builder = buildDatasetExportToPlatform(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe(props.buttonLabel); + expect(result.description).toBe(props.description); + expect(result.successTitle).toBe(props.successTitle); + expect(result.title).toBe(props.title); + }); + + it("includes fileManifestState from viewContext", () => { + const props = EXPORTS.CAVATICA; + const builder = buildDatasetExportToPlatform(props); + const fileManifestState = createMockFileManifestState({ + isFacetsSuccess: true, + }); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext({ fileManifestState }); + + const result = builder(response, viewContext); + + expect(result.fileManifestState).toBe(fileManifestState); + }); + + it("includes correct fileSummaryFacetName", () => { + const props = EXPORTS.CANCER_GENOMICS_CLOUD; + const builder = buildDatasetExportToPlatform(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.fileSummaryFacetName).toBe( + ANVIL_CMG_CATEGORY_KEY.FILE_FILE_FORMAT + ); + }); + + it("includes correct speciesFacetName", () => { + const props = EXPORTS.BIO_DATA_CATALYST; + const builder = buildDatasetExportToPlatform(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.speciesFacetName).toBe( + ANVIL_CMG_CATEGORY_KEY.DONOR_ORGANISM_TYPE + ); + }); + + it("includes filters derived from response", () => { + const props = EXPORTS.CAVATICA; + const builder = buildDatasetExportToPlatform(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.filters).toBeDefined(); + expect(Array.isArray(result.filters)).toBe(true); + }); + }); + + describe("works correctly for each platform", () => { + it("returns correct props for BioData Catalyst config", () => { + const builder = buildDatasetExportToPlatform(EXPORTS.BIO_DATA_CATALYST); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe("Open BioData Catalyst"); + expect(result.title).toBe("Analyze in BioData Catalyst"); + }); + + it("returns correct props for CAVATICA config", () => { + const builder = buildDatasetExportToPlatform(EXPORTS.CAVATICA); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe("Open CAVATICA"); + expect(result.title).toBe("Analyze in CAVATICA"); + }); + + it("returns correct props for Cancer Genomics Cloud config", () => { + const builder = buildDatasetExportToPlatform( + EXPORTS.CANCER_GENOMICS_CLOUD + ); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe("Open Cancer Genomics Cloud"); + expect(result.title).toBe("Analyze in Cancer Genomics Cloud"); + }); + }); +}); + +describe("buildDatasetExportToPlatformHero", () => { + it("returns provided title as dataset title", () => { + const title = "Analyze in BioData Catalyst"; + const builder = buildDatasetExportToPlatformHero(title); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.title).toBe("Test Dataset Title"); + }); + + it("builds correct breadcrumb structure", () => { + const title = "Analyze in CAVATICA"; + const builder = buildDatasetExportToPlatformHero(title); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + const breadcrumbs = result.breadcrumbs as Breadcrumb[]; + + expect(result.breadcrumbs).toBeDefined(); + expect(Array.isArray(result.breadcrumbs)).toBe(true); + expect(breadcrumbs.length).toBe(4); + + // First breadcrumb should link to datasets list + expect(breadcrumbs[0].text).toBe("Datasets"); + expect(breadcrumbs[0].path).toBe("/datasets"); + + // Second breadcrumb should link to the dataset + expect(breadcrumbs[1].text).toBe("Test Dataset Title"); + expect(breadcrumbs[1].path).toBe("/datasets/test-dataset-id"); + + // Third breadcrumb should link to export choice + expect(breadcrumbs[2].text).toBe("Choose Export Method"); + expect(breadcrumbs[2].path).toBe("/datasets/test-dataset-id/export"); + + // Fourth breadcrumb should be the current page title + expect(breadcrumbs[3].text).toBe(title); + }); + + it("uses dataset ID from response for paths", () => { + const title = "Export to Cancer Genomics Cloud"; + const builder = buildDatasetExportToPlatformHero(title); + const response = createMockDatasetsResponse({ + datasets: [ + { + accessible: true, + dataset_id: "custom-dataset-123", + title: "Custom Dataset", + }, + ], + entryId: "custom-dataset-123", + } as Partial); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + const breadcrumbs = result.breadcrumbs as Breadcrumb[]; + + expect(breadcrumbs[1].path).toBe("/datasets/custom-dataset-123"); + expect(breadcrumbs[2].path).toBe("/datasets/custom-dataset-123/export"); + }); +}); + +describe("buildDatasetExportToPlatformMethod", () => { + it("returns route with dataset path prefix", () => { + const props = EXPORT_METHODS.BIO_DATA_CATALYST; + const builder = buildDatasetExportToPlatformMethod(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.route).toBe( + "/datasets/test-dataset-id/export/biodata-catalyst" + ); + }); + + it("returns buttonLabel from props", () => { + const props = EXPORT_METHODS.CAVATICA; + const builder = buildDatasetExportToPlatformMethod(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe(props.buttonLabel); + }); + + it("returns description from props", () => { + const props = EXPORT_METHODS.CANCER_GENOMICS_CLOUD; + const builder = buildDatasetExportToPlatformMethod(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.description).toBe(props.description); + }); + + it("returns title from props", () => { + const props = EXPORT_METHODS.BIO_DATA_CATALYST; + const builder = buildDatasetExportToPlatformMethod(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.title).toBe(props.title); + }); + + describe("accessibility", () => { + it("returns isAccessible true when facets loaded and files available", () => { + const props = EXPORT_METHODS.CAVATICA; + const builder = buildDatasetExportToPlatformMethod(props); + const response = createMockDatasetsResponse(); + const fileManifestState = createMockFileManifestState({ + filesFacets: [createMockFileFacet()], + isFacetsSuccess: true, + summary: { fileCount: 10 }, + }); + const viewContext = createMockViewContext({ fileManifestState }); + + const result = builder(response, viewContext); + + expect(result.isAccessible).toBe(true); + }); + + it("returns isAccessible false when facets not yet loaded", () => { + const props = EXPORT_METHODS.CANCER_GENOMICS_CLOUD; + const builder = buildDatasetExportToPlatformMethod(props); + const response = createMockDatasetsResponse(); + const fileManifestState = createMockFileManifestState({ + isFacetsSuccess: false, + }); + const viewContext = createMockViewContext({ fileManifestState }); + + const result = builder(response, viewContext); + + expect(result.isAccessible).toBe(false); + }); + }); + + describe("works correctly for each platform method", () => { + it("returns correct props for BioData Catalyst method", () => { + const builder = buildDatasetExportToPlatformMethod( + EXPORT_METHODS.BIO_DATA_CATALYST + ); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe("Analyze in NHLBI BioData Catalyst"); + expect(result.route).toBe( + "/datasets/test-dataset-id/export/biodata-catalyst" + ); + }); + + it("returns correct props for CAVATICA method", () => { + const builder = buildDatasetExportToPlatformMethod( + EXPORT_METHODS.CAVATICA + ); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe("Analyze in CAVATICA"); + expect(result.route).toBe("/datasets/test-dataset-id/export/cavatica"); + }); + + it("returns correct props for Cancer Genomics Cloud method", () => { + const builder = buildDatasetExportToPlatformMethod( + EXPORT_METHODS.CANCER_GENOMICS_CLOUD + ); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.buttonLabel).toBe("Analyze in Cancer Genomics Cloud"); + expect(result.route).toBe( + "/datasets/test-dataset-id/export/cancer-genomics-cloud" + ); + }); + }); +}); diff --git a/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts b/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts index 241409f50..586cf3948 100644 --- a/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts +++ b/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts @@ -488,6 +488,75 @@ export const buildDatasetExportMethodTerra = ( }; }; +/** + * Build props for the dataset ExportToPlatform component. + * @param props - Props to pass to the ExportToPlatform component. + * @returns model to be used as props for the ExportToPlatform component. + */ +export const buildDatasetExportToPlatform = ( + props: Pick< + ComponentProps, + "buttonLabel" | "description" | "successTitle" | "title" + > +): (( + response: DatasetsResponse, + viewContext: ViewContext +) => ComponentProps) => { + return (response: DatasetsResponse, viewContext: ViewContext) => { + const { fileManifestState } = viewContext; + return { + ...props, + fileManifestState, + fileSummaryFacetName: ANVIL_CMG_CATEGORY_KEY.FILE_FILE_FORMAT, + filters: getExportTerraEntityFilters(response), + formFacet: getFormFacets(fileManifestState), + speciesFacetName: ANVIL_CMG_CATEGORY_KEY.DONOR_ORGANISM_TYPE, + }; + }; +}; + +/** + * Build props for dataset ExportToPlatform BackPageHero component. + * @param title - Title of the export method. + * @returns model to be used as props for the BackPageHero component. + */ +export const buildDatasetExportToPlatformHero = ( + title: string +): (( + response: DatasetsResponse, + viewContext: ViewContext +) => React.ComponentProps) => { + return (response: DatasetsResponse) => { + return getDatasetExportMethodHero(response, title); + }; +}; + +/** + * Build props for dataset ExportMethod component for display of the export to [platform] metadata section. + * @param props - Props to pass to the ExportMethod component. + * @param props.route - Route to the export method. + * @returns model to be used as props for the dataset ExportMethod component. + */ +export const buildDatasetExportToPlatformMethod = ({ + route, + ...props +}: Pick< + ComponentProps, + "buttonLabel" | "description" | "route" | "title" +>): (( + response: DatasetsResponse, + viewContext: ViewContext +) => ComponentProps) => { + return (response: DatasetsResponse, viewContext: ViewContext) => { + const datasetPath = buildDatasetPath(response); + return { + ...props, + ...getExportMethodAccessibility(viewContext), + route: `${datasetPath}${route}`, + }; + }; +}; + /** * Build props for BackPageHero component from the given datasets response. * @param datasetsResponse - Response model return from datasets API. diff --git a/pages/[entityListType]/[...params].tsx b/pages/[entityListType]/[...params].tsx index 03e09b185..c8680c4da 100644 --- a/pages/[entityListType]/[...params].tsx +++ b/pages/[entityListType]/[...params].tsx @@ -29,9 +29,19 @@ import { ParsedUrlQuery } from "querystring"; import { EntityGuard } from "../../app/components/Detail/components/EntityGuard/entityGuard"; import { readFile } from "../../app/utils/tsvParser"; import { useRouter } from "next/router"; +import { useFeatureFlag } from "@databiosphere/findable-ui/lib/hooks/useFeatureFlag/useFeatureFlag"; +import { FEATURES } from "../../app/shared/entities"; +import NextError from "next/error"; +import { ROUTES } from "../../site-config/anvil-cmg/dev/export/routes"; const setOfProcessedIds = new Set(); +const NCPI_EXPORT_PATHS = [ + ROUTES.BIO_DATA_CATALYST, + ROUTES.CANCER_GENOMICS_CLOUD, + ROUTES.CAVATICA, +]; + interface StaticPath { params: PageUrl; } @@ -53,9 +63,12 @@ export interface EntityDetailPageProps extends AzulEntityStaticResponse { * @returns Entity detail view component. */ const EntityDetailPage = (props: EntityDetailPageProps): JSX.Element => { + const isNCPIExportEnabled = useFeatureFlag(FEATURES.NCPI_EXPORT); const { query } = useRouter(); if (!props.entityListType) return <>; if (props.override) return ; + if (!isNCPIExportEnabled && isNCPIExportRoute(query)) + return ; if (isChooseExportView(query)) return ; if (isExportMethodView(query)) return ; return ; @@ -77,6 +90,19 @@ function findOverride( return overrides.find(({ entryId }) => entryId === entityId); } +/** + * Returns true if the current route is an NCPI export route. + * @param query - Parsed URL query. + * @returns True if the route matches an NCPI export path. + */ +function isNCPIExportRoute(query: ParsedUrlQuery): boolean { + const params = query.params as string[] | undefined; + const lastParam = params?.[params.length - 1] || ""; + return NCPI_EXPORT_PATHS.map((path) => path.replace("/export/", "")).includes( + lastParam + ); +} + /** * Returns true if the entity is a special case e.g. an "override". * @param override - Override. diff --git a/site-config/anvil-cmg/dev/detail/dataset/export/export.ts b/site-config/anvil-cmg/dev/detail/dataset/export/export.ts index 98729284b..d0ee2b45b 100644 --- a/site-config/anvil-cmg/dev/detail/dataset/export/export.ts +++ b/site-config/anvil-cmg/dev/detail/dataset/export/export.ts @@ -8,6 +8,9 @@ import * as C from "../../../../../../app/components"; import { DatasetsResponse } from "app/apis/azul/anvil-cmg/common/responses"; import { ROUTES } from "../../../export/routes"; import * as MDX from "../../../../../../app/components/common/MDXContent/anvil-cmg"; +import { ExportMethod } from "../../../../../../app/components/Export/components/AnVILExplorer/platform/ExportMethod/exportMethod"; +import { EXPORT_METHODS, EXPORTS } from "../../../export/constants"; +import { ExportToPlatform } from "../../../../../../app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/exportToPlatform"; /** * Badge indicating dataset accessibility. @@ -72,6 +75,60 @@ export const exportConfig: ExportConfig = { } as ComponentConfig, ], }, + { + mainColumn: [ + /* --------- */ + /* Dataset is not accessible; render warning */ + /* --------- */ + { + children: [ + { + children: [ + { + component: MDX.Alert, + viewBuilder: V.buildAlertDatasetExportWarning, + } as ComponentConfig, + ], + component: C.BackPageContentSingleColumn, + } as ComponentConfig, + ], + component: C.ConditionalComponent, + viewBuilder: V.renderDatasetExportWarning, + } as ComponentConfig, + /* ------ */ + /* Dataset is accessible; render BioData Catalyst export method */ + /* ------ */ + { + children: [ + { + children: [ + { + component: ExportToPlatform, + viewBuilder: V.buildDatasetExportToPlatform( + EXPORTS.BIO_DATA_CATALYST + ), + } as ComponentConfig, + ], + component: C.BackPageContentMainColumn, + } as ComponentConfig, + /* sideColumn */ + ...exportSideColumn, + ], + component: C.ConditionalComponent, + viewBuilder: V.renderDatasetExport, + } as ComponentConfig, + ], + route: ROUTES.BIO_DATA_CATALYST, + top: [ + { + children: [DATASET_ACCESSIBILITY_BADGE], + component: C.BackPageHero, + viewBuilder: V.buildDatasetExportToPlatformHero( + EXPORTS.BIO_DATA_CATALYST.title + ), + } as ComponentConfig, + ], + }, { mainColumn: [ /* --------- */ @@ -165,6 +222,12 @@ export const exportConfig: ExportConfig = { component: C.ExportMethod, viewBuilder: V.buildDatasetExportMethodTerra, } as ComponentConfig, + { + component: ExportMethod, + viewBuilder: V.buildDatasetExportToPlatformMethod( + EXPORT_METHODS.BIO_DATA_CATALYST + ), + } as ComponentConfig, { component: C.ExportMethod, viewBuilder: V.buildDatasetExportMethodManifestDownload, From caedebf9832213bd8369a29c2efaacc1446bf3d0 Mon Sep 17 00:00:00 2001 From: Fran McDade <18710366+frano-m@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:47:06 +1000 Subject: [PATCH 5/6] feat: export dataset to cavatica (#4643) --- .../dev/detail/dataset/export/export.ts | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/site-config/anvil-cmg/dev/detail/dataset/export/export.ts b/site-config/anvil-cmg/dev/detail/dataset/export/export.ts index d0ee2b45b..1f687257c 100644 --- a/site-config/anvil-cmg/dev/detail/dataset/export/export.ts +++ b/site-config/anvil-cmg/dev/detail/dataset/export/export.ts @@ -129,6 +129,58 @@ export const exportConfig: ExportConfig = { } as ComponentConfig, ], }, + { + mainColumn: [ + /* --------- */ + /* Dataset is not accessible; render warning */ + /* --------- */ + { + children: [ + { + children: [ + { + component: MDX.Alert, + viewBuilder: V.buildAlertDatasetExportWarning, + } as ComponentConfig, + ], + component: C.BackPageContentSingleColumn, + } as ComponentConfig, + ], + component: C.ConditionalComponent, + viewBuilder: V.renderDatasetExportWarning, + } as ComponentConfig, + /* ------ */ + /* Dataset is accessible; render CAVATICA export method */ + /* ------ */ + { + children: [ + { + children: [ + { + component: ExportToPlatform, + viewBuilder: V.buildDatasetExportToPlatform(EXPORTS.CAVATICA), + } as ComponentConfig, + ], + component: C.BackPageContentMainColumn, + } as ComponentConfig, + /* sideColumn */ + ...exportSideColumn, + ], + component: C.ConditionalComponent, + viewBuilder: V.renderDatasetExport, + } as ComponentConfig, + ], + route: ROUTES.CAVATICA, + top: [ + { + children: [DATASET_ACCESSIBILITY_BADGE], + component: C.BackPageHero, + viewBuilder: V.buildDatasetExportToPlatformHero( + EXPORTS.CAVATICA.title + ), + } as ComponentConfig, + ], + }, { mainColumn: [ /* --------- */ @@ -228,6 +280,12 @@ export const exportConfig: ExportConfig = { EXPORT_METHODS.BIO_DATA_CATALYST ), } as ComponentConfig, + { + component: ExportMethod, + viewBuilder: V.buildDatasetExportToPlatformMethod( + EXPORT_METHODS.CAVATICA + ), + } as ComponentConfig, { component: C.ExportMethod, viewBuilder: V.buildDatasetExportMethodManifestDownload, From 27b768685104095484fd9b70952c1d34fce4dd05 Mon Sep 17 00:00:00 2001 From: Fran McDade <18710366+frano-m@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:54:50 +1000 Subject: [PATCH 6/6] feat: export dataset to cancer genomics cloud (cgc) (#4645) --- .../dev/detail/dataset/export/export.ts | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/site-config/anvil-cmg/dev/detail/dataset/export/export.ts b/site-config/anvil-cmg/dev/detail/dataset/export/export.ts index 1f687257c..994ac51f8 100644 --- a/site-config/anvil-cmg/dev/detail/dataset/export/export.ts +++ b/site-config/anvil-cmg/dev/detail/dataset/export/export.ts @@ -181,6 +181,60 @@ export const exportConfig: ExportConfig = { } as ComponentConfig, ], }, + { + mainColumn: [ + /* --------- */ + /* Dataset is not accessible; render warning */ + /* --------- */ + { + children: [ + { + children: [ + { + component: MDX.Alert, + viewBuilder: V.buildAlertDatasetExportWarning, + } as ComponentConfig, + ], + component: C.BackPageContentSingleColumn, + } as ComponentConfig, + ], + component: C.ConditionalComponent, + viewBuilder: V.renderDatasetExportWarning, + } as ComponentConfig, + /* ------ */ + /* Dataset is accessible; render Cancer Genomics Cloud export method */ + /* ------ */ + { + children: [ + { + children: [ + { + component: ExportToPlatform, + viewBuilder: V.buildDatasetExportToPlatform( + EXPORTS.CANCER_GENOMICS_CLOUD + ), + } as ComponentConfig, + ], + component: C.BackPageContentMainColumn, + } as ComponentConfig, + /* sideColumn */ + ...exportSideColumn, + ], + component: C.ConditionalComponent, + viewBuilder: V.renderDatasetExport, + } as ComponentConfig, + ], + route: ROUTES.CANCER_GENOMICS_CLOUD, + top: [ + { + children: [DATASET_ACCESSIBILITY_BADGE], + component: C.BackPageHero, + viewBuilder: V.buildDatasetExportToPlatformHero( + EXPORTS.CANCER_GENOMICS_CLOUD.title + ), + } as ComponentConfig, + ], + }, { mainColumn: [ /* --------- */ @@ -286,6 +340,12 @@ export const exportConfig: ExportConfig = { EXPORT_METHODS.CAVATICA ), } as ComponentConfig, + { + component: ExportMethod, + viewBuilder: V.buildDatasetExportToPlatformMethod( + EXPORT_METHODS.CANCER_GENOMICS_CLOUD + ), + } as ComponentConfig, { component: C.ExportMethod, viewBuilder: V.buildDatasetExportMethodManifestDownload,