Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: request access via duos in addition to dbgap (#4127) #4362

Merged
merged 1 commit into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/apis/azul/anvil-cmg/common/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export interface DatasetEntity {
consent_group: (string | null)[];
dataset_id: string;
description?: string;
duos_id: string | null;
registered_identifier: (string | null)[];
title: string;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {
ButtonProps,
ListItemTextProps,
MenuProps,
SvgIconProps,
} from "@mui/material";
import {
TEXT_BODY_500,
TEXT_BODY_SMALL_400_2_LINES,
} from "@databiosphere/findable-ui/lib/theme/common/typography";

export const BUTTON_PROPS: ButtonProps = {
color: "primary",
variant: "contained",
};

export const LIST_ITEM_TEXT_PROPS: ListItemTextProps = {
primaryTypographyProps: { variant: TEXT_BODY_500 },
secondaryTypographyProps: { variant: TEXT_BODY_SMALL_400_2_LINES },
};

export const MENU_PROPS: Partial<MenuProps> = {
variant: "menu",
};

export const SVG_ICON_PROPS: SvgIconProps = {
fontSize: "small",
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Button } from "@mui/material";
import styled from "@emotion/styled";
import { css } from "@emotion/react";
import { primaryDark } from "@databiosphere/findable-ui/lib/styles/common/mixins/colors";
import { DropdownMenu } from "@databiosphere/findable-ui/lib/components/common/DropdownMenu/dropdownMenu";

interface Props {
open?: boolean;
}

export const StyledDropdownMenu = styled(DropdownMenu)`
.MuiPaper-menu {
max-width: 324px;

.MuiListItemText-root {
display: grid;
gap: 4px;
white-space: normal;
}
}
`;

export const StyledButton = styled(Button, {
shouldForwardProp: (prop) => prop !== "open",
})<Props>`
padding-right: 8px;

.MuiButton-endIcon {
margin-left: -6px;
}

${(props) =>
props.open &&
css`
background-color: ${primaryDark(props)};
`}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Props } from "./types";
import { ListItemText, MenuItem } from "@mui/material";
import { Actions } from "@databiosphere/findable-ui/lib/components/Layout/components/BackPage/components/BackPageHero/components/Actions/actions";
import { CallToActionButton } from "@databiosphere/findable-ui/lib/components/common/Button/components/CallToActionButton/callToActionButton";
import ArrowDropDownRoundedIcon from "@mui/icons-material/ArrowDropDownRounded";
import { StyledButton, StyledDropdownMenu } from "./requestAccess.styles";
import {
ANCHOR_TARGET,
REL_ATTRIBUTE,
} from "@databiosphere/findable-ui/lib/components/Links/common/entities";
import {
BUTTON_PROPS,
LIST_ITEM_TEXT_PROPS,
MENU_PROPS,
SVG_ICON_PROPS,
} from "./constants";
import { getRequestAccessOptions } from "./utils";

export const RequestAccess = ({
datasetsResponse,
}: Props): JSX.Element | null => {
const options = getRequestAccessOptions(datasetsResponse);
// If there are no request access options, return null.
if (options.length === 0) return null;
// If there is only one request access option, render a CallToActionButton.
if (options.length === 1)
return (
<Actions>
<CallToActionButton
callToAction={{ label: "Request Access", url: options[0].href }}
/>
</Actions>
);
// Otherwise, render a dropdown menu for multiple request access options.
return (
<Actions>
<StyledDropdownMenu
{...MENU_PROPS}
button={(props) => (
<StyledButton
{...BUTTON_PROPS}
endIcon={<ArrowDropDownRoundedIcon {...SVG_ICON_PROPS} />}
{...props}
>
Request Access
</StyledButton>
)}
>
{({ closeMenu }): JSX.Element[] => [
...options.map(({ href, primary, secondary }, i) => (
<MenuItem
key={i}
component="a"
href={href}
rel={REL_ATTRIBUTE.NO_OPENER_NO_REFERRER}
target={ANCHOR_TARGET.BLANK}
onClick={closeMenu}
>
<ListItemText
{...LIST_ITEM_TEXT_PROPS}
primary={primary}
secondary={secondary}
/>
</MenuItem>
)),
]}
</StyledDropdownMenu>
</Actions>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { DatasetsResponse } from "../../../../../../apis/azul/anvil-cmg/common/responses";

export interface Props {
datasetsResponse: DatasetsResponse;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { DatasetsResponse } from "../../../../../../apis/azul/anvil-cmg/common/responses";
import { takeArrayValueAt } from "../../../../../../viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders";
import {
processEntityArrayValue,
processEntityValue,
} from "../../../../../../apis/azul/common/utils";
import { LABEL } from "@databiosphere/findable-ui/lib/apis/azul/common/entities";
import { ListItemTextProps } from "@mui/material";

/**
* Generates a list of request access menu options based on the provided dataset response.
* This function extracts identifiers (DUOS ID and dbGaP ID) from the datasets response and returns an array of menu option objects.
* Each menu option contains a link `href` and title `primary` and description text `secondary`, to be used in Material UI's `MenuItem` and `ListItemText` component.
* @param datasetsResponse - Response model return from datasets API.
* @returns menu option objects with `href`, `primary`, and `secondary` properties.
*/
export function getRequestAccessOptions(
datasetsResponse: DatasetsResponse
): (Pick<ListItemTextProps, "primary" | "secondary"> & { href: string })[] {
// Get the dbGaP ID and DUOS ID from the datasets response.
const dbGapId = takeArrayValueAt(
processEntityArrayValue(
datasetsResponse.datasets,
"registered_identifier",
LABEL.EMPTY
),
0
);
const duosId = processEntityValue(
datasetsResponse.datasets,
"duos_id",
LABEL.EMPTY
);
const options = [];
if (duosId) {
// If a DUOS ID is present, add a menu option for DUOS.
options.push({
href: `https://duos.org/dataset/${duosId}`,
primary: "DUOS",
secondary:
"Request access via DUOS, which streamlines data access for NHGRI-sponsored studies, both registered and unregistered in dbGaP.",
});
}
if (dbGapId) {
// If a dbGaP ID is present, add a menu option for dbGaP.
options.push({
href: `https://dbgap.ncbi.nlm.nih.gov/aa/wga.cgi?adddataset=${dbGapId}`,
primary: "dbGaP",
secondary:
"Request access via the dbGaP Authorized Access portal for studies registered in dbGaP, following the standard data access process.",
});
}
return options;
}
34 changes: 21 additions & 13 deletions app/viewModelBuilders/azul/anvil-cmg/common/viewModelBuilders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
ChipProps as MChipProps,
FadeProps as MFadeProps,
} from "@mui/material";
import React from "react";
import React, { ReactNode } from "react";
import {
ANVIL_CMG_CATEGORY_KEY,
ANVIL_CMG_CATEGORY_LABEL,
Expand Down Expand Up @@ -112,6 +112,7 @@ 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";
import { RequestAccess } from "../../../../components/Detail/components/AnVILCMG/components/RequestAccess/requestAccess";

/**
* Build props for activity type BasicCell component from the given activities response.
Expand Down Expand Up @@ -512,6 +513,7 @@ export const buildDatasetHero = (
datasetsResponse: DatasetsResponse
): React.ComponentProps<typeof C.BackPageHero> => {
return {
actions: getDatasetRequestAccess(datasetsResponse),
breadcrumbs: getDatasetBreadcrumbs(datasetsResponse),
callToAction: getDatasetCallToAction(datasetsResponse),
title: getDatasetTitle(datasetsResponse),
Expand Down Expand Up @@ -1079,10 +1081,7 @@ function getDatasetCallToAction(
): CallToAction | undefined {
const isReady = isResponseReady(datasetsResponse);
const isAccessGranted = isDatasetAccessible(datasetsResponse);
const registeredIdentifier = getDatasetRegisteredIdentifier(datasetsResponse);
if (!isReady) {
return;
}
if (!isReady) return;
// Display export button if user is authorized to access the dataset.
if (isAccessGranted) {
return {
Expand All @@ -1091,14 +1090,6 @@ function getDatasetCallToAction(
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.
}

Expand All @@ -1116,6 +1107,23 @@ export function getDatasetRegisteredIdentifier(
);
}

/**
* Returns the `actions` prop for the Hero component from the given datasets response.
* @param datasetsResponse - Response model return from datasets API.
* @returns react node to be used as the `actions` props for the Hero component.
*/
function getDatasetRequestAccess(
datasetsResponse: DatasetsResponse
): ReactNode {
const isReady = isResponseReady(datasetsResponse);
const isAccessGranted = isDatasetAccessible(datasetsResponse);
if (!isReady) return null;
// Display nothing if user is authorized to access the dataset.
if (isAccessGranted) return null;
// Display request access button if user is not authorized to access the dataset.
return RequestAccess({ datasetsResponse });
}

/**
* Returns StatusBadge component props from the given datasets response.
* @param datasetsResponse - Response model return from datasets API.
Expand Down
Loading