Skip to content

Commit 71a71f7

Browse files
feat: enable curl download for anvil-cmg cohort export (#4727)
* feat: enable curl download for anvil-cmg cohort export #4692 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add NRES consent group check for dataset curl download #4692 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: update curl download text for open-access data #4692 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address Copilot review - rename config and use ROUTES constant #4692 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 6f6ada2 commit 71a71f7

8 files changed

Lines changed: 182 additions & 16 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
### Download via curl
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
### Download via curl
22

3-
<TagWarning>Please note</TagWarning> This download includes only files available
4-
through the AWS Open Data Program. Files hosted exclusively on Google Cloud are
5-
not included in this command.
3+
<TagWarning>Please note</TagWarning> Only the open-access portion (consent group
4+
NRES) of the selected data will be included in the download.

app/components/common/MDXContent/anvil-cmg/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export { default as AlertExportWarning } from "./alertExportWarning.mdx";
77
export { default as AlertExportWarningContent } from "./alertExportWarningContent.mdx";
88
export { default as AlertLoginReminder } from "./alertLoginReminder.mdx";
99
export { default as DataReleasePolicy } from "./dataReleasePolicy.mdx";
10+
export { default as DownloadCurlCommandDatasetStart } from "./downloadCurlCommandDatasetStart.mdx";
1011
export { default as DownloadCurlCommandStart } from "./downloadCurlCommandStart.mdx";
1112
export { default as DownloadCurlCommandSuccess } from "./downloadCurlCommandSuccess.mdx";
1213
export { default as LoginTermsOfService } from "./loginTermsOfService.mdx";

app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -508,9 +508,9 @@ export const buildDatasetExportMethodCurlCommand = (
508508
const datasetPath = buildDatasetPath(datasetsResponse);
509509
return {
510510
buttonLabel: "Request curl Command",
511-
description: "Obtain a curl command for downloading the selected data.",
511+
description: "Obtain a curl command for downloading the dataset.",
512512
route: `${datasetPath}${ROUTES.CURL_DOWNLOAD}`,
513-
title: "Download Study Data and Metadata (curl Command)",
513+
title: "Download Open-Access Data and Metadata (curl Command)",
514514
};
515515
};
516516

@@ -531,7 +531,7 @@ export const buildDatasetDownloadCurlCommand = (
531531
const formFacet = getFormFacets(fileManifestState);
532532
return {
533533
DownloadCurlForm: C.DownloadCurlCommandForm,
534-
DownloadCurlStart: MDX.DownloadCurlCommandStart,
534+
DownloadCurlStart: MDX.DownloadCurlCommandDatasetStart,
535535
DownloadCurlSuccess: MDX.DownloadCurlCommandSuccess,
536536
fileManifestState,
537537
fileSummaryFacetName: ANVIL_CMG_CATEGORY_KEY.FILE_FILE_FORMAT,
@@ -888,6 +888,70 @@ export const buildExportMethodTerra = (
888888
};
889889
};
890890

891+
/**
892+
* Build props for DownloadCurlCommand component.
893+
* @param _ - Unused.
894+
* @param viewContext - View context.
895+
* @returns model to be used as props for the DownloadCurlCommand component.
896+
*/
897+
export const buildDownloadCurlCommand = (
898+
_: unknown,
899+
viewContext: ViewContext<unknown>
900+
): React.ComponentProps<typeof C.DownloadCurlCommand> => {
901+
const {
902+
exploreState: { filterState },
903+
fileManifestState,
904+
} = viewContext;
905+
const formFacet = getFormFacets(fileManifestState);
906+
return {
907+
DownloadCurlForm: C.DownloadCurlCommandForm,
908+
DownloadCurlStart: MDX.DownloadCurlCommandStart,
909+
DownloadCurlSuccess: MDX.DownloadCurlCommandSuccess,
910+
fileManifestState,
911+
fileSummaryFacetName: ANVIL_CMG_CATEGORY_KEY.FILE_FILE_FORMAT,
912+
filters: filterState,
913+
formFacet,
914+
speciesFacetName: ANVIL_CMG_CATEGORY_KEY.DONOR_ORGANISM_TYPE,
915+
};
916+
};
917+
918+
/**
919+
* Build props for ExportMethod component for display of the bulk download section.
920+
* @param _ - Unused.
921+
* @param viewContext - View context.
922+
* @returns model to be used as props for the ExportMethod component.
923+
*/
924+
export const buildExportMethodBulkDownload = (
925+
_: unknown,
926+
viewContext: ViewContext<unknown>
927+
): React.ComponentProps<typeof C.ExportMethod> => {
928+
return {
929+
...getExportMethodAccessibility(viewContext),
930+
buttonLabel: "Request curl Command",
931+
description:
932+
"Obtain a curl command for downloading the open-access portion of the selected data.",
933+
route: ROUTES.CURL_DOWNLOAD,
934+
title: "Download Open-Access Data and Metadata (curl Command)",
935+
};
936+
};
937+
938+
/**
939+
* Build props for download curl command BackPageHero component.
940+
* @param _ - Unused.
941+
* @param viewContext - View context.
942+
* @returns model to be used as props for the BackPageHero component.
943+
*/
944+
export const buildExportMethodHeroCurlCommand = (
945+
_: unknown,
946+
viewContext: ViewContext<unknown>
947+
): React.ComponentProps<typeof C.BackPageHero> => {
948+
const title = 'Download Selected Data Using "curl"';
949+
const {
950+
exploreState: { tabValue },
951+
} = viewContext;
952+
return getExportMethodHero(tabValue, title);
953+
};
954+
891955
/**
892956
* Build props for ExportSelectedDataSummary component.
893957
* @param _ - Unused.
@@ -1723,6 +1787,22 @@ export const renderWhenUnAuthenticated = (
17231787
};
17241788
};
17251789

1790+
/**
1791+
* Renders dataset curl download components when the given dataset is accessible
1792+
* and has NRES consent group.
1793+
* @param datasetsResponse - Response model return from datasets API.
1794+
* @returns model to be used as props for the ConditionalComponent component.
1795+
*/
1796+
export const renderDatasetCurlDownload = (
1797+
datasetsResponse: DatasetsResponse
1798+
): React.ComponentProps<typeof C.ConditionalComponent> => {
1799+
const consentGroups = getConsentGroup(datasetsResponse);
1800+
const isNRES = consentGroups.includes("NRES");
1801+
return {
1802+
isIn: isDatasetAccessible(datasetsResponse) && isNRES,
1803+
};
1804+
};
1805+
17261806
/**
17271807
* Renders dataset export-related components (either the choose export method component,
17281808
* or specific export components) when the given dataset is accessble.

pages/[entityListType]/[...params].tsx

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,13 @@ import { ParsedUrlQuery } from "querystring";
2929
import { EntityGuard } from "../../app/components/Detail/components/EntityGuard/entityGuard";
3030
import { readFile } from "../../app/utils/tsvParser";
3131
import { useRouter } from "next/router";
32+
import { useConfig } from "@databiosphere/findable-ui/lib/hooks/useConfig";
3233
import { useFeatureFlag } from "@databiosphere/findable-ui/lib/hooks/useFeatureFlag/useFeatureFlag";
3334
import { FEATURES } from "../../app/shared/entities";
3435
import NextError from "next/error";
3536
import { ROUTES } from "../../site-config/anvil-cmg/dev/export/routes";
37+
import { getConsentGroup } from "../../app/apis/azul/anvil-cmg/common/transformers";
38+
import { DatasetsResponse } from "../../app/apis/azul/anvil-cmg/common/responses";
3639

3740
const setOfProcessedIds = new Set<string>();
3841

@@ -65,15 +68,21 @@ export interface EntityDetailPageProps extends AzulEntityStaticResponse {
6568
* @returns Entity detail view component.
6669
*/
6770
const EntityDetailPage = (props: EntityDetailPageProps): JSX.Element => {
71+
const { config: siteConfig } = useConfig();
72+
const isAnVIL = siteConfig.appTitle?.includes("AnVIL");
6873
const isNCPIExportEnabled = useFeatureFlag(FEATURES.NCPI_EXPORT);
6974
const isCurlDownloadEnabled = useFeatureFlag(FEATURES.CURL_DOWNLOAD);
7075
const { query } = useRouter();
7176
if (!props.entityListType) return <></>;
7277
if (props.override) return <EntityGuard override={props.override} />;
7378
if (!isNCPIExportEnabled && isNCPIExportRoute(query))
7479
return <NextError statusCode={404} />;
75-
if (!isCurlDownloadEnabled && isCurlDownloadRoute(query))
76-
return <NextError statusCode={404} />;
80+
// Curl download requires feature flag AND NRES consent group (AnVIL only)
81+
if (isAnVIL && isCurlDownloadRoute(query)) {
82+
if (!isCurlDownloadEnabled || !isNRESDataset(props.data)) {
83+
return <NextError statusCode={404} />;
84+
}
85+
}
7786
if (isChooseExportView(query)) return <EntityExportView {...props} />;
7887
if (isExportMethodView(query)) return <EntityExportMethodView {...props} />;
7988
return <EntityDetailView {...props} />;
@@ -119,6 +128,17 @@ function isCurlDownloadRoute(query: ParsedUrlQuery): boolean {
119128
return lastParam === CURL_DOWNLOAD_PATH.replace("/export/", "");
120129
}
121130

131+
/**
132+
* Returns true if the dataset has NRES consent group.
133+
* @param data - Entity response data.
134+
* @returns True if the dataset has NRES consent group.
135+
*/
136+
function isNRESDataset(data: AzulEntityStaticResponse | undefined): boolean {
137+
if (!data) return false;
138+
const consentGroups = getConsentGroup(data as DatasetsResponse);
139+
return consentGroups.includes("NRES");
140+
}
141+
122142
/**
123143
* Returns true if the entity is a special case e.g. an "override".
124144
* @param override - Override.
@@ -256,6 +276,7 @@ export const getStaticProps: GetStaticProps<AzulEntityStaticResponse> = async ({
256276
const slug = (params as PageUrl).params;
257277
const entityConfig = getEntityConfig(entities, entityListType);
258278
const entityTab = getSlugPath(slug, PARAMS_INDEX_TAB);
279+
const entityExportMethod = getSlugPath(slug, PARAMS_INDEX_EXPORT_METHOD);
259280
const entityId = getSlugPath(slug, PARAMS_INDEX_UUID);
260281

261282
if (!entityConfig || !entityId) return { notFound: true };
@@ -268,7 +289,13 @@ export const getStaticProps: GetStaticProps<AzulEntityStaticResponse> = async ({
268289
if (props.override) return { props };
269290

270291
// Process entity props.
271-
await processEntityProps(entityConfig, entityTab, entityId, props);
292+
await processEntityProps(
293+
entityConfig,
294+
entityTab,
295+
entityExportMethod,
296+
entityId,
297+
props
298+
);
272299

273300
return {
274301
props,
@@ -472,12 +499,14 @@ function processEntityPaths(
472499
* Processes the entity props for the given entity page.
473500
* @param entityConfig - Entity config.
474501
* @param entityTab - Entity tab.
502+
* @param entityExportMethod - Entity export method.
475503
* @param entityId - Entity ID.
476504
* @param props - Entity detail page props.
477505
*/
478506
async function processEntityProps(
479507
entityConfig: EntityConfig,
480508
entityTab = "",
509+
entityExportMethod = "",
481510
entityId: string,
482511
props: EntityDetailPageProps
483512
): Promise<void> {
@@ -487,8 +516,16 @@ async function processEntityProps(
487516
} = entityConfig;
488517
// Early exit; return if the entity is not to be statically loaded.
489518
if (!staticLoad) return;
490-
// When the entity detail is to be fetched from API, we only do so for the first tab.
491-
if (exploreMode === EXPLORE_MODE.SS_FETCH_SS_FILTERING && entityTab) return;
519+
// When the entity detail is to be fetched from API, we only do so for the first tab,
520+
// unless it's the curl download route which needs data for NRES check.
521+
const isCurlDownload =
522+
entityExportMethod === CURL_DOWNLOAD_PATH.replace("/export/", "");
523+
if (
524+
exploreMode === EXPLORE_MODE.SS_FETCH_SS_FILTERING &&
525+
entityTab &&
526+
!isCurlDownload
527+
)
528+
return;
492529
if (exploreMode === EXPLORE_MODE.CS_FETCH_CS_FILTERING) {
493530
// Seed database.
494531
await seedDatabase(entityConfig.route, entityConfig);

pages/export/get-curl-command.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { JSX } from "react";
22
import { ExportMethodView } from "@databiosphere/findable-ui/lib/views/ExportMethodView/exportMethodView";
3+
import { useConfig } from "@databiosphere/findable-ui/lib/hooks/useConfig";
4+
import { useFeatureFlag } from "@databiosphere/findable-ui/lib/hooks/useFeatureFlag/useFeatureFlag";
35
import { GetStaticProps } from "next";
6+
import NextError from "next/error";
7+
import { FEATURES } from "../../app/shared/entities";
48

59
export const getStaticProps: GetStaticProps = async () => {
610
return {
@@ -15,6 +19,11 @@ export const getStaticProps: GetStaticProps = async () => {
1519
* @returns download curl command view component.
1620
*/
1721
const GetCurlCommandPage = (): JSX.Element => {
22+
const { config } = useConfig();
23+
const isAnVIL = config.appTitle?.includes("AnVIL");
24+
const isEnabled = useFeatureFlag(FEATURES.CURL_DOWNLOAD);
25+
// Feature flag only applies to AnVIL sites
26+
if (isAnVIL && !isEnabled) return <NextError statusCode={404} />;
1827
return <ExportMethodView />;
1928
};
2029

site-config/anvil-cmg/dev/detail/dataset/export/export.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const exportConfig: ExportConfig = {
4747
viewBuilder: V.renderDatasetExportWarning,
4848
} as ComponentConfig<typeof C.ConditionalComponent, DatasetsResponse>,
4949
/* ------ */
50-
/* Dataset is accessible; render curl download method */
50+
/* Dataset is accessible and NRES; render curl download method */
5151
/* ------ */
5252
{
5353
children: [
@@ -67,7 +67,7 @@ export const exportConfig: ExportConfig = {
6767
...exportSideColumn,
6868
],
6969
component: C.ConditionalComponent,
70-
viewBuilder: V.renderDatasetExport,
70+
viewBuilder: V.renderDatasetCurlDownload,
7171
} as ComponentConfig<typeof C.ConditionalComponent, DatasetsResponse>,
7272
],
7373
route: ROUTES.CURL_DOWNLOAD,
@@ -379,9 +379,18 @@ export const exportConfig: ExportConfig = {
379379
viewBuilder: V.buildDatasetExportPropsWithFilter,
380380
} as ComponentConfig<typeof C.AnVILExportEntity>,
381381
{
382-
component: CurlDownloadExportMethod,
383-
viewBuilder: V.buildDatasetExportMethodCurlCommand,
384-
} as ComponentConfig<typeof CurlDownloadExportMethod>,
382+
children: [
383+
{
384+
component: CurlDownloadExportMethod,
385+
viewBuilder: V.buildDatasetExportMethodCurlCommand,
386+
} as ComponentConfig<typeof CurlDownloadExportMethod>,
387+
],
388+
component: C.ConditionalComponent,
389+
viewBuilder: V.renderDatasetCurlDownload,
390+
} as ComponentConfig<
391+
typeof C.ConditionalComponent,
392+
DatasetsResponse
393+
>,
385394
{
386395
component: C.ExportMethod,
387396
viewBuilder: V.buildDatasetExportMethodTerra,

site-config/anvil-cmg/dev/export/export.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,36 @@ import { ROUTES } from "./routes";
88
import { mainColumn as exportMainColumn } from "./exportMainColumn";
99
import { sideColumn as exportSideColumn } from "./exportSideColumn";
1010
import { ExportMethod } from "../../../../app/components/Export/components/AnVILExplorer/platform/ExportMethod/exportMethod";
11+
import { CurlDownloadExportMethod } from "../../../../app/components/Export/components/AnVILExplorer/CurlDownload/curlDownloadExportMethod";
1112
import { EXPORT_METHODS, EXPORTS } from "./constants";
1213

1314
export const exportConfig: ExportConfig = {
1415
exportMethods: [
16+
{
17+
mainColumn: [
18+
/* mainColumn - top section - warning - some datasets are not available */
19+
...exportMainColumn,
20+
/* mainColumn */
21+
{
22+
children: [
23+
{
24+
component: C.DownloadCurlCommand,
25+
viewBuilder: V.buildDownloadCurlCommand,
26+
} as ComponentConfig<typeof C.DownloadCurlCommand>,
27+
],
28+
component: C.BackPageContentMainColumn,
29+
} as ComponentConfig<typeof C.BackPageContentMainColumn>,
30+
/* sideColumn */
31+
...exportSideColumn,
32+
],
33+
route: ROUTES.CURL_DOWNLOAD,
34+
top: [
35+
{
36+
component: C.BackPageHero,
37+
viewBuilder: V.buildExportMethodHeroCurlCommand,
38+
} as ComponentConfig<typeof C.BackPageHero>,
39+
],
40+
},
1541
{
1642
mainColumn: [
1743
/* mainColumn - top section - warning - some datasets are not available */
@@ -154,6 +180,10 @@ export const exportConfig: ExportConfig = {
154180
/* mainColumn */
155181
{
156182
children: [
183+
{
184+
component: CurlDownloadExportMethod,
185+
viewBuilder: V.buildExportMethodBulkDownload,
186+
} as ComponentConfig<typeof CurlDownloadExportMethod>,
157187
{
158188
component: C.ExportMethod,
159189
viewBuilder: V.buildExportMethodTerra,

0 commit comments

Comments
 (0)