diff --git a/app/components/Export/components/AnVILExplorer/components/ExportEntity/exportEntity.tsx b/app/components/Export/components/AnVILExplorer/components/ExportEntity/exportEntity.tsx new file mode 100644 index 000000000..53fede3a1 --- /dev/null +++ b/app/components/Export/components/AnVILExplorer/components/ExportEntity/exportEntity.tsx @@ -0,0 +1,18 @@ +import { Filters } from "@databiosphere/findable-ui/lib/common/entities"; +import { useFileManifest } from "@databiosphere/findable-ui/lib/hooks/useFileManifest/useFileManifest"; + +export interface ExportEntityProps { + filters: Filters; +} + +/** + * Empty wrapper component that triggers calls to populate the selected data in the side channel. Required only + * for choose export method functionality (as individual export methods trigger the required calls themsleves). + * @param {Object} props - The properties object. + * @param {ExportEntityProps} props.filters - The base filters for displaying the selected data related. + * @returns Fragment. + */ +export const ExportEntity = ({ filters }: ExportEntityProps): JSX.Element => { + useFileManifest(filters); + return <>; +}; diff --git a/app/components/index.tsx b/app/components/index.tsx index 03336e852..8e9d586dd 100644 --- a/app/components/index.tsx +++ b/app/components/index.tsx @@ -95,6 +95,7 @@ export { FileNameCell } from "./Detail/components/GeneratedMatricesTables/compon export { GeneratedMatricesTables } from "./Detail/components/GeneratedMatricesTables/generatedMatricesTables"; export { AtlasSection } from "./Detail/components/Section/components/AtlasSection/atlasSection"; export { ManifestDownloadEntity as AnVILManifestDownloadEntity } from "./Export/components/AnVILExplorer/components/ManifestDownload/components/ManifestDownloadEntity/manifestDownloadEntity"; +export { ExportEntity as AnVILExportEntity } from "./Export/components/AnVILExplorer/components/ExportEntity/exportEntity"; 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/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts b/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts index e5bc0c1f5..6da95403a 100644 --- a/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts +++ b/app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts @@ -111,6 +111,7 @@ import { FEATURE_FLAGS } from "../../../common/contants"; import { Unused, Void } from "../../../common/entities"; import { SUMMARY_DISPLAY_TEXT } from "./summaryMapper/constants"; import { mapExportSummary } from "./summaryMapper/summaryMapper"; +import { ExportEntity } from "app/components/Export/components/AnVILExplorer/components/ExportEntity/exportEntity"; /** * Build props for activity type BasicCell component from the given activities response. @@ -126,29 +127,52 @@ export const buildActivityType = ( }; /** - * Build props for list view access warning Alert component. + * Build props for dataset-related export warning Alert component. * @param _ - Unused. * @param viewContext - View context. * @returns model to be used as props for the Alert component. */ -export const buildAlertEntityListWarning = ( +export const buildAlertDatasetExportWarning = ( _: Unused, - viewContext: ViewContext -): React.ComponentProps => { + viewContext: ViewContext +): React.ComponentProps => { + const content = isUserAuthenticated(viewContext) + ? "To export this dataset, please request access." + : "To export this dataset, please sign in and, if necessary, request access."; return { ...ALERT_PROPS.STANDARD_WARNING, component: C.FluidPaper, - entityName: viewContext.entityConfig.label, + content, }; }; /** - * Build props for entity related export warning Alert component. + * Build props for entity related download manifest warning Alert component. * @param _ - Unused. * @param viewContext - View context. * @returns model to be used as props for the Alert component. */ -export const buildAlertExportEntityWarning = ( +export const buildAlertDatasetManifestDownloadWarning = ( + _: Unused, + viewContext: ViewContext +): React.ComponentProps => { + const content = isUserAuthenticated(viewContext) + ? "To download this dataset manifest, please request access." + : "To download this dataset manifest, please sign in and, if necessary, request access."; + return { + ...ALERT_PROPS.STANDARD_WARNING, + component: C.FluidPaper, + content, + }; +}; + +/** + * Build props for dataset-related export warning Alert component. + * @param _ - Unused. + * @param viewContext - View context. + * @returns model to be used as props for the Alert component. + */ +export const buildAlertDatasetTerraExportWarning = ( _: Unused, viewContext: ViewContext ): React.ComponentProps => { @@ -168,41 +192,38 @@ export const buildAlertExportEntityWarning = ( }; /** - * Build props for export warning Alert component. + * Build props for list view access warning Alert component. * @param _ - Unused. * @param viewContext - View context. * @returns model to be used as props for the Alert component. */ -export const buildAlertExportWarning = ( +export const buildAlertEntityListWarning = ( _: Unused, viewContext: ViewContext -): React.ComponentProps => { - const isAuthenticated = isUserAuthenticated(viewContext); +): React.ComponentProps => { return { ...ALERT_PROPS.STANDARD_WARNING, component: C.FluidPaper, - content: isAuthenticated ? null : MDX.AlertExportWarningContent({}), - size: isAuthenticated ? SIZE.MEDIUM : SIZE.LARGE, + entityName: viewContext.entityConfig.label, }; }; /** - * Build props for entity related download manifest warning Alert component. + * Build props for export warning Alert component. * @param _ - Unused. * @param viewContext - View context. * @returns model to be used as props for the Alert component. */ -export const buildAlertManifestDownloadEntityWarning = ( +export const buildAlertExportWarning = ( _: Unused, - viewContext: ViewContext -): React.ComponentProps => { - const content = isUserAuthenticated(viewContext) - ? "To download this dataset manifest, please request access." - : "To download this dataset manifest, please sign in and, if necessary, request access."; + viewContext: ViewContext +): React.ComponentProps => { + const isAuthenticated = isUserAuthenticated(viewContext); return { ...ALERT_PROPS.STANDARD_WARNING, component: C.FluidPaper, - content, + content: isAuthenticated ? null : MDX.AlertExportWarningContent({}), + size: isAuthenticated ? SIZE.MEDIUM : SIZE.LARGE, }; }; @@ -351,6 +372,137 @@ export const buildDatasetDetails = ( }; }; +/** + * Build base breadcrumbs for dataset export. Includes link to all datasets and + * the selected dataset. + * @param datasetsResponse - Response model return from datasets API. + * @returns array of breadcrumbs to be used by dataset export and dataset export method pages. + */ +export function buildDatasetExportBreadcrumbs( + datasetsResponse: DatasetsResponse +): Breadcrumb[] { + const datasetPath = buildDatasetPath(datasetsResponse); + const datasetTitle = getDatasetTitle(datasetsResponse); + return [ + { path: URL_DATASETS, text: "Datasets" }, + { path: datasetPath, text: datasetTitle }, + ]; +} + +/** + * Build props for dataset export BackPageHero component. + * @param datasetsResponse - Response model return from datasets API. + * @returns model to be used as props for the BackPageHero component. + */ +export function buildDatasetExportHero( + datasetsResponse: DatasetsResponse +): React.ComponentProps { + return { + breadcrumbs: [ + ...buildDatasetExportBreadcrumbs(datasetsResponse), + { path: "", text: "Choose Export Method" }, + ], + title: getDatasetTitle(datasetsResponse), + }; +} + +/** + * Returns breadcrumbs and title for dataset export method Hero component. + * @param datasetsResponse - Response model return from datasets API. + * @param title - Short export method description (e.g. Request File Manifest). + * @returns model to be used as props for the Hero component. + */ +function getDatasetExportMethodHero( + datasetsResponse: DatasetsResponse, + title: string +): React.ComponentProps { + const datasetPath = buildDatasetPath(datasetsResponse); + return { + breadcrumbs: [ + ...buildDatasetExportBreadcrumbs(datasetsResponse), + { path: `${datasetPath}/export`, text: "Choose Export Method" }, + { path: "", text: title }, + ], + title: getDatasetTitle(datasetsResponse), + }; +} + +/** + * Build props for dataset manifest download BackPageHero component. + * @param datasetsResponse - Response model return from datasets API. + * @returns model to be used as props for the BackPageHero component. + */ +export const buildDatasetExportMethodHeroManifestDownload = ( + datasetsResponse: DatasetsResponse +): React.ComponentProps => { + const title = "File Manifest"; + return getDatasetExportMethodHero(datasetsResponse, title); +}; + +/** + * Build props for dataset manifest download BackPageHero component. + * @param datasetsResponse - Response model return from datasets API. + * @returns model to be used as props for the BackPageHero component. + */ +export const buildDatasetExportMethodHeroTerraExport = ( + datasetsResponse: DatasetsResponse +): React.ComponentProps => { + const title = "Analyze in Terra"; + return getDatasetExportMethodHero(datasetsResponse, title); +}; + +/** + * Build props for ExportMethod component for display of the dataset manifest download section. + * @param datasetsResponse - Response model return from datasets API. + * @returns model to be used as props for the dataset file manifest export method component. + */ +export const buildDatasetExportMethodManifestDownload = ( + datasetsResponse: DatasetsResponse +): React.ComponentProps => { + const datasetPath = buildDatasetPath(datasetsResponse); + return { + 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}`, + title: "Download a File Manifest with Metadata", + }; +}; + +/** + * Build props for either the ExportEntity component for the display of the choose export methods or + * the AnVILManifestDownloadEntity component for the display of the manifest download method. + * @param datasetsResponse - Response model return from datasets API. + * @returns model to be used as props for the ExportEntity component. + */ +export const buildDatasetExportPropsWithFilter = ( + datasetsResponse: DatasetsResponse +): + | React.ComponentProps + | typeof C.AnVILManifestDownloadEntity => { + return { + filters: getExportEntityFilters(datasetsResponse), + }; +}; + +/** + * Build props for ExportMethod component for display of the export to terra metadata section. + * @param datasetsResponse - Response model return from datasets API. + * @returns model to be used as props for the dataset Terra export method component. + */ +export const buildDatasetExportMethodTerra = ( + datasetsResponse: DatasetsResponse +): React.ComponentProps => { + const datasetPath = buildDatasetPath(datasetsResponse); + return { + 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}`, + title: "Export Dataset Data and Metadata to Terra Workspace", + }; +}; + /** * Build props for BackPageHero component from the given datasets response. * @param datasetsResponse - Response model return from datasets API. @@ -380,6 +532,45 @@ export const buildDatasetIds = ( }; }; +/** + * Build path to dataset from the given datasets response. + * @param datasetsResponse - Response model return from datasets API. + * @returns path to the dataset. + */ +export function buildDatasetPath(datasetsResponse: DatasetsResponse): string { + const datasetId = getDatasetEntryId(datasetsResponse); + return `${URL_DATASETS}/${datasetId}`; +} + +/** + * Build props for dataset ExportToTerra component. + * @param datasetsResponse - Response model return from datasets API. + * @param viewContext - View context. + * @returns model to be used as props for the ExportToTerra component. + */ +export const builDatasetTerraExport = ( + datasetsResponse: DatasetsResponse, + viewContext: ViewContext +): React.ComponentProps => { + const { fileManifestState } = viewContext; + // Get the initial filters. + const filters = getExportEntityFilters(datasetsResponse); + // Grab the form facet. + const formFacet = getFormFacets(fileManifestState); + return { + ExportForm: C.ExportToTerraForm, + ExportToTerraStart: MDX.ExportToTerraStart, + ExportToTerraSuccess: MDX.ExportToTerraSuccess, + fileManifestState, + fileManifestType: FILE_MANIFEST_TYPE.ENTITY_EXPORT_TO_TERRA, + fileSummaryFacetName: ANVIL_CMG_CATEGORY_KEY.FILE_FILE_FORMAT, + filters, + formFacet, + manifestDownloadFormat: MANIFEST_DOWNLOAD_FORMAT.VERBATIM_PFB, + manifestDownloadFormats: [MANIFEST_DOWNLOAD_FORMAT.VERBATIM_PFB], + }; +}; + /** * Build dataset title Link component from the given datasets response. * @param datasetsResponse - Response model return from datasets API. @@ -479,35 +670,6 @@ export const buildExportCurrentQuery = ( }; }; -/** - * Build props for ExportToTerra component from the given datasets response. - * @param datasetsResponse - Response model return from datasets API. - * @param viewContext - View context. - * @returns model to be used as props for the ExportToTerra component. - */ -export const buildExportEntityToTerra = ( - datasetsResponse: DatasetsResponse, - viewContext: ViewContext -): React.ComponentProps => { - const { fileManifestState } = viewContext; - // Get the initial filters. - const filters = getExportEntityFilters(datasetsResponse); - // Grab the form facet. - const formFacet = getFormFacets(fileManifestState); - return { - ExportForm: C.ExportToTerraForm, - ExportToTerraStart: MDX.ExportToTerraStart, - ExportToTerraSuccess: MDX.ExportToTerraSuccess, - fileManifestState, - fileManifestType: FILE_MANIFEST_TYPE.ENTITY_EXPORT_TO_TERRA, - fileSummaryFacetName: ANVIL_CMG_CATEGORY_KEY.FILE_FILE_FORMAT, - filters, - formFacet, - manifestDownloadFormat: MANIFEST_DOWNLOAD_FORMAT.VERBATIM_PFB, - manifestDownloadFormats: [MANIFEST_DOWNLOAD_FORMAT.VERBATIM_PFB], - }; -}; - /** * Build props for export BackPageHero component. * @param _ - Void. @@ -768,19 +930,6 @@ export const buildManifestDownload = ( }; }; -/* - * Build props for ManifestDownloadEntity component. - * @param datasetsResponse - Response model return from datasets API. - * @returns model to be used as props for the ManifestDownloadEntity component. - */ -export const buildManifestDownloadEntity = ( - datasetsResponse: DatasetsResponse -): React.ComponentProps => { - return { - filters: getExportEntityFilters(datasetsResponse), - }; -}; - /** * Build props for organism type BasicCell component from the given donors response. * @param response - Response model return from index/donors API endpoint. @@ -931,18 +1080,26 @@ function getDatasetCallToAction( const isReady = isResponseReady(datasetsResponse); const isAccessGranted = isDatasetAccessible(datasetsResponse); const registeredIdentifier = getDatasetRegisteredIdentifier(datasetsResponse); - if ( - !isReady || - isAccessGranted || - registeredIdentifier === LABEL.UNSPECIFIED - ) { + if (!isReady) { return; } - return { - label: "Request Access", - target: ANCHOR_TARGET.BLANK, - url: `https://dbgap.ncbi.nlm.nih.gov/aa/wga.cgi?adddataset=${registeredIdentifier}`, - }; + // Display export button if user is authorized to access the dataset. + if (isAccessGranted) { + return { + label: "Export", + target: ANCHOR_TARGET.SELF, + url: `/datasets/${getDatasetEntryId(datasetsResponse)}/export`, + }; + } + // Display request access button if user is not authorized to access the dataset. + if (registeredIdentifier === LABEL.UNSPECIFIED) { + return { + label: "Request Access", + target: ANCHOR_TARGET.BLANK, + url: `https://dbgap.ncbi.nlm.nih.gov/aa/wga.cgi?adddataset=${registeredIdentifier}`, + }; + } + // Otherwise, display nothing. } /** @@ -1309,12 +1466,14 @@ export const renderWhenUnAuthenticated = ( }; /** - * Renders entity related export when the given datasests response is accessible. + * Renders dataset export to Terra component when the given datasests response is accessible. Note, + * this can be removed once the verbatim feature flag is removed (use renderDatasetExport instead). * @param datasetsResponse - Response model return from datasets API. * @param viewContext - View context. * @returns model to be used as props for the ConditionalComponent component. + * @deprecated */ -export const renderExportEntity = ( +export const renderDatasetTerraExport = ( datasetsResponse: DatasetsResponse, viewContext: ViewContext ): React.ComponentProps => { @@ -1329,12 +1488,14 @@ export const renderExportEntity = ( }; /** - * Renders entity related export warning when the given datasests response is not accessible. + * Renders dataset export to Terra warning component when the given datasests response is accessible. Note, + * this can be removed once the verbatim feature flag is removed (use renderDatasetExportWarning instead). * @param datasetsResponse - Response model return from datasets API. * @param viewContext - View context. * @returns model to be used as props for the ConditionalComponent component. + * @deprecated */ -export const renderExportEntityWarning = ( +export const renderDatasetTerraExportWarning = ( datasetsResponse: DatasetsResponse, viewContext: ViewContext ): React.ComponentProps => { @@ -1342,19 +1503,19 @@ export const renderExportEntityWarning = ( exploreState: { featureFlagState }, } = viewContext; return { - isIn: !( - isDatasetAccessible(datasetsResponse) && - Boolean(featureFlagState?.includes(FEATURE_FLAGS.VERBATIM)) - ), + isIn: + !isDatasetAccessible(datasetsResponse) || + Boolean(!featureFlagState?.includes(FEATURE_FLAGS.VERBATIM)), }; }; /** - * Renders entity related download manifest when the given datasests response is accessible. + * Renders dataset export-related components (either the choose export method component, + * or specific export components) when the given dataset is accessble. * @param datasetsResponse - Response model return from datasets API. * @returns model to be used as props for the ConditionalComponent component. */ -export const renderManifestDownloadEntity = ( +export const renderDatasetExport = ( datasetsResponse: DatasetsResponse ): React.ComponentProps => { return { @@ -1363,11 +1524,12 @@ export const renderManifestDownloadEntity = ( }; /** - * Renders entity related download manifest warning when the given datasests response is not accessible. + * Renders dataset export-related warning components (either the choose export method component, + * or specific export components) when the given dataset is not accessible. * @param datasetsResponse - Response model return from datasets API. * @returns model to be used as props for the ConditionalComponent component. */ -export const renderManifestDownloadEntityWarning = ( +export const renderDatasetExportWarning = ( datasetsResponse: DatasetsResponse ): React.ComponentProps => { return { diff --git a/e2e/anvil/anvil-backpages.spec.ts b/e2e/anvil/anvil-backpages.spec.ts deleted file mode 100644 index 7f5bbbc5b..000000000 --- a/e2e/anvil/anvil-backpages.spec.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { test } from "@playwright/test"; -import { - testBackpageAccess, - testBackpageDetails, - testExportBackpage, -} from "../testFunctions"; -import { ANVIL_TABS } from "./anvil-tabs"; - -//TODO: this should probably check a feature flag instead -//eslint-disable-next-line sonarjs/no-skipped-tests -- disabled since Export to Terra is disabled on AnVIL -test.skip("Smoke test `Export to Terra` button on the first available dataset", async ({ - context, - page, -}) => { - const testResult = await testExportBackpage( - context, - page, - ANVIL_TABS.DATASETS - ); - if (!testResult) { - test.fail(); - } -}); - -//TODO: this should probably check a feature flag instead -//eslint-disable-next-line sonarjs/no-skipped-tests -- disabled since Export to Terra is disabled on AnVIL -test.skip("Check access controls on the datasets backpages work for the first two tabs", async ({ - page, -}) => { - const testResult = await testBackpageAccess(page, ANVIL_TABS.DATASETS); - if (!testResult) { - test.fail(); - } -}); - -test("Check that information on the backpages matches information in the data tables", async ({ - page, -}) => { - const testResult = await testBackpageDetails(page, ANVIL_TABS.DATASETS); - if (!testResult) { - test.fail(); - } -}); diff --git a/e2e/anvil/anvil-dataset.spec.ts b/e2e/anvil/anvil-dataset.spec.ts new file mode 100644 index 000000000..7b087e55e --- /dev/null +++ b/e2e/anvil/anvil-dataset.spec.ts @@ -0,0 +1,225 @@ +import { expect, Locator, Page, Request, test } from "@playwright/test"; +import { + BUTTON_TEXT_ANALYZE_IN_TERRA, + BUTTON_TEXT_EXPORT, + BUTTON_TEXT_REQUEST_ACCESS, + BUTTON_TEXT_REQUEST_FILE_MANIFEST, + CHIP_TEXT_ACCESS_GRANTED, + CHIP_TEXT_ACCESS_REQUIRED, + DatasetAccess, +} from "./common/constants"; +import { + MUI_ALERT_ROOT, + MUI_BUTTON_GROUP_ROOT, + MUI_TABLE_CELL_ROOT, + MUI_TABLE_ROOT, + MUI_TABLE_ROW_ROOT, +} from "../features/common/constants"; +import { ROUTE_MANIFEST_DOWNLOAD } from "../../site-config/anvil-cmg/dev/export/constants"; +import { ANVIL_CMG_CATEGORY_KEY } from "../../site-config/anvil-cmg/category"; + +const { describe } = test; + +const API_ENDPOINT_SUMMARY = "summary"; + +describe("Dataset", () => { + test.beforeEach(async ({ page }) => { + await goToDatasetsList(page); + }); + + test("displays request access button", async ({ page }) => { + await goToDataset(page, CHIP_TEXT_ACCESS_REQUIRED); + + // Confirm request access button is visible. + const exportButton = getLinkWithText(page, BUTTON_TEXT_REQUEST_ACCESS); + await expect(exportButton).toBeVisible(); + }); + + test("displays export button", async ({ page }) => { + await goToDataset(page, CHIP_TEXT_ACCESS_GRANTED); + + // Confirm export button is visible. + const exportButton = getLinkWithText(page, BUTTON_TEXT_EXPORT); + await expect(exportButton).toBeVisible(); + }); + + test("displays login to export method", async ({ page }) => { + await goToDataset(page, CHIP_TEXT_ACCESS_REQUIRED); + + // Navigate to the choose export method page. + const currentUrl = page.url(); + await page.goto(`${currentUrl}/export`); + + // Confirm the login alert is displayed. + await expect( + page.locator( + `${MUI_ALERT_ROOT}:has-text("To export this dataset, please sign in and, if necessary, request access.")` + ) + ).toBeVisible(); + }); + + test("displays export method", async ({ page }) => { + await goToDataset(page, CHIP_TEXT_ACCESS_GRANTED); + + // Confirm export button is visible and click it. + await clickLink(page, BUTTON_TEXT_EXPORT); + + // Confim Terra and file manifest export methods are listed. + await expect( + getLinkWithText(page, BUTTON_TEXT_ANALYZE_IN_TERRA) + ).toBeVisible(); + await expect( + getLinkWithText(page, BUTTON_TEXT_REQUEST_FILE_MANIFEST) + ).toBeVisible(); + }); + + test("displays export method selected data", async ({ page }) => { + await goToDataset(page, CHIP_TEXT_ACCESS_GRANTED); + + // Wait for the summary request once the export button is clicked. + const [request] = await Promise.all([ + page.waitForRequest((request) => + request.url().includes(API_ENDPOINT_SUMMARY) + ), + clickLink(page, BUTTON_TEXT_EXPORT), + ]); + + // Confirm summary request has dataset ID request param. + verifySummaryRequest(request); + }); + + test("displays login to download file manifest", async ({ page }) => { + await goToDataset(page, CHIP_TEXT_ACCESS_REQUIRED); + + // Navigate to the export file manifest page. + const currentUrl = page.url(); + await page.goto(`${currentUrl}${ROUTE_MANIFEST_DOWNLOAD}`); + + // Confirm the login alert is displayed. + await expect( + page.locator( + `${MUI_ALERT_ROOT}:has-text("To download this dataset manifest, please sign in and, if necessary, request access.")` + ) + ).toBeVisible(); + }); + + test("displays download file manifest", async ({ page }) => { + await goToDataset(page, CHIP_TEXT_ACCESS_GRANTED); + + // Confirm export button is visible and click it. + await clickLink(page, BUTTON_TEXT_EXPORT); + + // Confirm file manifest export method is visible and click it. + await clickLink(page, BUTTON_TEXT_REQUEST_FILE_MANIFEST); + + // Confirm the file manifest page is loaded: check there are two buttons + // (one for download, one for copy to clipboard). + const buttons = page.locator(`${MUI_BUTTON_GROUP_ROOT} button`); + + // Ensure there are exactly two buttons. + await expect(buttons).toHaveCount(2); + + // Ensure both buttons are visible. + await expect(buttons.nth(0)).toBeVisible(); + await expect(buttons.nth(1)).toBeVisible(); + }); + + test("displays download file manifest selected data", async ({ page }) => { + await goToDataset(page, CHIP_TEXT_ACCESS_GRANTED); + + // Confirm export button is visible and click it. + await clickLink(page, BUTTON_TEXT_EXPORT); + + // Wait for the summary request once the file manifest button is clicked. + const [request] = await Promise.all([ + page.waitForRequest((request) => + request.url().includes(API_ENDPOINT_SUMMARY) + ), + clickLink(page, BUTTON_TEXT_REQUEST_FILE_MANIFEST), + ]); + + // Confirm summary request has dataset ID request param. + verifySummaryRequest(request); + }); + + test("displays analyze in Terra", async ({ page }) => { + await goToDataset(page, CHIP_TEXT_ACCESS_GRANTED); + + // Confirm export button is visible and click it. + await clickLink(page, BUTTON_TEXT_EXPORT); + + // Confirm Terra export method is visible and click it. + await clickLink(page, BUTTON_TEXT_ANALYZE_IN_TERRA); + + // Confirm the analyze in Terra page is loaded: check the "coming soon" + // message is displayed. + await expect( + page.locator(`${MUI_ALERT_ROOT}:has-text("under development")`) + ).toBeVisible(); + }); +}); + +/** + * Click the link wit the given text. + * @param page - Playwright page object. + * @param buttonText - The text of the button to click. + */ +async function clickLink(page: Page, buttonText: string): Promise { + await getLinkWithText(page, buttonText).click(); +} + +/** + * Return the link with the given text. + * @param page - Playwright page object. + * @param buttonText - The text of the button to find. + * @returns - Playwright locator object for the dataset export button + */ +function getLinkWithText(page: Page, buttonText: string): Locator { + return page.locator(`a:has-text("${buttonText}")`); +} + +/** + * Navigate to the datasets list. + * @param page - Playwright page object. + */ +async function goToDatasetsList(page: Page): Promise { + if (page.url() !== "/") { + await page.goto("/"); + } +} + +/** + * Select a dataset with the given access from the datasets list and navigate to it. + * @param page - Playwright page object. + * @param access - The access of the dataset, either "Granted" or "Required" + */ +async function goToDataset(page: Page, access: DatasetAccess): Promise { + // Find a dataset that user has access to. + const datasetRow = page + .locator( + `${MUI_TABLE_ROOT} ${MUI_TABLE_ROW_ROOT}:has(${MUI_TABLE_CELL_ROOT}:has-text("${access}"))` + ) + .first(); + await expect(datasetRow).toBeVisible(); // Confirm at least one dataset has been found. + const datasetLink = datasetRow.locator( + `${MUI_TABLE_CELL_ROOT}:first-child a` + ); + const datasetTitle = await datasetLink.innerText(); + await datasetLink.click(); + + // Wait for the dataset detail page to load (specifically the dataset title). + await page.waitForSelector(`h1:has-text("${datasetTitle}")`); +} + +/** + * Confirm dataset ID is in the /summary request URL. + * @param request - The request object. + */ +function verifySummaryRequest(request: Request): void { + // Grab the filters param from the request. + const url = new URL(request.url()); + const paramValue = url.searchParams.get("filters") || ""; + + // Validate dataset ID is in the filters query parameter. + expect(paramValue).toContain(ANVIL_CMG_CATEGORY_KEY.DATASET_ID); +} diff --git a/e2e/anvil/common/constants.tsx b/e2e/anvil/common/constants.tsx new file mode 100644 index 000000000..27b05a69b --- /dev/null +++ b/e2e/anvil/common/constants.tsx @@ -0,0 +1,10 @@ +export const CHIP_TEXT_ACCESS_GRANTED = "Granted"; +export const CHIP_TEXT_ACCESS_REQUIRED = "Required"; +export const BUTTON_TEXT_ANALYZE_IN_TERRA = "Analyze in Terra"; +export const BUTTON_TEXT_EXPORT = "Export"; +export const BUTTON_TEXT_REQUEST_ACCESS = "Request Access"; +export const BUTTON_TEXT_REQUEST_FILE_MANIFEST = "Request File Manifest"; + +export type DatasetAccess = + | typeof CHIP_TEXT_ACCESS_GRANTED + | typeof CHIP_TEXT_ACCESS_REQUIRED; diff --git a/e2e/features/common/constants.ts b/e2e/features/common/constants.ts new file mode 100644 index 000000000..bc57e390e --- /dev/null +++ b/e2e/features/common/constants.ts @@ -0,0 +1,5 @@ +export const MUI_ALERT_ROOT = ".MuiAlert-root"; +export const MUI_BUTTON_GROUP_ROOT = ".MuiButtonGroup-root"; +export const MUI_TABLE_CELL_ROOT = ".MuiTableCell-root"; +export const MUI_TABLE_ROOT = ".MuiTable-root"; +export const MUI_TABLE_ROW_ROOT = ".MuiTableRow-root"; diff --git a/e2e/testFunctions.ts b/e2e/testFunctions.ts index 2ec752b21..b165a6c66 100644 --- a/e2e/testFunctions.ts +++ b/e2e/testFunctions.ts @@ -1,9 +1,5 @@ -import { BrowserContext, expect, Locator, Page } from "@playwright/test"; -import { - BackpageHeader, - ColumnDescription, - TabDescription, -} from "./testInterfaces"; +import { expect, Locator, Page } from "@playwright/test"; +import { ColumnDescription, TabDescription } from "./testInterfaces"; /* eslint-disable sonarjs/no-duplicate-string -- ignoring duplicate strings here */ @@ -804,21 +800,6 @@ export async function testDeselectFiltersThroughSearchBar( } } -/** - * Get the first link to a backpage with specified backpage access - * @param page - a Playright page locator - * @param access - the string denoting the level of access desired - * @returns a Playwright locator object to the first link to a backpage with the specified access - */ -const getBackpageLinkLocatorByAccess = (page: Page, access: string): Locator => - page - .getByRole("row") - .filter({ has: page.getByRole("cell", { name: access }) }) - .first() - .getByRole("cell") - .first() - .getByRole("link"); - /** * Make an export request that leaves only the minimal number of checkboxes selected * @param page - a Playwright page object @@ -864,147 +845,6 @@ const makeMinimalExportRequest = async ( await exportRequestButtonLocator.click(); }; -/** - * Test the export process for the specified tab - * @param context - a Playwright browser context object - * @param page - a Playwright page object - * @param tab - the tab to test on - * @returns - true if the test passes, false if the test fails but does not fail an assertion - */ -export async function testExportBackpage( - context: BrowserContext, - page: Page, - tab: TabDescription -): Promise { - if ( - tab.backpageExportButtons === undefined || - tab.backpageAccessTags === undefined - ) { - // Fail if this test u on a tab without defined backpages - return false; - } - // Goto the specified tab - await page.goto(tab.url); - // Expect to find row with a granted status indicator - const grantedRowLocator = getBackpageLinkLocatorByAccess( - page, - tab.backpageAccessTags.grantedShortName - ); - await expect(grantedRowLocator).toBeVisible(); - // Click into the selected row - // dispatchEvent necessary because the table loading component sometimes interrupts a click event - await grantedRowLocator.dispatchEvent("click"); - await expect( - page.getByText(tab.backpageExportButtons.detailsName) - ).toBeVisible(); - // Click the "Export" tab - await page - .getByText(tab.backpageExportButtons.exportTabName, { - exact: true, - }) - .click(); - await expect( - page.getByText(tab.backpageExportButtons.requestLandingMessage) - ).toBeVisible(); - await expect(page).toHaveURL(tab.backpageExportButtons.exportUrlRegExp); - await expect(page.getByRole("checkbox").first()).toBeVisible(); - const exportRequestButtonLocator = page.getByRole("button", { - name: tab.backpageExportButtons.exportRequestButtonText, - }); - // Complete the export request form - await makeMinimalExportRequest(page, exportRequestButtonLocator); - await expect( - page.getByText(tab.backpageExportButtons.actionLandingMessage, { - exact: true, - }) - ).toBeVisible({ timeout: TIMEOUT_EXPORT_REQUEST }); - const exportActionButtonLocator = page.getByRole("button", { - name: tab.backpageExportButtons?.exportActionButtonText, - }); - await expect(exportActionButtonLocator).toBeEnabled(); - // Click the Export Action Button and await a new browser tab - const newPagePromise = context.waitForEvent("page"); - await exportActionButtonLocator.click(); - const newPage = await newPagePromise; - // Expect the new browser tab to display the new tab content - await expect( - newPage.getByText(tab.backpageExportButtons?.newTabMessage) - ).toBeVisible(); - return true; -} - -/** - * Test that export access is available on entries where access shows as available - * and is not on entries where access shows as unavailable - * @param page - a Playwright page objext - * @param tab - the tab object to test on - * @returns - true if the test passes, false if the test fails but does not fail an assertion - */ -export async function testBackpageAccess( - page: Page, - tab: TabDescription -): Promise { - if ( - tab.backpageExportButtons === undefined || - tab.backpageAccessTags === undefined - ) { - // Fail if this test is run on a tab without defined backpages - return false; - } - // Goto the specified tab - await page.goto(tab.url); - // Check that the first "Granted" tab has access granted - const grantedRowLocator = getBackpageLinkLocatorByAccess( - page, - tab.backpageAccessTags.grantedShortName - ); - await expect(grantedRowLocator).toBeVisible(); - // dispatchEvent necessary because the table loading component sometimes interrupts a click event - await grantedRowLocator.dispatchEvent("click"); - await expect( - page.getByText(tab.backpageExportButtons.detailsName) - ).toBeVisible(); - await expect( - page.getByText(tab.backpageAccessTags.grantedLongName) - ).toBeVisible(); - await page - .getByText(tab.backpageExportButtons.exportTabName, { - exact: true, - }) - .click(); - await expect(page).toHaveURL(tab.backpageExportButtons.exportUrlRegExp); - await expect(page.getByRole("checkbox").first()).toBeVisible(); - const requestLinkButtonLocator = page.getByRole("button", { - name: tab.backpageExportButtons.exportRequestButtonText, - }); - await expect(requestLinkButtonLocator).toBeEnabled(); - // Go back to the table page - await page.getByRole("link", { name: tab.tabName }).click(); - // Check that the first "Required" tab does not have access granted - const deniedRowLocator = getBackpageLinkLocatorByAccess( - page, - tab.backpageAccessTags.deniedShortName - ); - await expect(deniedRowLocator).toBeVisible(); - // dispatchEvent necessary because the table loading component sometimes interrupts a click event - await deniedRowLocator.dispatchEvent("click"); - await expect( - page.getByText(tab.backpageAccessTags.deniedLongName) - ).toBeVisible(); - await page - .getByText(tab.backpageExportButtons.exportTabName, { - exact: true, - }) - .click(); - await expect(page).toHaveURL(tab.backpageExportButtons.exportUrlRegExp); - await expect( - page.getByText(tab.backpageExportButtons.accessNotGrantedMessage, { - exact: true, - }) - ).toBeVisible(); - return true; -} - /** * Get the text from a cell by reading the tooltip if it appears to be an N-tag * cell or by reading the text if it does not appear to be @@ -1050,102 +890,6 @@ const hoverAndGetText = async ( return cellText.trim(); }; -const FOOTER_LINK_NAME = "Privacy"; - -/** - * Check that the details in the backpage sidebar match information in the data table - * @param page - a Playwright page object - * @param tab - the tab to test on - * @returns - true if the test passes, false if the test fails due to an issue with the tab configuration - */ -export async function testBackpageDetails( - page: Page, - tab: TabDescription -): Promise { - if ( - tab.backpageHeaders === undefined || - tab.backpageExportButtons === undefined - ) { - // If the tab is not set up with backpage info, fail the test - console.log( - "BACKPAGE DETAILS error: tab is not set up with backpage info, so test cannot continue" - ); - return false; - } - await page.goto(tab.url); - // Enable test columns - await testSelectableColumns(page, tab); - const headers: { header: string; value: string }[] = []; - const preselectedColumnObjectArray = Array.from( - Object.values(tab.preselectedColumns) - ); - const selectableColumnObjectArray = Array.from( - Object.values(tab.selectableColumns) - ); - const combinedColumns = preselectedColumnObjectArray.concat( - selectableColumnObjectArray - ); - const filterString = (x: string | undefined): x is string => x !== undefined; - // Get the columns that correspond with a header on the backpage details - const backpageCorrespondingColumns: string[] = tab.backpageHeaders - .map((header) => header?.correspondingColumn?.name) - .filter(filterString) - .map((x) => x.trim()); - for (let i = 0; i < combinedColumns.length; i++) { - // Get the name of the current column - const columnHeaderName = ( - await page.getByRole("columnheader").nth(i).innerText() - ).trim(); - // If the selected column has an entry on the backpage - if (backpageCorrespondingColumns.includes(columnHeaderName)) { - // Get the object representing the current column - const columnObject = combinedColumns.find( - (x) => x.name == columnHeaderName - ); - // Get the entry text - const nonOverlappingElement = page.getByRole("link", { - name: FOOTER_LINK_NAME, - }); - const tableEntryText = await hoverAndGetText( - page, - columnObject, - 0, - i, - nonOverlappingElement - ); - // Get the name of the corresponding header on the backpage - const correspondingHeaderName = tab.backpageHeaders.find( - (header: BackpageHeader) => - header?.correspondingColumn?.name === columnHeaderName - )?.name; - if (correspondingHeaderName === undefined) { - // Fail the test, because this means there is an incorrect configuration in the tab definition - console.log( - "BACKPAGE DETAILS error: backpageHeaders is configured incorrectly, so test cannot continue" - ); - return false; - } - headers.push({ header: correspondingHeaderName, value: tableEntryText }); - } - } - // Go to the backpage - await getMthRowNthColumnCellLocator(page, 0, 0).click(); - // Expect the details name to be visible - await expect( - page.getByText(tab.backpageExportButtons.detailsName) - ).toBeVisible(); - for (const headerValue of headers) { - // Expect the correct value to be below the correct header in the dataset values table - await expect( - page - .locator(`:below(:text('${headerValue.header}'))`) - .getByText(headerValue.value) - .first() - ).toBeVisible(); - } - return true; -} - /** * Test that the Bulk Download index export workflow can be initiated and results in a download on the specified tab * @param page - a Playwright page object diff --git a/package-lock.json b/package-lock.json index eaea71384..5324fe859 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "explorer", "version": "2.7.1", "dependencies": { - "@databiosphere/findable-ui": "21.0.0", + "@databiosphere/findable-ui": "21.1.0", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@mdx-js/loader": "^3.0.1", @@ -2358,9 +2358,9 @@ } }, "node_modules/@databiosphere/findable-ui": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@databiosphere/findable-ui/-/findable-ui-21.0.0.tgz", - "integrity": "sha512-uYDLbNEBWTkgW5bS7YsRRFKCJusGhaG0OosPHBL5ZEZwchsXPuVaV6himlnuHcsgVZmV4ur5lPA0pyjOWAIJ5g==", + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@databiosphere/findable-ui/-/findable-ui-21.1.0.tgz", + "integrity": "sha512-vV38Y5BDCcxXvTi2KiGlDBQQ8+elLRrKinKBZ8zHEkmz7n04Lr+JD5Mci7+gkYDOxPvjj6ykcf5tlq+7n0Sm7Q==", "engines": { "node": "20.10.0" }, @@ -26125,9 +26125,9 @@ } }, "@databiosphere/findable-ui": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@databiosphere/findable-ui/-/findable-ui-21.0.0.tgz", - "integrity": "sha512-uYDLbNEBWTkgW5bS7YsRRFKCJusGhaG0OosPHBL5ZEZwchsXPuVaV6himlnuHcsgVZmV4ur5lPA0pyjOWAIJ5g==", + "version": "21.1.0", + "resolved": "https://registry.npmjs.org/@databiosphere/findable-ui/-/findable-ui-21.1.0.tgz", + "integrity": "sha512-vV38Y5BDCcxXvTi2KiGlDBQQ8+elLRrKinKBZ8zHEkmz7n04Lr+JD5Mci7+gkYDOxPvjj6ykcf5tlq+7n0Sm7Q==", "requires": {} }, "@digitak/esrun": { diff --git a/package.json b/package.json index 030b2e5e3..577a1f21d 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "check-system-status:anvil-cmg": "esrun e2e/anvil/anvil-check-system-status.ts" }, "dependencies": { - "@databiosphere/findable-ui": "21.0.0", + "@databiosphere/findable-ui": "21.1.0", "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@mdx-js/loader": "^3.0.1", diff --git a/pages/[entityListType]/[...params].tsx b/pages/[entityListType]/[...params].tsx index f58b8bf58..311bd5518 100644 --- a/pages/[entityListType]/[...params].tsx +++ b/pages/[entityListType]/[...params].tsx @@ -5,6 +5,7 @@ import { AzulListParams, } from "@databiosphere/findable-ui/lib/apis/azul/common/entities"; import { + PARAMS_INDEX_EXPORT_METHOD, PARAMS_INDEX_TAB, PARAMS_INDEX_UUID, } from "@databiosphere/findable-ui/lib/common/constants"; @@ -19,11 +20,14 @@ import { getEntityService } from "@databiosphere/findable-ui/lib/hooks/useEntity import { EXPLORE_MODE } from "@databiosphere/findable-ui/lib/hooks/useExploreMode"; import { database } from "@databiosphere/findable-ui/lib/utils/database"; import { EntityDetailView } from "@databiosphere/findable-ui/lib/views/EntityDetailView/entityDetailView"; +import { EntityExportView } from "@databiosphere/findable-ui/lib/views/EntityExportView/entityExportView"; +import { EntityExportMethodView } from "@databiosphere/findable-ui/lib/views/EntityExportMethodView/entityExportMethodView"; import { config } from "app/config/config"; import { GetStaticPaths, GetStaticProps, GetStaticPropsContext } from "next"; import { ParsedUrlQuery } from "querystring"; import { EntityGuard } from "../../app/components/Detail/components/EntityGuard/entityGuard"; import { readFile } from "../../app/utils/tsvParser"; +import { useRouter } from "next/router"; const setOfProcessedIds = new Set(); @@ -48,8 +52,11 @@ export interface EntityDetailPageProps extends AzulEntityStaticResponse { * @returns Entity detail view component. */ const EntityDetailPage = (props: EntityDetailPageProps): JSX.Element => { + const { query } = useRouter(); if (!props.entityListType) return <>; if (props.override) return ; + if (isChooseExportView(query)) return ; + if (isExportMethodView(query)) return ; return ; }; @@ -83,6 +90,38 @@ function isOverride(override: Override): boolean { ); } +/** + * Check if the current view matches the expected export path segment. + * @param query - The current query object. + * @param expectedIndex - The expected index for the parameter length check. + * @returns True if the view matches the expected export criteria. + */ +function isExportView(query: ParsedUrlQuery, expectedIndex: number): boolean { + return ( + !!query.params && + query.params.length === expectedIndex + 1 && + query.params[PARAMS_INDEX_TAB] === "export" + ); +} + +/** + * Determine if the current view is choose export method. + * @param query - The current query object. + * @returns True if the current view is choose export. + */ +function isChooseExportView(query: ParsedUrlQuery): boolean { + return isExportView(query, PARAMS_INDEX_TAB); +} + +/** + * Determine if the current view is an export method (e.g. file manifest or Terra). + * @param query - The current query object. + * @returns True if the current view is an export method. + */ +function isExportMethodView(query: ParsedUrlQuery): boolean { + return isExportView(query, PARAMS_INDEX_EXPORT_METHOD); +} + /** * Seed database. * @param entityListType - Entity list type. @@ -343,7 +382,7 @@ function processEntityPaths( entitiesResponse: AzulEntitiesResponse, paths: StaticPath[] ): void { - const { detail, route: entityListType } = entityConfig; + const { detail, export: exportConfig, route: entityListType } = entityConfig; const { tabs } = detail; const { hits: entities } = entitiesResponse; const tabRoutes = getTabRoutes(tabs); @@ -363,6 +402,26 @@ function processEntityPaths( }, }); } + // Generate paths for exports: add paths for choose method and each individual + // export method. + if (!exportConfig) continue; + for (const exportRoute of [ + ...exportConfig.tabs, + ...exportConfig.exportMethods, + ]) { + // Split export routes into individual paths (e.g. /export/download-manifest), + // removing any slashes. Default to empty array if no route is provided. + const exportRoutePaths = + exportRoute?.route?.split("/").filter(Boolean) || []; + // Build and add paths for each export route. + const params = [entityId, ...exportRoutePaths]; + paths.push({ + params: { + entityListType, + params, + }, + }); + } } } diff --git a/site-config/anvil-cmg/dev/detail/dataset/export/export.ts b/site-config/anvil-cmg/dev/detail/dataset/export/export.ts new file mode 100644 index 000000000..01e22f31f --- /dev/null +++ b/site-config/anvil-cmg/dev/detail/dataset/export/export.ts @@ -0,0 +1,195 @@ +import { + ComponentConfig, + ExportConfig, +} from "@databiosphere/findable-ui/lib/config/entities"; +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 * as MDX from "../../../../../../app/components/common/MDXContent/anvil-cmg"; + +/** + * Badge indicating dataset accessibility. + */ +const DATASET_ACCESSIBILITY_BADGE = { + component: C.AccessibilityBadge, + viewBuilder: V.buildDatasetAccessibilityBadge, +} as ComponentConfig; + +/** + * Dataset export configuration. + */ +export const exportConfig: ExportConfig = { + exportMethods: [ + { + mainColumn: [ + /* --------- */ + /* Dataset is not accessible; render warning */ + /* --------- */ + { + children: [ + { + children: [ + { + component: MDX.Alert, + viewBuilder: V.buildAlertDatasetTerraExportWarning, + } as ComponentConfig, + ], + component: C.BackPageContentSingleColumn, + } as ComponentConfig, + ], + component: C.ConditionalComponent, + viewBuilder: V.renderDatasetTerraExportWarning, + } as ComponentConfig, + /* ------ */ + /* Dataset is accessible; render Terra export method */ + /* ------ */ + { + children: [ + { + children: [ + { + component: C.ExportToTerra, + viewBuilder: V.builDatasetTerraExport, + } as ComponentConfig, + ], + component: C.BackPageContentMainColumn, + } as ComponentConfig, + /* sideColumn */ + ...exportSideColumn, + ], + component: C.ConditionalComponent, + viewBuilder: V.renderDatasetTerraExport, + } as ComponentConfig, + ], + route: ROUTE_EXPORT_TO_TERRA, + top: [ + { + children: [DATASET_ACCESSIBILITY_BADGE], + component: C.BackPageHero, + viewBuilder: V.buildDatasetExportMethodHeroTerraExport, + } as ComponentConfig, + ], + }, + { + mainColumn: [ + /* --------- */ + /* Dataset is not accessible; render warning */ + /* --------- */ + { + children: [ + { + children: [ + { + component: MDX.Alert, + viewBuilder: V.buildAlertDatasetManifestDownloadWarning, + } as ComponentConfig, + ], + component: C.BackPageContentSingleColumn, + } as ComponentConfig, + ], + component: C.ConditionalComponent, + viewBuilder: V.renderDatasetExportWarning, + } as ComponentConfig, + /* ------ */ + /* Dataset is accessible; render file manifest method */ + /* ------ */ + { + children: [ + { + children: [ + { + component: C.AnVILManifestDownloadEntity, + viewBuilder: V.buildDatasetExportPropsWithFilter, + } as ComponentConfig< + typeof C.AnVILManifestDownloadEntity, + DatasetsResponse + >, + ], + component: C.BackPageContentMainColumn, + } as ComponentConfig, + /* sideColumn */ + ...exportSideColumn, + ], + component: C.ConditionalComponent, + viewBuilder: V.renderDatasetExport, + } as ComponentConfig, + ], + route: ROUTE_MANIFEST_DOWNLOAD, + top: [ + { + children: [DATASET_ACCESSIBILITY_BADGE], + component: C.BackPageHero, + viewBuilder: V.buildDatasetExportMethodHeroManifestDownload, + } as ComponentConfig, + ], + }, + ], + staticLoad: true, // TODO this matches the cohort export config, but is it necessary? + tabs: [ + { + label: "Choose Export Method", + 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 export entity */ + /* ------ */ + { + children: [ + { + children: [ + // Empty component for triggering calls to populate the selected data in the side channel. + { + component: C.AnVILExportEntity, + viewBuilder: V.buildDatasetExportPropsWithFilter, + } as ComponentConfig, + { + component: C.ExportMethod, + viewBuilder: V.buildDatasetExportMethodTerra, + } as ComponentConfig, + { + component: C.ExportMethod, + viewBuilder: V.buildDatasetExportMethodManifestDownload, + } as ComponentConfig, + ], + component: C.BackPageContentMainColumn, + } as ComponentConfig, + /* sideColumn */ + ...exportSideColumn, + ], + component: C.ConditionalComponent, + viewBuilder: V.renderDatasetExport, + } as ComponentConfig, + ], + route: "/export", + }, + ], + top: [ + { + children: [DATASET_ACCESSIBILITY_BADGE], + component: C.BackPageHero, + viewBuilder: V.buildDatasetExportHero, + } as ComponentConfig, + ], +}; diff --git a/site-config/anvil-cmg/dev/detail/dataset/exportMainColumn.ts b/site-config/anvil-cmg/dev/detail/dataset/exportMainColumn.ts deleted file mode 100644 index b7ab9aee4..000000000 --- a/site-config/anvil-cmg/dev/detail/dataset/exportMainColumn.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ComponentConfig } from "@databiosphere/findable-ui/lib/config/entities"; -import { DatasetsResponse } from "../../../../../app/apis/azul/anvil-cmg/common/responses"; -import * as C from "../../../../../app/components"; -import * as MDX from "../../../../../app/components/common/MDXContent/anvil-cmg"; -import * as V from "../../../../../app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders"; -import { sideColumn } from "../../export/exportSideColumn"; - -export const mainColumn: ComponentConfig[] = [ - { - /* Dataset is not accessible; render warning */ - children: [ - { - children: [ - { - component: MDX.Alert, - viewBuilder: V.buildAlertExportEntityWarning, - } as ComponentConfig, - ], - component: C.BackPageContentSingleColumn, - } as ComponentConfig, - ], - component: C.ConditionalComponent, - viewBuilder: V.renderExportEntityWarning, - } as ComponentConfig, - { - /* Dataset is accessible; render export entity */ - children: [ - /* mainColumn */ - { - children: [ - { - component: C.ExportToTerra, - viewBuilder: V.buildExportEntityToTerra, - } as ComponentConfig, - ], - component: C.BackPageContentMainColumn, - } as ComponentConfig, - /* sideColumn */ - ...sideColumn, - ], - component: C.ConditionalComponent, - viewBuilder: V.renderExportEntity, - } as ComponentConfig, -]; diff --git a/site-config/anvil-cmg/dev/detail/dataset/metadataMainColumn.ts b/site-config/anvil-cmg/dev/detail/dataset/metadataMainColumn.ts deleted file mode 100644 index 2168418cb..000000000 --- a/site-config/anvil-cmg/dev/detail/dataset/metadataMainColumn.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ComponentConfig } from "@databiosphere/findable-ui/lib/config/entities"; -import { DatasetsResponse } from "../../../../../app/apis/azul/anvil-cmg/common/responses"; -import * as C from "../../../../../app/components"; -import * as MDX from "../../../../../app/components/common/MDXContent/anvil-cmg"; -import * as V from "../../../../../app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders"; -import { sideColumn } from "../../export/exportSideColumn"; - -export const mainColumn: ComponentConfig[] = [ - { - /* Dataset is not accessible; render warning */ - children: [ - { - children: [ - { - component: MDX.Alert, - viewBuilder: V.buildAlertManifestDownloadEntityWarning, - } as ComponentConfig, - ], - component: C.BackPageContentSingleColumn, - } as ComponentConfig, - ], - component: C.ConditionalComponent, - viewBuilder: V.renderManifestDownloadEntityWarning, - } as ComponentConfig, - { - /* Dataset is accessible; render export entity */ - children: [ - /* mainColumn */ - { - children: [ - { - component: C.AnVILManifestDownloadEntity, - viewBuilder: V.buildManifestDownloadEntity, - } as ComponentConfig< - typeof C.AnVILManifestDownloadEntity, - DatasetsResponse - >, - ], - component: C.BackPageContentMainColumn, - } as ComponentConfig, - /* sideColumn */ - ...sideColumn, - ], - component: C.ConditionalComponent, - viewBuilder: V.renderManifestDownloadEntity, - } as ComponentConfig, -]; diff --git a/site-config/anvil-cmg/dev/index/datasetsEntityConfig.ts b/site-config/anvil-cmg/dev/index/datasetsEntityConfig.ts index cfbeb8f89..a4ef47095 100644 --- a/site-config/anvil-cmg/dev/index/datasetsEntityConfig.ts +++ b/site-config/anvil-cmg/dev/index/datasetsEntityConfig.ts @@ -14,13 +14,12 @@ import { ANVIL_CMG_CATEGORY_KEY, ANVIL_CMG_CATEGORY_LABEL, } from "../../category"; -import { mainColumn as exportMainColumn } from "../detail/dataset/exportMainColumn"; -import { mainColumn as metadataMainColumn } from "../detail/dataset/metadataMainColumn"; import { mainColumn } from "../detail/dataset/overviewMainColumn"; import { sideColumn } from "../detail/dataset/overviewSideColumn"; import { top } from "../detail/dataset/top"; import { listHero } from "../listView/datasetsListHero"; import { subTitleHero } from "../listView/subTitleHero"; +import { exportConfig } from "../detail/dataset/export/export"; /** * Entity config object responsible for config related to the /datasets route. @@ -37,22 +36,14 @@ export const datasetsEntityConfig: EntityConfig = { route: "", sideColumn: sideColumn, }, - { - label: "Metadata", - mainColumn: metadataMainColumn, - route: "project-metadata", - }, - { - label: "Export", - mainColumn: exportMainColumn, - route: "export-to-terra", - }, ], top: top, }, exploreMode: EXPLORE_MODE.SS_FETCH_SS_FILTERING, + export: exportConfig, getId: getDatasetEntryId, getTitle: getTitle, + hideTabs: true, label: "Datasets", list: { columns: [