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

CRDCDH-730 Validation for Applicable Types, & Notistack #261

Merged
merged 8 commits into from
Jan 17, 2024
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
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