Skip to content

Commit

Permalink
Merge pull request #261 from CBIIT/CRDCDH-730
Browse files Browse the repository at this point in the history
CRDCDH-730 Validation for Applicable Types, & Notistack
  • Loading branch information
Alejandro-Vega authored Jan 17, 2024
2 parents c752c6e + 45ca65e commit 7b5c82e
Show file tree
Hide file tree
Showing 9 changed files with 195 additions and 44 deletions.
31 changes: 31 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"dayjs": "^1.11.8",
"graphql": "^16.7.1",
"lodash": "^4.17.21",
"notistack": "^3.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0",
Expand Down
23 changes: 19 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { SnackbarProvider } from 'notistack';
import { ThemeProvider, CssBaseline, createTheme } from "@mui/material";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import routeConfig from "./router";
import StyledNotistackAlerts from './components/StyledNotistackAlerts';

declare module '@mui/material/styles' {
interface PaletteOptions {
Expand Down Expand Up @@ -42,10 +44,23 @@ const router = createBrowserRouter(routeConfig);
function App() {
return (
<ThemeProvider theme={theme}>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<CssBaseline />
<RouterProvider router={router} />
</LocalizationProvider>
<SnackbarProvider
maxSnack={3}
autoHideDuration={10000}
anchorOrigin={{ vertical: "top", horizontal: "center" }}
Components={{
default: StyledNotistackAlerts,
error: StyledNotistackAlerts,
success: StyledNotistackAlerts,
}}
hideIconVariant
preventDuplicate
>
<LocalizationProvider dateAdapter={AdapterDayjs}>
<CssBaseline />
<RouterProvider router={router} />
</LocalizationProvider>
</SnackbarProvider>
</ThemeProvider>
);
}
Expand Down
96 changes: 57 additions & 39 deletions src/components/DataSubmissions/ValidationControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { FC, useEffect, useMemo, useState } from 'react';
import { useMutation } from '@apollo/client';
import { FormControlLabel, RadioGroup, styled } from '@mui/material';
import { LoadingButton } from '@mui/lab';
import { useSnackbar } from 'notistack';
import { useAuthContext } from '../Contexts/AuthContext';
import StyledRadioButton from "../Questionnaire/StyledRadioButton";
import { VALIDATE_SUBMISSION, ValidateSubmissionResp } from '../../graphql';
import GenericAlert, { AlertState } from '../GenericAlert';
import { getDefaultValidationType, getValidationTypes } from '../../utils';

type Props = {
/**
Expand All @@ -22,10 +23,6 @@ type Props = {
onValidate: (success: boolean) => void;
};

type ValidationType = "Metadata" | "Files" | "All";

type UploadType = "New" | "All";

const StyledValidateButton = styled(LoadingButton)({
alignSelf: "center",
display: "flex",
Expand Down Expand Up @@ -137,23 +134,49 @@ const ValidateStatuses: Submission["status"][] = ["In Progress", "Withdrawn", "R
*/
const ValidationControls: FC<Props> = ({ dataSubmission, onValidate }: Props) => {
const { user } = useAuthContext();
const [validationType, setValidationType] = useState<ValidationType>("Metadata");
const [uploadType, setUploadType] = useState<UploadType>("New");
const { enqueueSnackbar } = useSnackbar();

const [validationType, setValidationType] = useState<ValidationType>(null);
const [uploadType, setUploadType] = useState<ValidationTarget>("New");
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isValidating, setIsValidating] = useState<boolean>(dataSubmission?.fileValidationStatus === "Validating"
|| dataSubmission?.metadataValidationStatus === "Validating");
const [validationAlert, setValidationAlert] = useState<AlertState>(null);

const canValidateData: boolean = useMemo(() => ValidateRoles.includes(user?.role), [user?.role]);
const validateButtonEnabled: boolean = useMemo(() => ValidateStatuses.includes(dataSubmission?.status), [dataSubmission?.status]);
const canValidateMetadata: boolean = useMemo(() => {
if (!user?.role || ValidateRoles.includes(user?.role) === false) {
return false;
}
if (!dataSubmission?.status || ValidateStatuses.includes(dataSubmission?.status) === false) {
return false;
}

return dataSubmission?.metadataValidationStatus !== null;
}, [user?.role, dataSubmission?.metadataValidationStatus]);

const canValidateFiles: boolean = useMemo(() => {
if (!user?.role || ValidateRoles.includes(user?.role) === false) {
return false;
}
if (!dataSubmission?.status || ValidateStatuses.includes(dataSubmission?.status) === false) {
return false;
}

return dataSubmission?.fileValidationStatus !== null;
}, [user?.role, dataSubmission?.fileValidationStatus]);

const [validateSubmission] = useMutation<ValidateSubmissionResp>(VALIDATE_SUBMISSION, {
context: { clientName: 'backend' },
fetchPolicy: 'no-cache'
});

const handleValidateFiles = async () => {
if (isValidating) {
if (isValidating || !validationType || !uploadType) {
return;
}
if (!canValidateFiles && validationType === "Files") {
return;
}
if (!canValidateMetadata && validationType === "Metadata") {
return;
}

Expand All @@ -162,90 +185,85 @@ const ValidationControls: FC<Props> = ({ dataSubmission, onValidate }: Props) =>
const { data, errors } = await validateSubmission({
variables: {
_id: dataSubmission?._id,
types: getTypes(validationType),
types: getValidationTypes(validationType),
scope: uploadType === "New" ? "New" : "All",
}
});

if (errors || !data?.validateSubmission?.success) {
setValidationAlert({ message: "Unable to initiate validation process.", severity: "error" });
enqueueSnackbar("Unable to initiate validation process.", { variant: "error" });
setIsValidating(false);
onValidate?.(false);
} else {
setValidationAlert({ message: "Validation process is starting; this may take some time. Please wait before initiating another validation.", severity: "success" });
enqueueSnackbar("Validation process is starting; this may take some time. Please wait before initiating another validation.", { variant: "success" });
setIsValidating(true);
onValidate?.(true);
}

// Reset form to default values
setValidationType("Metadata");
setValidationType(getDefaultValidationType(dataSubmission));
setUploadType("New");
setIsLoading(false);
setTimeout(() => setValidationAlert(null), 10000);
};

const getTypes = (validationType: ValidationType): string[] => {
switch (validationType) {
case "Metadata":
return ["metadata"];
case "Files":
return ["file"];
default:
return ["metadata", "file"];
}
};

useEffect(() => {
setIsValidating(dataSubmission?.fileValidationStatus === "Validating"
|| dataSubmission?.metadataValidationStatus === "Validating");
}, [dataSubmission?.fileValidationStatus, dataSubmission?.metadataValidationStatus]);

useEffect(() => {
if (validationType !== null) {
return;
}
if (typeof dataSubmission === "undefined") {
return;
}

setValidationType(getDefaultValidationType(dataSubmission));
}, [dataSubmission]);

return (
<StyledFileValidationSection>
<GenericAlert open={!!validationAlert} severity={validationAlert?.severity} key="data-validation-alert">
{validationAlert?.message}
</GenericAlert>
<div className="fileValidationLeftSide">
<div className="fileValidationLeftSideTopRow">
<div className="headerText">Validation Type:</div>
<div className="fileValidationRadioButtonGroup">
<RadioGroup value={validationType} onChange={(event, val: ValidationType) => setValidationType(val)} row>
<RadioGroup value={validationType} onChange={(e, val: ValidationType) => setValidationType(val)} row>
<StyledRadioControl
value="Metadata"
control={<StyledRadioButton readOnly={false} />}
label="Validate Metadata"
disabled={!canValidateData}
disabled={!canValidateMetadata}
/>
<StyledRadioControl
value="Files"
control={<StyledRadioButton readOnly={false} />}
label="Validate Data Files"
disabled={!canValidateData}
disabled={!canValidateFiles}
/>
<StyledRadioControl
value="All"
control={<StyledRadioButton readOnly={false} />}
label="Both"
disabled={!canValidateData}
disabled={!canValidateFiles || !canValidateMetadata}
/>
</RadioGroup>
</div>
</div>
<div className="fileValidationLeftSideBottomRow">
<div className="headerText">Validation Target:</div>
<div className="fileValidationRadioButtonGroup">
<RadioGroup value={uploadType} onChange={(event, val: UploadType) => setUploadType(val)} row>
<RadioGroup value={uploadType} onChange={(event, val: ValidationTarget) => setUploadType(val)} row>
<StyledRadioControl
value="New"
control={<StyledRadioButton readOnly={false} />}
label="New Uploaded Data"
disabled={!canValidateData}
disabled={!canValidateFiles && !canValidateMetadata}
/>
<StyledRadioControl
value="All"
control={<StyledRadioButton readOnly={false} />}
label="All Uploaded Data"
disabled={!canValidateData}
disabled={!canValidateFiles && !canValidateMetadata}
/>
</RadioGroup>
</div>
Expand All @@ -254,7 +272,7 @@ const ValidationControls: FC<Props> = ({ dataSubmission, onValidate }: Props) =>
<StyledValidateButton
variant="contained"
disableElevation
disabled={!canValidateData || !validateButtonEnabled || isValidating}
disabled={(!canValidateFiles && !canValidateMetadata) || isValidating}
loading={isLoading}
onClick={handleValidateFiles}
>
Expand Down
5 changes: 5 additions & 0 deletions src/components/GenericAlert/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ type Props = {
children: React.ReactNode;
};

/**
* Basic alert component that can be used to display a message to the user.
*
* @deprecated DO NOT USE. Replaced by `enqueueSnackbar` from Notistack.
*/
const GenericAlert : FC<Props> = ({
open,
children,
Expand Down
29 changes: 29 additions & 0 deletions src/components/StyledNotistackAlerts/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { styled } from '@mui/material';
import { MaterialDesignContent } from 'notistack';

const BaseSnackbarStyles = {
color: '#ffffff',
width: '535px',
minHeight: '50px',
boxShadow: '-4px 8px 27px 4px rgba(27,28,28,0.09)',
boxSizing: 'border-box',
userSelect: 'none',
justifyContent: 'center',
};

const StyledNotistackAlerts = styled(MaterialDesignContent)({
'&.notistack-MuiContent-default': {
...BaseSnackbarStyles,
backgroundColor: '#5D53F6',
},
'&.notistack-MuiContent-error': {
...BaseSnackbarStyles,
backgroundColor: '#E74040',
},
'&.notistack-MuiContent-success': {
...BaseSnackbarStyles,
backgroundColor: '#5D53F6',
},
});

export default StyledNotistackAlerts;
18 changes: 17 additions & 1 deletion src/types/Submissions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ type Submission = {
updatedAt: string; // ISO 8601 date time format with UTC or offset e.g., 2023-05-01T09:23:30Z
};

type ValidationStatus = "New" | "Validating" | "Passed" | "Error" | "Warning";
/**
* The status of a Metadata or Files in a submission.
*
* @note `null` indicates that the type has not been uploaded yet.
* @note `New` indicates that the type has been uploaded but not validated yet.
*/
type ValidationStatus = null | "New" | "Validating" | "Passed" | "Error" | "Warning";

type SubmissionStatus =
| "New"
Expand Down Expand Up @@ -218,3 +224,13 @@ type DataValidationResult = {
*/
message: string;
};

/**
* The type of Data Validation to perform.
*/
type ValidationType = "Metadata" | "Files" | "All";

/**
* The target of Data Validation action.
*/
type ValidationTarget = "New" | "All";
Loading

0 comments on commit 7b5c82e

Please sign in to comment.