- Login to CRDC
+ Login to CRDC Submission Portal
Please login with a Login.gov account to make a data submission request or to upload data for approved submissions
diff --git a/src/content/organizations/ListView.tsx b/src/content/organizations/ListView.tsx
index a8e20dc7..c9078570 100644
--- a/src/content/organizations/ListView.tsx
+++ b/src/content/organizations/ListView.tsx
@@ -161,6 +161,12 @@ const StyledTablePagination = styled(TablePagination)<{ component: React.Element
background: "#F5F7F8",
});
+const StyledStudyCount = styled(Typography)<{ component: ElementType }>(({ theme }) => ({
+ textDecoration: "underline",
+ cursor: "pointer",
+ color: theme.palette.primary.main,
+}));
+
const columns: Column[] = [
{
label: "Name",
@@ -174,33 +180,34 @@ const columns: Column[] = [
},
{
label: "Studies",
- value: ({ _id, studies }) => (
-
-
- {studies?.slice(0, 2).map((s) => s.studyAbbreviation).join(", ")}
- {studies?.length > 2 && ", ..."}
-
- {studies?.length > 0 && (
-
- {studies.map(({ studyName, studyAbbreviation }) => (
-
- {studyName}
- {" ("}
- {studyAbbreviation}
- {") "}
-
-
- ))}
-
- )}
- placement="top"
- arrow
- />
- )}
-
- ),
+ value: ({ _id, studies }) => {
+ if (!studies || studies?.length < 1) {
+ return "";
+ }
+
+ return (
+ <>
+ {studies[0].studyAbbreviation}
+ {studies.length > 1 && " and "}
+ {studies.length > 1 && (
+
}
+ placement="top"
+ open={undefined}
+ onBlur={undefined}
+ disableHoverListener={false}
+ arrow
+ >
+
+ other
+ {" "}
+ {studies.length - 1}
+
+
+ )}
+ >
+ );
+ },
},
{
label: "Status",
@@ -219,6 +226,20 @@ const columns: Column[] = [
},
];
+const StudyContent: FC<{ _id: Organization["_id"], studies: Organization["studies"] }> = ({ _id, studies }) => (
+
+ {studies?.map(({ studyName, studyAbbreviation }) => (
+
+ {studyName}
+ {" ("}
+ {studyAbbreviation}
+ {") "}
+
+
+ ))}
+
+);
+
/**
* View for List of Organizations
*
diff --git a/src/content/questionnaire/FormView.tsx b/src/content/questionnaire/FormView.tsx
index 0a1f4f6b..c781d9be 100644
--- a/src/content/questionnaire/FormView.tsx
+++ b/src/content/questionnaire/FormView.tsx
@@ -571,7 +571,7 @@ const FormView: FC
= ({ section, classes } : Props) => {
diff --git a/src/content/questionnaire/ListView.tsx b/src/content/questionnaire/ListView.tsx
index 53732183..39599e74 100644
--- a/src/content/questionnaire/ListView.tsx
+++ b/src/content/questionnaire/ListView.tsx
@@ -246,7 +246,7 @@ const ListingView: FC = () => {
<>
@@ -342,7 +342,7 @@ const ListingView: FC = () => {
fontSize={18}
color="#AAA"
>
- There are no applications associated with your account
+ There are no submission requests associated with your account
diff --git a/src/content/users/APITokenDialog.tsx b/src/content/users/APITokenDialog.tsx
new file mode 100644
index 00000000..a85fe2b3
--- /dev/null
+++ b/src/content/users/APITokenDialog.tsx
@@ -0,0 +1,275 @@
+import { FC, useState } from "react";
+import { Button, Dialog, DialogProps, IconButton, OutlinedInput, Stack, Typography, styled } from "@mui/material";
+import { useMutation } from "@apollo/client";
+import { GRANT_TOKEN, GrantTokenResp } from "../../graphql";
+import GenericAlert, { AlertState } from "../../components/GenericAlert";
+import { ReactComponent as CopyIconSvg } from "../../assets/icons/copy_icon.svg";
+import { ReactComponent as CloseIconSvg } from "../../assets/icons/close_icon.svg";
+import { useAuthContext } from "../../components/Contexts/AuthContext";
+
+const StyledDialog = styled(Dialog)({
+ "& .MuiDialog-paper": {
+ maxWidth: "none",
+ borderRadius: "8px",
+ width: "755px !important",
+ padding: "47px 59px 71px 54px",
+ border: "2px solid #0B7F99",
+ background: "linear-gradient(0deg, #F2F6FA 0%, #F2F6FA 100%), #2E4D7B",
+ boxShadow: "0px 4px 45px 0px rgba(0, 0, 0, 0.40)",
+ },
+});
+
+const StyledHeader = styled(Typography)({
+ color: "#0B7F99",
+ fontFamily: "'Nunito Sans', 'Rubik', sans-serif",
+ fontSize: "35px",
+ fontStyle: "normal",
+ fontWeight: 900,
+ lineHeight: "30px",
+ marginBottom: "50px"
+});
+
+const StyledTitle = styled(Typography)({
+ fontFamily: "'Nunito', 'Rubik', sans-serif",
+ fontSize: "16px",
+ fontStyle: "normal",
+ fontWeight: 400,
+ lineHeight: "19.6px",
+ marginBottom: "58px"
+});
+
+const StyledExplanationText = styled(Typography)({
+ fontFamily: "'Nunito', 'Rubik', sans-serif",
+ color: "#083A50",
+ textAlign: "center",
+ fontSize: "16px",
+ fontStyle: "italic",
+ fontWeight: 400,
+ lineHeight: "19.6px",
+ marginTop: "20px"
+});
+
+const StyledTokenInput = styled(OutlinedInput)({
+ display: "flex",
+ width: "313px",
+ height: "44px",
+ padding: "12px 9px",
+ margin: "34px auto 0",
+ justifyContent: "center",
+ alignItems: "center",
+ gap: "10px",
+ flexShrink: 0,
+ background: "#FFFFFF",
+ color: "#000000",
+ fontFamily: "'Nunito', 'Rubik', sans-serif",
+ fontSize: "16px",
+ fontStyle: "normal",
+ fontWeight: 400,
+ lineHeight: "19.6px",
+ "& .MuiOutlinedInput-notchedOutline": {
+ borderRadius: "8px",
+ border: "1px solid #6B7294 !important",
+
+ }
+});
+
+const StyledGenerateButton = styled(Button)({
+ display: "flex",
+ width: "128px",
+ height: "42px",
+ padding: "12px 7px",
+ justifyContent: "center",
+ alignItems: "center",
+ borderRadius: "8px",
+ border: "1px solid #000",
+ background: "#1D91AB",
+ color: "#FFFFFF",
+ textAlign: "center",
+ fontFamily: "'Nunito', 'Rubik', sans-serif",
+ fontSize: "16px",
+ fontStyle: "normal",
+ fontWeight: 700,
+ lineHeight: "24px",
+ letterSpacing: "0.32px",
+ textTransform: "none",
+ "&:hover": {
+ background: "#1D91AB",
+ },
+});
+
+const StyledCopyTokenButton = styled(IconButton)(() => ({
+ color: "#000000",
+ "&.MuiIconButton-root.Mui-disabled": {
+ color: "#B0B0B0"
+ }
+}));
+
+const StyledCloseDialogButton = styled(IconButton)(() => ({
+ position: 'absolute',
+ right: "21px",
+ top: "11px",
+ padding: "10px",
+ "& svg": {
+ color: "#44627C"
+ }
+}));
+
+const StyledCloseButton = styled(Button)({
+ display: "flex",
+ width: "128px",
+ height: "42px",
+ padding: "12px 60px",
+ justifyContent: "center",
+ alignItems: "center",
+ borderRadius: "8px",
+ border: "1px solid #000",
+ color: "#000",
+ textAlign: "center",
+ fontFamily: "'Nunito', 'Rubik', sans-serif",
+ fontSize: "16px",
+ fontStyle: "normal",
+ fontWeight: "700",
+ lineHeight: "24px",
+ letterSpacing: "0.32px",
+ textTransform: "none",
+ alignSelf: "center",
+ marginTop: "45px",
+ "&:hover": {
+ background: "transparent",
+ border: "1px solid #000",
+ }
+});
+
+const canGenerateTokenRoles: User["role"][] = ["Submitter", "Organization Owner"];
+
+type Props = {
+ title?: string;
+ message?: string;
+ disableActions?: boolean;
+ loading?: boolean;
+ onClose?: () => void;
+ onSubmit?: (reviewComment: string) => void;
+} & Omit;
+
+const APITokenDialog: FC = ({
+ title,
+ message,
+ disableActions,
+ loading,
+ onClose,
+ onSubmit,
+ open,
+ ...rest
+}) => {
+ const { user } = useAuthContext();
+
+ const [tokens, setTokens] = useState([]);
+ const [tokenIdx, setTokenIdx] = useState(null);
+ const [changesAlert, setChangesAlert] = useState(null);
+
+ const [grantToken] = useMutation(GRANT_TOKEN, {
+ context: { clientName: 'userService' },
+ fetchPolicy: 'no-cache'
+ });
+
+ const onGenerateTokenError = () => {
+ setChangesAlert({ severity: "error", message: `Token was unable to be created.` });
+ setTimeout(() => setChangesAlert(null), 10000);
+ };
+
+ const generateToken = async () => {
+ if (!canGenerateTokenRoles.includes(user?.role)) {
+ onGenerateTokenError();
+ return;
+ }
+
+ try {
+ const { data: d, errors } = await grantToken();
+ const tokens = d?.grantToken?.tokens;
+ if (errors || !tokens?.length) {
+ onGenerateTokenError();
+ return;
+ }
+
+ setTokens(tokens);
+ setTokenIdx(0);
+ } catch (err) {
+ onGenerateTokenError();
+ }
+ };
+
+ const handleCreateToken = () => {
+ if (!tokens?.length || tokenIdx + 1 >= tokens.length) {
+ generateToken();
+ return;
+ }
+ setTokenIdx((idx) => idx++);
+ };
+
+ const handleCopyToken = () => {
+ if (!tokens?.length || tokenIdx === null || tokenIdx > tokens.length) {
+ return;
+ }
+ navigator.clipboard.writeText(tokens[tokenIdx]);
+ };
+
+ const handleCloseDialog = () => {
+ if (typeof onClose === "function") {
+ onClose();
+ }
+ setTokens(null);
+ setTokenIdx(null);
+ setChangesAlert(null);
+ };
+
+ return (
+
+
+
+ {changesAlert?.message}
+
+
+
+
+
+
+
+ An API Token is required to utilize the Uploader CLI tool for file uploads.
+
+
+ Each time you click the 'Create Token' button, a new token will be generated, and
+
+ the previous token will be invalidated. A token expires 60 days after its creation.
+
+
+
+ Create Token
+
+
+
+
+
+
+
+ Copy your token to the clipboard,
+
+ as this will be the only time you can see this token
+
+
+ Close
+
+
+ );
+};
+
+export default APITokenDialog;
diff --git a/src/graphql/createBatch.ts b/src/graphql/createBatch.ts
new file mode 100644
index 00000000..f0f2bb19
--- /dev/null
+++ b/src/graphql/createBatch.ts
@@ -0,0 +1,31 @@
+import gql from "graphql-tag";
+
+export const mutation = gql`
+ mutation createBatch ($submissionID: ID!, $type: String, $metadataIntention: String, $files: [FileInput]) {
+ createBatch(
+ submissionID: $submissionID,
+ type: $type,
+ metadataIntention: $metadataIntention,
+ files: $files
+ ) {
+ _id
+ submissionID
+ bucketName
+ filePrefix
+ type
+ metadataIntention
+ fileCount
+ files {
+ fileName
+ signedURL
+ }
+ status
+ createdAt
+ updatedAt
+ }
+ }
+`;
+
+export type Response = {
+ createBatch: NewBatch
+};
diff --git a/src/graphql/getDataSubmission.ts b/src/graphql/getSubmission.ts
similarity index 68%
rename from src/graphql/getDataSubmission.ts
rename to src/graphql/getSubmission.ts
index 541a78fd..1bb2c032 100644
--- a/src/graphql/getDataSubmission.ts
+++ b/src/graphql/getSubmission.ts
@@ -1,17 +1,20 @@
import gql from "graphql-tag";
export const query = gql`
- query getDataSubmission($id: ID!) {
- getDataSubmission(_id: $id) {
+ query getSubmission($id: ID!) {
+ getSubmission(_id: $id) {
_id
name
submitterID
submitterName
- organization
+ organization {
+ _id
+ name
+ }
dataCommons
modelVersion
studyAbbreviation
- dbGapID
+ dbGaPID
bucketName
rootPath
status
@@ -20,17 +23,15 @@ export const query = gql`
reviewComment
dateTime
userID
- __typename
}
- concierge
+ conciergeName
conciergeEmail
createdAt
updatedAt
- __typename
}
}
`;
export type Response = {
- getDataSubmission: Submission;
+ getSubmission: Submission;
};
diff --git a/src/graphql/grantToken.ts b/src/graphql/grantToken.ts
new file mode 100644
index 00000000..7bdf7f2d
--- /dev/null
+++ b/src/graphql/grantToken.ts
@@ -0,0 +1,14 @@
+import gql from 'graphql-tag';
+
+export const mutation = gql`
+ mutation {
+ grantToken {
+ tokens
+ message
+ }
+ }
+`;
+
+export type Response = {
+ grantToken: Tokens;
+};
diff --git a/src/graphql/index.ts b/src/graphql/index.ts
index b33c4b0e..d3a0ad96 100644
--- a/src/graphql/index.ts
+++ b/src/graphql/index.ts
@@ -27,12 +27,21 @@ export { mutation as UPDATE_MY_USER } from "./updateMyUser";
export type { Response as UpdateMyUserResp } from "./updateMyUser";
// Data Submissions
-export { query as GET_DATA_SUBMISSION } from "./getDataSubmission";
-export type { Response as GetDataSubmissionResp } from "./getDataSubmission";
+export { query as GET_SUBMISSION } from "./getSubmission";
+export type { Response as GetSubmissionResp } from "./getSubmission";
export { query as GET_DATA_SUBMISSION_BATCH_FILES } from "./getDataSubmissionBatchFiles";
export type { Response as GetDataSubmissionBatchFilesResp } from "./getDataSubmissionBatchFiles";
+export { mutation as CREATE_BATCH } from './createBatch';
+export type { Response as CreateBatchResp } from './createBatch';
+
+export { mutation as UPDATE_BATCH } from './updateBatch';
+export type { Response as UpdateBatchResp } from './updateBatch';
+
+export { query as LIST_BATCHES } from "./listBatches";
+export type { Response as ListBatchesResp } from "./listBatches";
+
// User Profile
export { query as GET_USER } from "./getUser";
export type { Response as GetUserResp } from "./getUser";
@@ -61,3 +70,7 @@ export type { Response as ListApprovedStudiesResp } from './listApprovedStudies'
export { mutation as CREATE_ORG } from './createOrganization';
export type { Response as CreateOrgResp } from './createOrganization';
+
+// Misc.
+export { mutation as GRANT_TOKEN } from './grantToken';
+export type { Response as GrantTokenResp } from './grantToken';
diff --git a/src/graphql/listBatches.ts b/src/graphql/listBatches.ts
new file mode 100644
index 00000000..280d8733
--- /dev/null
+++ b/src/graphql/listBatches.ts
@@ -0,0 +1,45 @@
+import gql from 'graphql-tag';
+
+export const query = gql`
+ query listBatches(
+ $submissionID: ID!
+ $first: Int
+ $offset: Int
+ $orderBy: String
+ $sortDirection: String
+ ) {
+ listBatches(
+ submissionID: $submissionID
+ first: $first
+ offset: $offset
+ orderBy: $orderBy
+ sortDirection: $sortDirection
+ ) {
+ total
+ batches {
+ _id
+ submissionID
+ type
+ metadataIntention
+ fileCount
+ files {
+ filePrefix
+ fileName
+ size
+ status
+ errors
+ createdAt
+ updatedAt
+ }
+ status
+ errors
+ createdAt
+ updatedAt
+ }
+ }
+ }
+`;
+
+export type Response = {
+ listBatches: ListBatches;
+};
diff --git a/src/graphql/updateBatch.ts b/src/graphql/updateBatch.ts
new file mode 100644
index 00000000..fbb3ec10
--- /dev/null
+++ b/src/graphql/updateBatch.ts
@@ -0,0 +1,39 @@
+import gql from "graphql-tag";
+
+/*
+ * NOTE: This is used to update the Batch after upload to signedURLs has been complete.
+ * This is NOT for upload type "update"
+ */
+export const mutation = gql`
+ mutation updateBatch(
+ $batchID: ID!
+ $files: [UploadResult]
+ ) {
+ updateBatch(
+ batchID: $batchID
+ files: $files
+ ) {
+ _id
+ submissionID
+ type
+ metadataIntention
+ fileCount
+ files {
+ filePrefix
+ fileName
+ size
+ status
+ errors
+ createdAt
+ updatedAt
+ }
+ status
+ createdAt
+ updatedAt
+ }
+ }
+`;
+
+export type Response = {
+ updateBatch: Batch;
+};
diff --git a/src/types/Auth.d.ts b/src/types/Auth.d.ts
index b625aa31..9394130a 100644
--- a/src/types/Auth.d.ts
+++ b/src/types/Auth.d.ts
@@ -69,3 +69,8 @@ type EditOrganizationInput = {
studies: Pick[];
status: Organization["status"];
};
+
+type Tokens = {
+ tokens: string[];
+ message: string;
+};
diff --git a/src/types/Submissions.d.ts b/src/types/Submissions.d.ts
index e8820f63..8a33b109 100644
--- a/src/types/Submissions.d.ts
+++ b/src/types/Submissions.d.ts
@@ -1,24 +1,101 @@
type Submission = {
- _id
- name
- submitterID
- submitterName
- organization
+ _id: string; // aka. submissionID
+ name: string;
+ submitterID: string;
+ submitterName: string; //
+ organization: Pick; // Organization
dataCommons: string;
- modelVersion: string; // # for future use
+ modelVersion: string; // for future use
studyAbbreviation: string;
dbGaPID: string; // # aka. phs number
bucketName: string; // # populated from organization
rootPath: string; // # a submission folder will be created under this path, default is / or "" meaning root folder
- status: DataSubmissionStatus; // [New, In Progress, Submitted, Released, Canceled, Transferred, Completed, Archived]
- history: DataSubmissionHistoryEvent[]
- conciergeName: string; // # Concierge name
- conciergeEmail: string; // # Concierge email (MIGHT CHANGE)
- createdAt: string; // # ISO 8601 date time format with UTC or offset e.g., 2023-05-01T09:23:30Z
- updatedAt: string; // # ISO 8601 date time format with UTC or offset e.g., 2023-05-01T09:23:30Z
+ status: SubmissionStatus; // [New, In Progress, Submitted, Released, Canceled, Transferred, Completed, Archived]
+ history: SubmissionHistoryEvent[];
+ conciergeName: string; // Concierge name
+ conciergeEmail: string; // Concierge email
+ createdAt: string; // ISO 8601 date time format with UTC or offset e.g., 2023-05-01T09:23:30Z
+ updatedAt: string; // ISO 8601 date time format with UTC or offset e.g., 2023-05-01T09:23:30Z
};
-type SubmissionStatus = "New" | "In Progress" | "Submitted" | "Released" | "Withdrawn" | "Rejected" | "Completed" | "Archived" | "Canceled";
+type SubmissionStatus =
+ | "New"
+ | "In Progress"
+ | "Submitted"
+ | "Released"
+ | "Withdrawn"
+ | "Rejected"
+ | "Completed"
+ | "Archived"
+ | "Canceled";
+
+type FileInfo = {
+ filePrefix: string; // prefix/path within S3 bucket
+ fileName: string;
+ size: number;
+ status: string; // [New, Uploaded, Failed]
+ errors: [string];
+ createdAt: string; // ISO 8601 date time format with UTC or offset e.g., 2023-05-01T09:23:30Z
+ updatedAt: string; // ISO 8601 date time format with UTC or offset e.g., 2023-05-01T09:23:30Z
+};
+
+type FileInput = {
+ fileName: string;
+ size: number;
+};
+
+type FileURL = {
+ fileName: string;
+ signedURL: string;
+};
+
+type UploadResult = {
+ fileName: string;
+ succeeded: boolean;
+ errors: string[];
+};
+
+type BatchFileInfo = {
+ filePrefix: string; // prefix/path within S3 bucket
+ fileName: string;
+ size: number;
+ status: string; // [New, Uploaded, Failed]
+ errors: string[];
+ createdAt: string; // ISO 8601 date time format with UTC or offset e.g., 2023-05-01T09:23:30Z
+ updatedAt: string // ISO 8601 date time format with UTC or offset e.g., 2023-05-01T09:23:30Z
+};
+
+type BatchStatus = "New" | "Uploaded" | "Upload Failed" | "Loaded" | "Rejected";
+
+type MetadataIntention = "New" | "Update" | "Delete";
+
+type Batch = {
+ _id: string;
+ submissionID: string; // parent
+ type: string; // [metadata, file]
+ metadataIntention: MetadataIntention; // [New, Update, Delete], Update is meant for "Update or insert", metadata only! file batches are always treated as Update
+ fileCount: number; // calculated by BE
+ files: BatchFileInfo[];
+ status: BatchStatus; // [New, Uploaded, Upload Failed, Loaded, Rejected] Loaded and Rejected are for metadata batch only
+ errors: string[];
+ createdAt: string; // ISO 8601 date time format with UTC or offset e.g., 2023-05-01T09:23:30Z
+ updatedAt: string; // ISO 8601 date time format with UTC or offset e.g., 2023-05-01T09:23:30Z
+};
+
+type NewBatch = {
+ _id: string;
+ submissionID: string; // parent
+ bucketName?: string; // S3 bucket of the submission, for file batch / CLI use
+ filePrefix?: string; // prefix/path within S3 bucket, for file batch / CLI use
+ type: string; // [metadata, file]
+ metadataIntention: MetadataIntention; // [New, Update, Delete], Update is meant for "Update or insert", metadata only! file batches are always treated as Update
+ fileCount: number; // calculated by BE
+ files: FileURL[];
+ status: BatchStatus; // [New, Uploaded, Upload Failed, Loaded, Rejected] Loaded and Rejected are for metadata batch only
+ errors: string[];
+ createdAt: string; // ISO 8601 date time format with UTC or offset e.g., 2023-05-01T09:23:30Z
+ updatedAt: string; // ISO 8601 date time format with UTC or offset e.g., 2023-05-01T09:23:30Z
+};
type BatchFile = {
_id: string;
@@ -29,4 +106,15 @@ type BatchFile = {
errorCount: number;
};
-type DataSubmissionHistoryEvent = HistoryBase;
+type ListBatches = {
+ total: number;
+ batches: Batch[];
+};
+
+type TempCredentials = {
+ accessKeyId: string;
+ secretAccessKey: string;
+ sessionToken: string;
+};
+
+type SubmissionHistoryEvent = HistoryBase;