From 4e8889f0469e312e8730079a467abd905c4dfce4 Mon Sep 17 00:00:00 2001 From: Fran McDade <18710366+frano-m@users.noreply.github.com> Date: Tue, 10 Nov 2026 15:07:25 +1000 Subject: [PATCH 1/2] feat: link the bdc-sb export page to velsera behind a feature flag (#5027) - add the velseraexport feature flag; with it on, the bdc-sb export method is enabled on the cohort and dataset export pages - build the bdc-sb pfb import url from the signed pfb location, with the import endpoint and source (bdc) in site config - disable request link on the bdc-sb page while the flag is off - show a 404 for platform export pages without an import endpoint (cavatica, cgc), and move their coming soon status into site config Co-Authored-By: Claude Opus 5.5 Co-authored-by: cc-claude-tools --- __tests__/components/exportToPlatform.test.ts | 70 +++++++++++++++++++ .../components/velseraExportMethod.test.tsx | 58 +++++++++++++++ .../site-config/export-constants.test.ts | 40 +++++++---- __tests__/site-config/export-utils.test.ts | 28 ++++++++ .../ExportToPlatform/exportToPlatform.tsx | 22 +++++- .../platform/ExportToPlatform/types.ts | 9 +++ .../platform/ExportToPlatform/utils.ts | 48 +++++++++++++ .../velseraExportMethod.tsx | 18 +++++ app/components/index.tsx | 1 + app/shared/entities.ts | 4 +- .../anvil-cmg/common/viewModelBuilders.tsx | 10 ++- pages/[entityListType]/[...params].tsx | 16 +++++ pages/export/biodata-catalyst.tsx | 7 +- pages/export/cancer-genomics-cloud.tsx | 7 +- pages/export/cavatica.tsx | 7 +- .../dev/detail/dataset/export/export.ts | 4 +- .../anvil-cmg/dev/export/constants.tsx | 10 ++- site-config/anvil-cmg/dev/export/export.ts | 4 +- site-config/anvil-cmg/dev/export/utils.ts | 14 ++++ 19 files changed, 347 insertions(+), 30 deletions(-) create mode 100644 __tests__/components/exportToPlatform.test.ts create mode 100644 __tests__/components/velseraExportMethod.test.tsx create mode 100644 __tests__/site-config/export-utils.test.ts create mode 100644 app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/utils.ts create mode 100644 app/components/Export/components/AnVILExplorer/platform/VelseraExportMethod/velseraExportMethod.tsx create mode 100644 site-config/anvil-cmg/dev/export/utils.ts diff --git a/__tests__/components/exportToPlatform.test.ts b/__tests__/components/exportToPlatform.test.ts new file mode 100644 index 000000000..b63ea5743 --- /dev/null +++ b/__tests__/components/exportToPlatform.test.ts @@ -0,0 +1,70 @@ +import type { FileLocation } from "@databiosphere/findable-ui/lib/hooks/useRequestFileLocation"; +import type { PlatformImport } from "../../app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/types"; +import { + buildPlatformImportUrl, + getPlatformImportUrl, + isPlatformImportAvailable, +} from "../../app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/utils"; + +const PLATFORM_IMPORT: PlatformImport = { + baseUrl: "https://platform.sb.biodatacatalyst.nhlbi.nih.gov/import/pfb", + source: "bdc", +}; + +const SIGNED_URL = + "https://storage.googleapis.com/bucket/manifest.avro?X-Goog-Signature=a%2Fb&X-Goog-Expires=3600"; + +const IMPORT_URL = + "https://platform.sb.biodatacatalyst.nhlbi.nih.gov/import/pfb?URL=https%3A%2F%2Fstorage.googleapis.com%2Fbucket%2Fmanifest.avro%3FX-Goog-Signature%3Da%252Fb%26X-Goog-Expires%3D3600&source=bdc"; + +const FILE_LOCATION: FileLocation = { location: SIGNED_URL, status: 302 }; + +describe("buildPlatformImportUrl", () => { + it("encodes the signed URL and appends the source", () => { + expect(buildPlatformImportUrl(PLATFORM_IMPORT, SIGNED_URL)).toBe( + IMPORT_URL + ); + }); + + it("round-trips the signed URL through the URL parameter", () => { + const url = new URL(buildPlatformImportUrl(PLATFORM_IMPORT, SIGNED_URL)); + expect(url.searchParams.get("URL")).toBe(SIGNED_URL); + expect(url.searchParams.get("source")).toBe("bdc"); + }); +}); + +describe("getPlatformImportUrl", () => { + it("returns the import URL when enabled with a config and file location", () => { + expect(getPlatformImportUrl(true, PLATFORM_IMPORT, FILE_LOCATION)).toBe( + IMPORT_URL + ); + }); + + it("returns an empty string when the feature flag is off", () => { + expect(getPlatformImportUrl(false, PLATFORM_IMPORT, FILE_LOCATION)).toBe( + "" + ); + }); + + it("returns an empty string when the platform has no import config", () => { + expect(getPlatformImportUrl(true, undefined, FILE_LOCATION)).toBe(""); + }); + + it("returns an empty string before the file location is available", () => { + expect(getPlatformImportUrl(true, PLATFORM_IMPORT, undefined)).toBe(""); + }); +}); + +describe("isPlatformImportAvailable", () => { + it("returns true when enabled with a config", () => { + expect(isPlatformImportAvailable(true, PLATFORM_IMPORT)).toBe(true); + }); + + it("returns false when the feature flag is off", () => { + expect(isPlatformImportAvailable(false, PLATFORM_IMPORT)).toBe(false); + }); + + it("returns false when the platform has no import config", () => { + expect(isPlatformImportAvailable(true, undefined)).toBe(false); + }); +}); diff --git a/__tests__/components/velseraExportMethod.test.tsx b/__tests__/components/velseraExportMethod.test.tsx new file mode 100644 index 000000000..4e34a0340 --- /dev/null +++ b/__tests__/components/velseraExportMethod.test.tsx @@ -0,0 +1,58 @@ +import { createAppTheme } from "@databiosphere/findable-ui/lib/theme/theme"; +import { ThemeProvider } from "@mui/material"; +import { render, screen } from "@testing-library/react"; +import type { ComponentProps } from "react"; +import { VelseraExportMethod } from "../../app/components/Export/components/AnVILExplorer/platform/VelseraExportMethod/velseraExportMethod"; +import { FEATURES } from "../../app/shared/entities"; + +jest.mock("@databiosphere/findable-ui/lib/hooks/useDownloadStatus", () => ({ + useDownloadStatus: (): { disabled: boolean; message: string } => ({ + disabled: false, + message: "", + }), +})); + +const COMING_SOON = "Coming Soon"; + +const THEME = createAppTheme(); + +const PROPS = { + description: "BDC-SB is a cloud workspace.", + route: "/export/biodata-catalyst", + title: "Export to BioData Catalyst", +}; + +/** + * Renders VelseraExportMethod within the app theme. + * @param props - Export method props. + */ +function renderMethod(props: ComponentProps): void { + render( + + + + ); +} + +describe("VelseraExportMethod", () => { + afterEach(() => { + localStorage.clear(); + }); + + it("shows coming soon when the Velsera export flag is off", () => { + renderMethod(PROPS); + expect(screen.getByText(COMING_SOON)).toBeTruthy(); + }); + + it("enables the method when the Velsera export flag is on", () => { + localStorage.setItem(FEATURES.VELSERA_EXPORT, "true"); + renderMethod(PROPS); + expect(screen.queryByText(COMING_SOON)).toBeNull(); + }); + + it("shows coming soon when configured so, even with the flag on", () => { + localStorage.setItem(FEATURES.VELSERA_EXPORT, "true"); + renderMethod({ ...PROPS, comingSoon: true }); + expect(screen.getByText(COMING_SOON)).toBeTruthy(); + }); +}); diff --git a/__tests__/site-config/export-constants.test.ts b/__tests__/site-config/export-constants.test.ts index d269aec0e..705f07a22 100644 --- a/__tests__/site-config/export-constants.test.ts +++ b/__tests__/site-config/export-constants.test.ts @@ -54,24 +54,24 @@ describe("EXPORTS constants", () => { }); it.each(Object.entries(EXPORTS))( - "%s has all required keys", + "%s has non-empty string values for all required keys", (_, config) => { for (const key of REQUIRED_EXPORT_KEYS) { - expect(config).toHaveProperty(key); - expect(config[key]).toBeTruthy(); + expect(typeof config[key]).toBe("string"); + expect(config[key].trim().length).toBeGreaterThan(0); } } ); + }); - 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("platform import config", () => { + it.each( + Object.entries(EXPORTS).filter(([, config]) => config.platformImport) + )("%s has a valid import base URL and source", (_, config) => { + const { baseUrl, source } = config.platformImport!; + expect(new URL(baseUrl).protocol).toBe("https:"); + expect(source.trim().length).toBeGreaterThan(0); + }); }); describe("BioData Catalyst export config", () => { @@ -96,6 +96,13 @@ describe("EXPORTS constants", () => { it("description mentions BDC-SB", () => { expect(EXPORTS.BIO_DATA_CATALYST.description).toContain("BDC-SB"); }); + + it("has the BDC-SB PFB import endpoint and source", () => { + expect(EXPORTS.BIO_DATA_CATALYST.platformImport).toEqual({ + baseUrl: "https://platform.sb.biodatacatalyst.nhlbi.nih.gov/import/pfb", + source: "bdc", + }); + }); }); describe("CAVATICA export config", () => { @@ -153,6 +160,15 @@ describe("EXPORT_METHODS constants", () => { expect(EXPORT_METHODS.CANCER_GENOMICS_CLOUD).toBeDefined(); }); + it("marks CAVATICA and Cancer Genomics Cloud as coming soon", () => { + expect(EXPORT_METHODS.CAVATICA.comingSoon).toBe(true); + expect(EXPORT_METHODS.CANCER_GENOMICS_CLOUD.comingSoon).toBe(true); + }); + + it("leaves BioData Catalyst coming soon to the Velsera export feature flag", () => { + expect(EXPORT_METHODS.BIO_DATA_CATALYST.comingSoon).toBeUndefined(); + }); + it.each(Object.entries(EXPORT_METHODS))( "%s has all required keys", (_, config) => { diff --git a/__tests__/site-config/export-utils.test.ts b/__tests__/site-config/export-utils.test.ts new file mode 100644 index 000000000..5a4c699f8 --- /dev/null +++ b/__tests__/site-config/export-utils.test.ts @@ -0,0 +1,28 @@ +import { ROUTES } from "../../site-config/anvil-cmg/dev/export/routes"; +import { isUnavailablePlatformExportRoute } from "../../site-config/anvil-cmg/dev/export/utils"; + +describe("isUnavailablePlatformExportRoute", () => { + it.each([ROUTES.CANCER_GENOMICS_CLOUD, ROUTES.CAVATICA])( + "returns true for %s, which has no import endpoint", + (route) => { + expect(isUnavailablePlatformExportRoute(route)).toBe(true); + } + ); + + it("returns false for BioData Catalyst, which has an import endpoint", () => { + expect(isUnavailablePlatformExportRoute(ROUTES.BIO_DATA_CATALYST)).toBe( + false + ); + }); + + it.each([ROUTES.CURL_DOWNLOAD, ROUTES.MANIFEST_DOWNLOAD, ROUTES.TERRA])( + "returns false for %s, which is not a platform export", + (route) => { + expect(isUnavailablePlatformExportRoute(route)).toBe(false); + } + ); + + it("returns false for an unknown route", () => { + expect(isUnavailablePlatformExportRoute("/export/unknown")).toBe(false); + }); +}); diff --git a/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/exportToPlatform.tsx b/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/exportToPlatform.tsx index 108c9197f..c61740991 100644 --- a/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/exportToPlatform.tsx +++ b/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/exportToPlatform.tsx @@ -15,14 +15,17 @@ import { REL_ATTRIBUTE, } from "@databiosphere/findable-ui/lib/components/Links/common/entities"; import { Loading } from "@databiosphere/findable-ui/lib/components/Loading/loading"; +import { useFeatureFlag } from "@databiosphere/findable-ui/lib/hooks/useFeatureFlag/useFeatureFlag"; 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 { Button } from "@mui/material"; +import { FEATURES } from "app/shared/entities"; import { JSX } from "react"; import { Props } from "./types"; +import { getPlatformImportUrl, isPlatformImportAvailable } from "./utils"; export const ExportToPlatform = ({ buttonLabel, @@ -31,6 +34,7 @@ export const ExportToPlatform = ({ fileSummaryFacetName, filters, formFacet, + platformImport, speciesFacetName, successTitle, title, @@ -47,7 +51,9 @@ export const ExportToPlatform = ({ const { requestMethod, requestUrl } = requestManifest; const response = useRequestFileLocation(requestUrl, requestMethod); - const url = ""; + const isEnabled = useFeatureFlag(FEATURES.VELSERA_EXPORT); + const canImport = isPlatformImportAvailable(isEnabled, platformImport); + const url = getPlatformImportUrl(isEnabled, platformImport, response.data); return url ? ( @@ -81,7 +87,7 @@ export const ExportToPlatform = ({

{description}

response.run()} @@ -105,3 +111,15 @@ export const ExportToPlatform = ({ function renderButton({ ...props }): JSX.Element { return Request Link; } + +/** + * Build the export button, disabled while the platform import is unavailable. + * @returns disabled button element. + */ +function renderDisabledButton(): JSX.Element { + return ( + + ); +} diff --git a/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/types.ts b/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/types.ts index 3c8b37810..373e97e60 100644 --- a/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/types.ts +++ b/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/types.ts @@ -2,6 +2,14 @@ import { Filters } from "@databiosphere/findable-ui/lib/common/entities"; import { FormFacet } from "@databiosphere/findable-ui/lib/components/Export/common/entities"; import { FileManifestState } from "@databiosphere/findable-ui/lib/providers/fileManifestState"; +/** + * Platform endpoint that imports a PFB from a signed URL. + */ +export interface PlatformImport { + baseUrl: string; + source: string; +} + export interface Props { buttonLabel: string; description: string; @@ -9,6 +17,7 @@ export interface Props { fileSummaryFacetName: string; filters: Filters; formFacet: FormFacet; + platformImport?: PlatformImport; speciesFacetName: string; successTitle: string; title: string; diff --git a/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/utils.ts b/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/utils.ts new file mode 100644 index 000000000..aef247415 --- /dev/null +++ b/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/utils.ts @@ -0,0 +1,48 @@ +import type { FileLocation } from "@databiosphere/findable-ui/lib/hooks/useRequestFileLocation"; +import type { PlatformImport } from "./types"; + +/** + * Builds the platform import URL for the given signed PFB URL. + * @param platformImport - Platform import endpoint and source. + * @param signedUrl - Signed URL of the PFB to import. + * @returns import URL, with the signed URL and source as query parameters. + */ +export function buildPlatformImportUrl( + platformImport: PlatformImport, + signedUrl: string +): string { + const url = new URL(platformImport.baseUrl); + url.searchParams.set("URL", signedUrl); + url.searchParams.set("source", platformImport.source); + return url.toString(); +} + +/** + * Returns the platform import URL once the PFB location is available. + * @param isEnabled - Whether the platform export feature flag is on. + * @param platformImport - Platform import endpoint and source; undefined when the platform has none. + * @param fileLocation - PFB file location; undefined until the manifest request succeeds. + * @returns import URL, or an empty string when the flag is off, the platform has no import endpoint, or the PFB location is not yet available. + */ +export function getPlatformImportUrl( + isEnabled: boolean, + platformImport: PlatformImport | undefined, + fileLocation: FileLocation | undefined +): string { + if (!isPlatformImportAvailable(isEnabled, platformImport) || !fileLocation) + return ""; + return buildPlatformImportUrl(platformImport, fileLocation.location); +} + +/** + * Returns true if the platform import is available: the feature flag is on and the platform has an import endpoint. + * @param isEnabled - Whether the platform export feature flag is on. + * @param platformImport - Platform import endpoint and source; undefined when the platform has none. + * @returns true if a platform import link can be built. + */ +export function isPlatformImportAvailable( + isEnabled: boolean, + platformImport: PlatformImport | undefined +): platformImport is PlatformImport { + return isEnabled && platformImport !== undefined; +} diff --git a/app/components/Export/components/AnVILExplorer/platform/VelseraExportMethod/velseraExportMethod.tsx b/app/components/Export/components/AnVILExplorer/platform/VelseraExportMethod/velseraExportMethod.tsx new file mode 100644 index 000000000..0957f4e82 --- /dev/null +++ b/app/components/Export/components/AnVILExplorer/platform/VelseraExportMethod/velseraExportMethod.tsx @@ -0,0 +1,18 @@ +import { ExportMethod } 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"; +import type { ComponentProps, JSX } from "react"; + +/** + * Export method for a Velsera platform; shown as "coming soon" when configured so, or when the Velsera export feature flag is off. + * @param props - Export method component props. + * @returns Export method component. + */ +export const VelseraExportMethod = ( + props: ComponentProps +): JSX.Element => { + const isEnabled = useFeatureFlag(FEATURES.VELSERA_EXPORT); + return ( + + ); +}; diff --git a/app/components/index.tsx b/app/components/index.tsx index f52d8ab97..8e5030072 100644 --- a/app/components/index.tsx +++ b/app/components/index.tsx @@ -95,6 +95,7 @@ export { AtlasSection } from "./Detail/components/Section/components/AtlasSectio export { ExportEntity as AnVILExportEntity } from "./Export/components/AnVILExplorer/components/ExportEntity/exportEntity"; export { ManifestDownloadEntity as AnVILManifestDownloadEntity } from "./Export/components/AnVILExplorer/components/ManifestDownload/components/ManifestDownloadEntity/manifestDownloadEntity"; export { ExportToPlatform } from "./Export/components/AnVILExplorer/platform/ExportToPlatform/exportToPlatform"; +export { VelseraExportMethod } from "./Export/components/AnVILExplorer/platform/VelseraExportMethod/velseraExportMethod"; export { BioNetworkCell } from "./Index/components/BioNetworkCell/bioNetworkCell"; export { ConsentCodesCell } from "./Index/components/ConsentCodesCell/consentCodesCell"; export { CopyCell } from "./Index/components/CopyCell/copyCell"; diff --git a/app/shared/entities.ts b/app/shared/entities.ts index 27dfc0bf2..6ae400b52 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 { + VELSERA_EXPORT = "velseraexport", +} diff --git a/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.tsx b/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.tsx index 3922818fc..52a5e18f0 100644 --- a/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.tsx +++ b/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.tsx @@ -530,7 +530,7 @@ export const buildDatasetDownloadCurlCommand = ( export const buildDatasetExportToPlatform = ( props: Pick< ComponentProps, - "buttonLabel" | "description" | "successTitle" | "title" + "buttonLabel" | "description" | "platformImport" | "successTitle" | "title" > ): (( response: DatasetsResponse, @@ -576,7 +576,7 @@ export const buildDatasetExportToPlatformMethod = ({ ...props }: Pick< ComponentProps, - "description" | "icon" | "route" | "title" + "comingSoon" | "description" | "icon" | "route" | "title" >): (( response: DatasetsResponse, viewContext: ViewContext @@ -586,7 +586,6 @@ export const buildDatasetExportToPlatformMethod = ({ return { ...props, ...getExportMethodAccessibility(viewContext), - comingSoon: true, route: `${datasetPath}${route}`, }; }; @@ -954,7 +953,7 @@ export const buildExportSelectedDataSummary = ( export const buildExportToPlatform = ( props: Pick< ComponentProps, - "buttonLabel" | "description" | "successTitle" | "title" + "buttonLabel" | "description" | "platformImport" | "successTitle" | "title" > ): (( _: unknown, @@ -1003,7 +1002,7 @@ export const buildExportToPlatformHero = ( export const buildExportToPlatformMethod = ( props: Pick< ComponentProps, - "description" | "icon" | "route" | "title" + "comingSoon" | "description" | "icon" | "route" | "title" > ): (( _: unknown, @@ -1013,7 +1012,6 @@ export const buildExportToPlatformMethod = ( return { ...props, ...getExportMethodAccessibility(viewContext), - comingSoon: true, }; }; }; diff --git a/pages/[entityListType]/[...params].tsx b/pages/[entityListType]/[...params].tsx index 65b3b9a0f..5c5fa6604 100644 --- a/pages/[entityListType]/[...params].tsx +++ b/pages/[entityListType]/[...params].tsx @@ -37,6 +37,7 @@ import type { SchemaDataset } from "../../app/utils/schemaOrg/types"; import { readFile } from "../../app/utils/tsvParser"; import { JsonLd } from "../../app/views/EntityDetailView/components/JsonLd/jsonLd"; import { ROUTES } from "../../site-config/anvil-cmg/dev/export/routes"; +import { isUnavailablePlatformExportRoute } from "../../site-config/anvil-cmg/dev/export/utils"; import type { DatasetsResponse } from "../../app/apis/azul/anvil-cmg/common/responses"; import { @@ -95,6 +96,10 @@ const EntityDetailPage = (props: EntityDetailPageProps): JSX.Element => { ) { return ; } + // Platform exports without an import endpoint are not available (AnVIL only) + if (isAnVIL && isUnavailablePlatformExportMethod(query)) { + return ; + } if (isChooseExportView(query)) return ; if (isExportMethodView(query)) return ; return ( @@ -159,6 +164,17 @@ function isOverride(override: Override): boolean { ); } +/** + * Returns true if the current route is a platform export method whose platform has no import endpoint. + * @param query - Parsed URL query. + * @returns True if the route is an unavailable platform export method. + */ +function isUnavailablePlatformExportMethod(query: ParsedUrlQuery): boolean { + const params = query.params as string[] | undefined; + const lastParam = params?.[params.length - 1] || ""; + return isUnavailablePlatformExportRoute(`/export/${lastParam}`); +} + /** * Check if the current view matches the expected export path segment. * @param query - The current query object. diff --git a/pages/export/biodata-catalyst.tsx b/pages/export/biodata-catalyst.tsx index 6c30eb30c..1fdf21f5f 100644 --- a/pages/export/biodata-catalyst.tsx +++ b/pages/export/biodata-catalyst.tsx @@ -1,6 +1,9 @@ import { ExportMethodView } from "@databiosphere/findable-ui/lib/views/ExportMethodView/exportMethodView"; import { GetStaticProps } from "next"; +import NextError from "next/error"; import { JSX } from "react"; +import { ROUTES } from "../../site-config/anvil-cmg/dev/export/routes"; +import { isUnavailablePlatformExportRoute } from "../../site-config/anvil-cmg/dev/export/utils"; export const getStaticProps: GetStaticProps = async () => { return { @@ -13,9 +16,11 @@ export const getStaticProps: GetStaticProps = async () => { /** * Export method page for BioData Catalyst. - * @returns export method view component. + * @returns export method view component, or a 404 when the platform has no import endpoint. */ const ExportMethodPage = (): JSX.Element => { + if (isUnavailablePlatformExportRoute(ROUTES.BIO_DATA_CATALYST)) + return ; return ; }; diff --git a/pages/export/cancer-genomics-cloud.tsx b/pages/export/cancer-genomics-cloud.tsx index b0644aec6..a2c3aad7a 100644 --- a/pages/export/cancer-genomics-cloud.tsx +++ b/pages/export/cancer-genomics-cloud.tsx @@ -1,6 +1,9 @@ import { ExportMethodView } from "@databiosphere/findable-ui/lib/views/ExportMethodView/exportMethodView"; import { GetStaticProps } from "next"; +import NextError from "next/error"; import { JSX } from "react"; +import { ROUTES } from "../../site-config/anvil-cmg/dev/export/routes"; +import { isUnavailablePlatformExportRoute } from "../../site-config/anvil-cmg/dev/export/utils"; export const getStaticProps: GetStaticProps = async () => { return { @@ -13,9 +16,11 @@ export const getStaticProps: GetStaticProps = async () => { /** * Export method page for Cancer Genomics Cloud. - * @returns export method view component. + * @returns export method view component, or a 404 when the platform has no import endpoint. */ const ExportMethodPage = (): JSX.Element => { + if (isUnavailablePlatformExportRoute(ROUTES.CANCER_GENOMICS_CLOUD)) + return ; return ; }; diff --git a/pages/export/cavatica.tsx b/pages/export/cavatica.tsx index 4b3e9a1dc..f84d42139 100644 --- a/pages/export/cavatica.tsx +++ b/pages/export/cavatica.tsx @@ -1,6 +1,9 @@ import { ExportMethodView } from "@databiosphere/findable-ui/lib/views/ExportMethodView/exportMethodView"; import { GetStaticProps } from "next"; +import NextError from "next/error"; import { JSX } from "react"; +import { ROUTES } from "../../site-config/anvil-cmg/dev/export/routes"; +import { isUnavailablePlatformExportRoute } from "../../site-config/anvil-cmg/dev/export/utils"; export const getStaticProps: GetStaticProps = async () => { return { @@ -13,9 +16,11 @@ export const getStaticProps: GetStaticProps = async () => { /** * Export method page for CAVATICA. - * @returns export method view component. + * @returns export method view component, or a 404 when the platform has no import endpoint. */ const ExportMethodPage = (): JSX.Element => { + if (isUnavailablePlatformExportRoute(ROUTES.CAVATICA)) + return ; return ; }; 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 f4db4f1f1..bead0af4b 100644 --- a/site-config/anvil-cmg/dev/detail/dataset/export/export.ts +++ b/site-config/anvil-cmg/dev/detail/dataset/export/export.ts @@ -386,11 +386,11 @@ export const exportConfig: ExportConfig = { viewBuilder: V.buildDatasetExportMethodTerra, } as ComponentConfig, { - component: C.ExportMethod, + component: C.VelseraExportMethod, viewBuilder: V.buildDatasetExportToPlatformMethod( EXPORT_METHODS.BIO_DATA_CATALYST ), - } as ComponentConfig, + } as ComponentConfig, { component: C.ExportMethod, viewBuilder: V.buildDatasetExportToPlatformMethod( diff --git a/site-config/anvil-cmg/dev/export/constants.tsx b/site-config/anvil-cmg/dev/export/constants.tsx index b2d2dc83c..7f3fc8f6d 100644 --- a/site-config/anvil-cmg/dev/export/constants.tsx +++ b/site-config/anvil-cmg/dev/export/constants.tsx @@ -8,13 +8,17 @@ export const EXPORTS: Record< string, Pick< ComponentProps, - "buttonLabel" | "description" | "successTitle" | "title" + "buttonLabel" | "description" | "platformImport" | "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.", + platformImport: { + baseUrl: "https://platform.sb.biodatacatalyst.nhlbi.nih.gov/import/pfb", + source: "bdc", + }, successTitle: "Your BioData Catalyst Workspace Link is Ready", title: "Analyze in BioData Catalyst", }, @@ -38,7 +42,7 @@ export const EXPORT_METHODS: Record< string, Pick< ComponentProps, - "description" | "icon" | "route" | "title" + "comingSoon" | "description" | "icon" | "route" | "title" > > = { BIO_DATA_CATALYST: { @@ -48,12 +52,14 @@ export const EXPORT_METHODS: Record< title: "Export to BioData Catalyst Powered by Seven Bridges (BDC-SB)", }, CANCER_GENOMICS_CLOUD: { + comingSoon: true, description: EXPORTS.CANCER_GENOMICS_CLOUD.description, icon: , route: ROUTES.CANCER_GENOMICS_CLOUD, title: "Export to Cancer Genomics Cloud (CGC)", }, CAVATICA: { + comingSoon: true, description: EXPORTS.CAVATICA.description, icon: , route: ROUTES.CAVATICA, diff --git a/site-config/anvil-cmg/dev/export/export.ts b/site-config/anvil-cmg/dev/export/export.ts index 818fc6e49..6ed31163d 100644 --- a/site-config/anvil-cmg/dev/export/export.ts +++ b/site-config/anvil-cmg/dev/export/export.ts @@ -188,11 +188,11 @@ export const exportConfig: ExportConfig = { viewBuilder: V.buildExportMethodTerra, } as ComponentConfig, { - component: C.ExportMethod, + component: C.VelseraExportMethod, viewBuilder: V.buildExportToPlatformMethod( EXPORT_METHODS.BIO_DATA_CATALYST ), - } as ComponentConfig, + } as ComponentConfig, { component: C.ExportMethod, viewBuilder: V.buildExportToPlatformMethod( diff --git a/site-config/anvil-cmg/dev/export/utils.ts b/site-config/anvil-cmg/dev/export/utils.ts new file mode 100644 index 000000000..1884be81a --- /dev/null +++ b/site-config/anvil-cmg/dev/export/utils.ts @@ -0,0 +1,14 @@ +import { EXPORTS } from "./constants"; +import { ROUTES } from "./routes"; + +/** + * Returns true if the route is a platform export whose platform has no import endpoint. + * @param route - Export method route, e.g. "/export/cavatica". + * @returns true if the route is a platform export without an import endpoint. + */ +export function isUnavailablePlatformExportRoute(route: string): boolean { + return Object.entries(ROUTES).some( + ([key, path]) => + path === route && key in EXPORTS && !EXPORTS[key].platformImport + ); +} From 2c20688607347624d555de92d402c31a547d8fdf Mon Sep 17 00:00:00 2001 From: Fran McDade <18710366+frano-m@users.noreply.github.com> Date: Wed, 11 Nov 2026 14:22:20 +1000 Subject: [PATCH 2/2] feat: link the BDC-SB export page to Velsera behind a feature flag (#5027) Addresses review feedback on #5030: - Drop the 404 for platform export pages without an import endpoint (CAVATICA, CGC); "Request Link" stays disabled on those pages instead. - Return a boolean from `isPlatformImportAvailable` and `undefined` from `getPlatformImportUrl` when no import URL is available. - Add render tests for `ExportToPlatform` and `comingSoon` tests for the platform export method builders. Co-Authored-By: Claude Opus 5.5 --- __tests__/components/exportToPlatform.test.ts | 70 ----- .../components/exportToPlatform.test.tsx | 266 ++++++++++++++++++ __tests__/site-config/export-utils.test.ts | 28 -- .../viewModelBuilders/dataset-export.test.ts | 27 ++ __tests__/viewModelBuilders/export.test.ts | 25 ++ .../platform/ExportToPlatform/utils.ts | 9 +- pages/[entityListType]/[...params].tsx | 16 -- pages/export/biodata-catalyst.tsx | 7 +- pages/export/cancer-genomics-cloud.tsx | 7 +- pages/export/cavatica.tsx | 7 +- site-config/anvil-cmg/dev/export/utils.ts | 14 - 11 files changed, 325 insertions(+), 151 deletions(-) delete mode 100644 __tests__/components/exportToPlatform.test.ts create mode 100644 __tests__/components/exportToPlatform.test.tsx delete mode 100644 __tests__/site-config/export-utils.test.ts delete mode 100644 site-config/anvil-cmg/dev/export/utils.ts diff --git a/__tests__/components/exportToPlatform.test.ts b/__tests__/components/exportToPlatform.test.ts deleted file mode 100644 index b63ea5743..000000000 --- a/__tests__/components/exportToPlatform.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { FileLocation } from "@databiosphere/findable-ui/lib/hooks/useRequestFileLocation"; -import type { PlatformImport } from "../../app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/types"; -import { - buildPlatformImportUrl, - getPlatformImportUrl, - isPlatformImportAvailable, -} from "../../app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/utils"; - -const PLATFORM_IMPORT: PlatformImport = { - baseUrl: "https://platform.sb.biodatacatalyst.nhlbi.nih.gov/import/pfb", - source: "bdc", -}; - -const SIGNED_URL = - "https://storage.googleapis.com/bucket/manifest.avro?X-Goog-Signature=a%2Fb&X-Goog-Expires=3600"; - -const IMPORT_URL = - "https://platform.sb.biodatacatalyst.nhlbi.nih.gov/import/pfb?URL=https%3A%2F%2Fstorage.googleapis.com%2Fbucket%2Fmanifest.avro%3FX-Goog-Signature%3Da%252Fb%26X-Goog-Expires%3D3600&source=bdc"; - -const FILE_LOCATION: FileLocation = { location: SIGNED_URL, status: 302 }; - -describe("buildPlatformImportUrl", () => { - it("encodes the signed URL and appends the source", () => { - expect(buildPlatformImportUrl(PLATFORM_IMPORT, SIGNED_URL)).toBe( - IMPORT_URL - ); - }); - - it("round-trips the signed URL through the URL parameter", () => { - const url = new URL(buildPlatformImportUrl(PLATFORM_IMPORT, SIGNED_URL)); - expect(url.searchParams.get("URL")).toBe(SIGNED_URL); - expect(url.searchParams.get("source")).toBe("bdc"); - }); -}); - -describe("getPlatformImportUrl", () => { - it("returns the import URL when enabled with a config and file location", () => { - expect(getPlatformImportUrl(true, PLATFORM_IMPORT, FILE_LOCATION)).toBe( - IMPORT_URL - ); - }); - - it("returns an empty string when the feature flag is off", () => { - expect(getPlatformImportUrl(false, PLATFORM_IMPORT, FILE_LOCATION)).toBe( - "" - ); - }); - - it("returns an empty string when the platform has no import config", () => { - expect(getPlatformImportUrl(true, undefined, FILE_LOCATION)).toBe(""); - }); - - it("returns an empty string before the file location is available", () => { - expect(getPlatformImportUrl(true, PLATFORM_IMPORT, undefined)).toBe(""); - }); -}); - -describe("isPlatformImportAvailable", () => { - it("returns true when enabled with a config", () => { - expect(isPlatformImportAvailable(true, PLATFORM_IMPORT)).toBe(true); - }); - - it("returns false when the feature flag is off", () => { - expect(isPlatformImportAvailable(false, PLATFORM_IMPORT)).toBe(false); - }); - - it("returns false when the platform has no import config", () => { - expect(isPlatformImportAvailable(true, undefined)).toBe(false); - }); -}); diff --git a/__tests__/components/exportToPlatform.test.tsx b/__tests__/components/exportToPlatform.test.tsx new file mode 100644 index 000000000..c85048105 --- /dev/null +++ b/__tests__/components/exportToPlatform.test.tsx @@ -0,0 +1,266 @@ +import type { + FileLocation, + UseRequestFileLocationResult, +} from "@databiosphere/findable-ui/lib/hooks/useRequestFileLocation"; +import type { FileManifestState } from "@databiosphere/findable-ui/lib/providers/fileManifestState"; +import { createAppTheme } from "@databiosphere/findable-ui/lib/theme/theme"; +import { ThemeProvider } from "@mui/material"; +import { render, screen } from "@testing-library/react"; +import { ExportToPlatform } from "../../app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/exportToPlatform"; +import type { + PlatformImport, + Props, +} from "../../app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/types"; +import { + buildPlatformImportUrl, + getPlatformImportUrl, + isPlatformImportAvailable, +} from "../../app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/utils"; +import { FEATURES } from "../../app/shared/entities"; + +const mockUseRequestFileLocation = jest.fn(); + +jest.mock("@databiosphere/findable-ui/lib/hooks/useDownloadStatus", () => ({ + useDownloadStatus: (): { + disabled: boolean; + isLoading: boolean; + message: string; + } => ({ disabled: false, isLoading: false, message: "" }), +})); + +jest.mock( + "@databiosphere/findable-ui/lib/hooks/useFileManifest/useFileManifest", + () => ({ useFileManifest: jest.fn() }) +); + +jest.mock( + "@databiosphere/findable-ui/lib/hooks/useFileManifest/useFileManifestFileCount", + () => ({ useFileManifestFileCount: jest.fn() }) +); + +jest.mock("@databiosphere/findable-ui/lib/hooks/useFileManifestState", () => ({ + useFileManifestState: (): { + fileManifestDispatch: jest.Mock; + fileManifestState: { isLoading: boolean }; + } => ({ + fileManifestDispatch: jest.fn(), + fileManifestState: { isLoading: false }, + }), +})); + +jest.mock( + "@databiosphere/findable-ui/lib/hooks/useRequestFileLocation", + () => ({ + useRequestFileLocation: (): UseRequestFileLocationResult => + mockUseRequestFileLocation(), + }) +); + +jest.mock( + "@databiosphere/findable-ui/lib/hooks/useRequestManifest/useRequestManifest", + () => ({ + useRequestManifest: (): { requestMethod: string; requestUrl: string } => ({ + requestMethod: "PUT", + requestUrl: "https://service.example.org/manifest/files", + }), + }) +); + +jest.mock("@databiosphere/findable-ui/lib/providers/loginGuard/hook", () => ({ + useLoginGuard: (): { requireLogin: jest.Mock } => ({ + requireLogin: jest.fn(), + }), +})); + +const PLATFORM_IMPORT: PlatformImport = { + baseUrl: "https://platform.sb.biodatacatalyst.nhlbi.nih.gov/import/pfb", + source: "bdc", +}; + +const SIGNED_URL = + "https://storage.googleapis.com/bucket/manifest.avro?X-Goog-Signature=a%2Fb&X-Goog-Expires=3600"; + +const IMPORT_URL = + "https://platform.sb.biodatacatalyst.nhlbi.nih.gov/import/pfb?URL=https%3A%2F%2Fstorage.googleapis.com%2Fbucket%2Fmanifest.avro%3FX-Goog-Signature%3Da%252Fb%26X-Goog-Expires%3D3600&source=bdc"; + +const FILE_LOCATION: FileLocation = { location: SIGNED_URL, status: 302 }; + +describe("buildPlatformImportUrl", () => { + it("encodes the signed URL and appends the source", () => { + expect(buildPlatformImportUrl(PLATFORM_IMPORT, SIGNED_URL)).toBe( + IMPORT_URL + ); + }); + + it("round-trips the signed URL through the URL parameter", () => { + const url = new URL(buildPlatformImportUrl(PLATFORM_IMPORT, SIGNED_URL)); + expect(url.searchParams.get("URL")).toBe(SIGNED_URL); + expect(url.searchParams.get("source")).toBe("bdc"); + }); +}); + +describe("getPlatformImportUrl", () => { + it("returns the import URL when enabled with a config and file location", () => { + expect(getPlatformImportUrl(true, PLATFORM_IMPORT, FILE_LOCATION)).toBe( + IMPORT_URL + ); + }); + + it("returns undefined when the feature flag is off", () => { + expect( + getPlatformImportUrl(false, PLATFORM_IMPORT, FILE_LOCATION) + ).toBeUndefined(); + }); + + it("returns undefined when the platform has no import config", () => { + expect( + getPlatformImportUrl(true, undefined, FILE_LOCATION) + ).toBeUndefined(); + }); + + it("returns undefined before the file location is available", () => { + expect( + getPlatformImportUrl(true, PLATFORM_IMPORT, undefined) + ).toBeUndefined(); + }); +}); + +describe("isPlatformImportAvailable", () => { + it("returns true when enabled with a config", () => { + expect(isPlatformImportAvailable(true, PLATFORM_IMPORT)).toBe(true); + }); + + it("returns false when the feature flag is off", () => { + expect(isPlatformImportAvailable(false, PLATFORM_IMPORT)).toBe(false); + }); + + it("returns false when the platform has no import config", () => { + expect(isPlatformImportAvailable(true, undefined)).toBe(false); + }); +}); + +const BUTTON_LABEL = "Open BioData Catalyst"; + +const REQUEST_LINK = "Request Link"; + +const FILE_MANIFEST_STATE: FileManifestState = { + fileCount: undefined, + fileSummary: undefined, + fileSummaryFacetName: undefined, + fileSummaryFilters: [], + filesFacets: [], + filters: [], + isEnabled: true, + isFacetsLoading: false, + isFacetsSuccess: true, + isFileSummaryLoading: false, + isLoading: false, + isSummaryLoading: false, + summary: undefined, +}; + +const PROPS: Props = { + buttonLabel: BUTTON_LABEL, + description: "BDC-SB is a cloud workspace.", + fileManifestState: FILE_MANIFEST_STATE, + fileSummaryFacetName: "files.file_format", + filters: [], + formFacet: {}, + platformImport: PLATFORM_IMPORT, + speciesFacetName: "donors.organism_type", + successTitle: "Your BioData Catalyst Workspace Link is Ready", + title: "Analyze in BioData Catalyst", +}; + +const THEME = createAppTheme(); + +/** + * Builds a useRequestFileLocation result. + * @param data - File location returned by the manifest request. + * @returns useRequestFileLocation result. + */ +function buildFileLocationResult( + data: FileLocation | undefined +): UseRequestFileLocationResult { + return { + data, + isIdle: !data, + isLoading: false, + isSuccess: !!data, + run: jest.fn(), + }; +} + +/** + * Returns the "Request Link" button. + * @returns "Request Link" button element. + */ +function getRequestLinkButton(): HTMLElement { + return screen.getByRole("button", { name: REQUEST_LINK }); +} + +/** + * Renders ExportToPlatform within the app theme. + * @param props - Export to platform props. + */ +function renderExportToPlatform(props: Props): void { + render( + + + + ); +} + +describe("ExportToPlatform", () => { + afterEach(() => { + localStorage.clear(); + mockUseRequestFileLocation.mockReset(); + }); + + it("disables Request Link when the Velsera export flag is off", () => { + mockUseRequestFileLocation.mockReturnValue( + buildFileLocationResult(undefined) + ); + renderExportToPlatform(PROPS); + expect(getRequestLinkButton().hasAttribute("disabled")).toBe(true); + }); + + it("disables Request Link when the platform has no import endpoint", () => { + localStorage.setItem(FEATURES.VELSERA_EXPORT, "true"); + mockUseRequestFileLocation.mockReturnValue( + buildFileLocationResult(undefined) + ); + renderExportToPlatform({ ...PROPS, platformImport: undefined }); + expect(getRequestLinkButton().hasAttribute("disabled")).toBe(true); + }); + + it("enables Request Link when the Velsera export flag is on", () => { + localStorage.setItem(FEATURES.VELSERA_EXPORT, "true"); + mockUseRequestFileLocation.mockReturnValue( + buildFileLocationResult(undefined) + ); + renderExportToPlatform(PROPS); + expect(getRequestLinkButton().hasAttribute("disabled")).toBe(false); + }); + + it("links to the import URL once the PFB location is available", () => { + localStorage.setItem(FEATURES.VELSERA_EXPORT, "true"); + mockUseRequestFileLocation.mockReturnValue( + buildFileLocationResult(FILE_LOCATION) + ); + renderExportToPlatform(PROPS); + const link = screen.getByRole("link", { name: BUTTON_LABEL }); + expect(link.getAttribute("href")).toBe(IMPORT_URL); + expect(link.getAttribute("target")).toBe("_blank"); + expect(link.getAttribute("rel")).toBe("noopener noreferrer"); + }); + + it("keeps the form when the flag is off, even with a PFB location", () => { + mockUseRequestFileLocation.mockReturnValue( + buildFileLocationResult(FILE_LOCATION) + ); + renderExportToPlatform(PROPS); + expect(screen.queryByRole("link", { name: BUTTON_LABEL })).toBeNull(); + expect(getRequestLinkButton().hasAttribute("disabled")).toBe(true); + }); +}); diff --git a/__tests__/site-config/export-utils.test.ts b/__tests__/site-config/export-utils.test.ts deleted file mode 100644 index 5a4c699f8..000000000 --- a/__tests__/site-config/export-utils.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ROUTES } from "../../site-config/anvil-cmg/dev/export/routes"; -import { isUnavailablePlatformExportRoute } from "../../site-config/anvil-cmg/dev/export/utils"; - -describe("isUnavailablePlatformExportRoute", () => { - it.each([ROUTES.CANCER_GENOMICS_CLOUD, ROUTES.CAVATICA])( - "returns true for %s, which has no import endpoint", - (route) => { - expect(isUnavailablePlatformExportRoute(route)).toBe(true); - } - ); - - it("returns false for BioData Catalyst, which has an import endpoint", () => { - expect(isUnavailablePlatformExportRoute(ROUTES.BIO_DATA_CATALYST)).toBe( - false - ); - }); - - it.each([ROUTES.CURL_DOWNLOAD, ROUTES.MANIFEST_DOWNLOAD, ROUTES.TERRA])( - "returns false for %s, which is not a platform export", - (route) => { - expect(isUnavailablePlatformExportRoute(route)).toBe(false); - } - ); - - it("returns false for an unknown route", () => { - expect(isUnavailablePlatformExportRoute("/export/unknown")).toBe(false); - }); -}); diff --git a/__tests__/viewModelBuilders/dataset-export.test.ts b/__tests__/viewModelBuilders/dataset-export.test.ts index b2fc36117..63ea8d40e 100644 --- a/__tests__/viewModelBuilders/dataset-export.test.ts +++ b/__tests__/viewModelBuilders/dataset-export.test.ts @@ -271,6 +271,33 @@ describe("buildDatasetExportToPlatformHero", () => { }); describe("buildDatasetExportToPlatformMethod", () => { + describe("coming soon", () => { + it.each([EXPORT_METHODS.CANCER_GENOMICS_CLOUD, EXPORT_METHODS.CAVATICA])( + "returns comingSoon true for $title", + (props) => { + const builder = buildDatasetExportToPlatformMethod(props); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.comingSoon).toBe(true); + } + ); + + it("returns comingSoon undefined for BioData Catalyst", () => { + const builder = buildDatasetExportToPlatformMethod( + EXPORT_METHODS.BIO_DATA_CATALYST + ); + const response = createMockDatasetsResponse(); + const viewContext = createMockViewContext(); + + const result = builder(response, viewContext); + + expect(result.comingSoon).toBeUndefined(); + }); + }); + it("returns route with dataset path prefix", () => { const props = EXPORT_METHODS.BIO_DATA_CATALYST; const builder = buildDatasetExportToPlatformMethod(props); diff --git a/__tests__/viewModelBuilders/export.test.ts b/__tests__/viewModelBuilders/export.test.ts index 98cffa0ab..758cc28ce 100644 --- a/__tests__/viewModelBuilders/export.test.ts +++ b/__tests__/viewModelBuilders/export.test.ts @@ -229,6 +229,31 @@ describe("buildExportToPlatformHero", () => { }); describe("buildExportToPlatformMethod", () => { + describe("coming soon", () => { + it.each([EXPORT_METHODS.CANCER_GENOMICS_CLOUD, EXPORT_METHODS.CAVATICA])( + "returns comingSoon true for $title", + (props) => { + const builder = buildExportToPlatformMethod(props); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.comingSoon).toBe(true); + } + ); + + it("returns comingSoon undefined for BioData Catalyst", () => { + const builder = buildExportToPlatformMethod( + EXPORT_METHODS.BIO_DATA_CATALYST + ); + const viewContext = createMockViewContext(); + + const result = builder(undefined, viewContext); + + expect(result.comingSoon).toBeUndefined(); + }); + }); + it("returns route from props", () => { const props = EXPORT_METHODS.BIO_DATA_CATALYST; const builder = buildExportToPlatformMethod(props); diff --git a/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/utils.ts b/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/utils.ts index aef247415..95fc69fe1 100644 --- a/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/utils.ts +++ b/app/components/Export/components/AnVILExplorer/platform/ExportToPlatform/utils.ts @@ -22,15 +22,14 @@ export function buildPlatformImportUrl( * @param isEnabled - Whether the platform export feature flag is on. * @param platformImport - Platform import endpoint and source; undefined when the platform has none. * @param fileLocation - PFB file location; undefined until the manifest request succeeds. - * @returns import URL, or an empty string when the flag is off, the platform has no import endpoint, or the PFB location is not yet available. + * @returns import URL, or undefined when the flag is off, the platform has no import endpoint, or the PFB location is not yet available. */ export function getPlatformImportUrl( isEnabled: boolean, platformImport: PlatformImport | undefined, fileLocation: FileLocation | undefined -): string { - if (!isPlatformImportAvailable(isEnabled, platformImport) || !fileLocation) - return ""; +): string | undefined { + if (!isEnabled || !platformImport || !fileLocation) return undefined; return buildPlatformImportUrl(platformImport, fileLocation.location); } @@ -43,6 +42,6 @@ export function getPlatformImportUrl( export function isPlatformImportAvailable( isEnabled: boolean, platformImport: PlatformImport | undefined -): platformImport is PlatformImport { +): boolean { return isEnabled && platformImport !== undefined; } diff --git a/pages/[entityListType]/[...params].tsx b/pages/[entityListType]/[...params].tsx index 5c5fa6604..65b3b9a0f 100644 --- a/pages/[entityListType]/[...params].tsx +++ b/pages/[entityListType]/[...params].tsx @@ -37,7 +37,6 @@ import type { SchemaDataset } from "../../app/utils/schemaOrg/types"; import { readFile } from "../../app/utils/tsvParser"; import { JsonLd } from "../../app/views/EntityDetailView/components/JsonLd/jsonLd"; import { ROUTES } from "../../site-config/anvil-cmg/dev/export/routes"; -import { isUnavailablePlatformExportRoute } from "../../site-config/anvil-cmg/dev/export/utils"; import type { DatasetsResponse } from "../../app/apis/azul/anvil-cmg/common/responses"; import { @@ -96,10 +95,6 @@ const EntityDetailPage = (props: EntityDetailPageProps): JSX.Element => { ) { return ; } - // Platform exports without an import endpoint are not available (AnVIL only) - if (isAnVIL && isUnavailablePlatformExportMethod(query)) { - return ; - } if (isChooseExportView(query)) return ; if (isExportMethodView(query)) return ; return ( @@ -164,17 +159,6 @@ function isOverride(override: Override): boolean { ); } -/** - * Returns true if the current route is a platform export method whose platform has no import endpoint. - * @param query - Parsed URL query. - * @returns True if the route is an unavailable platform export method. - */ -function isUnavailablePlatformExportMethod(query: ParsedUrlQuery): boolean { - const params = query.params as string[] | undefined; - const lastParam = params?.[params.length - 1] || ""; - return isUnavailablePlatformExportRoute(`/export/${lastParam}`); -} - /** * Check if the current view matches the expected export path segment. * @param query - The current query object. diff --git a/pages/export/biodata-catalyst.tsx b/pages/export/biodata-catalyst.tsx index 1fdf21f5f..6c30eb30c 100644 --- a/pages/export/biodata-catalyst.tsx +++ b/pages/export/biodata-catalyst.tsx @@ -1,9 +1,6 @@ import { ExportMethodView } from "@databiosphere/findable-ui/lib/views/ExportMethodView/exportMethodView"; import { GetStaticProps } from "next"; -import NextError from "next/error"; import { JSX } from "react"; -import { ROUTES } from "../../site-config/anvil-cmg/dev/export/routes"; -import { isUnavailablePlatformExportRoute } from "../../site-config/anvil-cmg/dev/export/utils"; export const getStaticProps: GetStaticProps = async () => { return { @@ -16,11 +13,9 @@ export const getStaticProps: GetStaticProps = async () => { /** * Export method page for BioData Catalyst. - * @returns export method view component, or a 404 when the platform has no import endpoint. + * @returns export method view component. */ const ExportMethodPage = (): JSX.Element => { - if (isUnavailablePlatformExportRoute(ROUTES.BIO_DATA_CATALYST)) - return ; return ; }; diff --git a/pages/export/cancer-genomics-cloud.tsx b/pages/export/cancer-genomics-cloud.tsx index a2c3aad7a..b0644aec6 100644 --- a/pages/export/cancer-genomics-cloud.tsx +++ b/pages/export/cancer-genomics-cloud.tsx @@ -1,9 +1,6 @@ import { ExportMethodView } from "@databiosphere/findable-ui/lib/views/ExportMethodView/exportMethodView"; import { GetStaticProps } from "next"; -import NextError from "next/error"; import { JSX } from "react"; -import { ROUTES } from "../../site-config/anvil-cmg/dev/export/routes"; -import { isUnavailablePlatformExportRoute } from "../../site-config/anvil-cmg/dev/export/utils"; export const getStaticProps: GetStaticProps = async () => { return { @@ -16,11 +13,9 @@ export const getStaticProps: GetStaticProps = async () => { /** * Export method page for Cancer Genomics Cloud. - * @returns export method view component, or a 404 when the platform has no import endpoint. + * @returns export method view component. */ const ExportMethodPage = (): JSX.Element => { - if (isUnavailablePlatformExportRoute(ROUTES.CANCER_GENOMICS_CLOUD)) - return ; return ; }; diff --git a/pages/export/cavatica.tsx b/pages/export/cavatica.tsx index f84d42139..4b3e9a1dc 100644 --- a/pages/export/cavatica.tsx +++ b/pages/export/cavatica.tsx @@ -1,9 +1,6 @@ import { ExportMethodView } from "@databiosphere/findable-ui/lib/views/ExportMethodView/exportMethodView"; import { GetStaticProps } from "next"; -import NextError from "next/error"; import { JSX } from "react"; -import { ROUTES } from "../../site-config/anvil-cmg/dev/export/routes"; -import { isUnavailablePlatformExportRoute } from "../../site-config/anvil-cmg/dev/export/utils"; export const getStaticProps: GetStaticProps = async () => { return { @@ -16,11 +13,9 @@ export const getStaticProps: GetStaticProps = async () => { /** * Export method page for CAVATICA. - * @returns export method view component, or a 404 when the platform has no import endpoint. + * @returns export method view component. */ const ExportMethodPage = (): JSX.Element => { - if (isUnavailablePlatformExportRoute(ROUTES.CAVATICA)) - return ; return ; }; diff --git a/site-config/anvil-cmg/dev/export/utils.ts b/site-config/anvil-cmg/dev/export/utils.ts deleted file mode 100644 index 1884be81a..000000000 --- a/site-config/anvil-cmg/dev/export/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { EXPORTS } from "./constants"; -import { ROUTES } from "./routes"; - -/** - * Returns true if the route is a platform export whose platform has no import endpoint. - * @param route - Export method route, e.g. "/export/cavatica". - * @returns true if the route is a platform export without an import endpoint. - */ -export function isUnavailablePlatformExportRoute(route: string): boolean { - return Object.entries(ROUTES).some( - ([key, path]) => - path === route && key in EXPORTS && !EXPORTS[key].platformImport - ); -}