diff --git a/frontend/src/app/components/AlertDialog.js b/frontend/src/app/components/AlertDialog.js index fe9977b0..c526b27a 100644 --- a/frontend/src/app/components/AlertDialog.js +++ b/frontend/src/app/components/AlertDialog.js @@ -6,6 +6,7 @@ import DialogActions from "@mui/material/DialogActions"; import DialogContent from "@mui/material/DialogContent"; import DialogContentText from "@mui/material/DialogContentText"; import DialogTitle from "@mui/material/DialogTitle"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; const AlertDialog = (props) => { const { @@ -31,11 +32,13 @@ const AlertDialog = (props) => { aria-labelledby="alert-dialog-title" aria-describedby="alert-dialog-description" > - {title} + + {title} + - + {dialogue} - + - + {confirmText && ( + + )} @@ -62,14 +67,17 @@ const AlertDialog = (props) => { AlertDialog.defaultProps = { dialogue: "", title: "", + cancelText: "cancel", + confirmText: "", }; AlertDialog.propTypes = { open: PropTypes.bool.isRequired, title: PropTypes.string, - dialogue: PropTypes.string, - cancelText: PropTypes.string.isRequired, + dialogue: PropTypes.oneOfType([PropTypes.string, PropTypes.object]) + .isRequired, + cancelText: PropTypes.string, handleCancel: PropTypes.func.isRequired, - confirmText: PropTypes.string.isRequired, + confirmText: PropTypes.string, handleConfirm: PropTypes.func.isRequired, }; diff --git a/frontend/src/app/styles/App.scss b/frontend/src/app/styles/App.scss index 86664c1e..9e130984 100644 --- a/frontend/src/app/styles/App.scss +++ b/frontend/src/app/styles/App.scss @@ -14,6 +14,9 @@ $default-link-blue: #568dba; $default-background-grey: #f2f2f2; $md: 991px; $button-background-blue: #003366; +$error-red: #ce3e39; +$warning-yellow: #fcba19; +$default-blue: #003366; .App { background-color: $default-background-grey; @@ -63,7 +66,7 @@ h2, h3, h4 { font-family: "Roboto", "Open Sans", sans-serif; - color: #003366; + color: $default-blue; font-weight: 500; } @@ -110,3 +113,14 @@ h4 { .page-content { flex-grow: 1; } + +.error { + color: $error-red; +} +.warning { + color: $warning-yellow; +} +.showMore { + color: $default-link-blue; + text-decoration: underline; +} \ No newline at end of file diff --git a/frontend/src/app/styles/FileUpload.scss b/frontend/src/app/styles/FileUpload.scss index ee6608c5..1a26f7b5 100644 --- a/frontend/src/app/styles/FileUpload.scss +++ b/frontend/src/app/styles/FileUpload.scss @@ -43,3 +43,9 @@ background-color: $default-background-grey; } } +.cancel-button { + color: $default-blue !important; +} +.confirm-button { + background-color: $default-blue !important; +} diff --git a/frontend/src/uploads/UploadContainer.js b/frontend/src/uploads/UploadContainer.js index b52b355b..8b185767 100644 --- a/frontend/src/uploads/UploadContainer.js +++ b/frontend/src/uploads/UploadContainer.js @@ -9,6 +9,7 @@ import UsersContainer from "../users/UsersContainer"; import Loading from "../app/components/Loading"; import useAxios from "../app/utilities/useAxios"; import WarningsList from "./components/WarningsList"; +import UploadIssues from "./components/UploadIssues"; const UploadContainer = () => { const [uploadFiles, setUploadFiles] = useState([]); // array of objects for files to be uploaded @@ -23,16 +24,21 @@ const UploadContainer = () => { const [alertSeverity, setAlertSeverity] = useState(""); const [openDialog, setOpenDialog] = useState(false); const [adminUser, setAdminUser] = useState(false); - const axios = useAxios(); - const axiosDefault = useAxios(true); - const [dataWarning, setDataWarning] = useState({}) + const [totalIssueCount, setTotalIssueCount] = useState({}); + const [groupedErrors, setGroupedErrors] = useState({}); + const [groupedWarnings, setGroupedWarnings] = useState({}); const [alertDialogText, setAlertDialogText] = useState({ title: "", content: "", confirmText: "", - confirmAction: ()=>{}, - cancelAction: ()=>{}, - }) + confirmAction: () => {}, + cancelAction: () => {}, + cancelText: "cancel", + }); + + const axios = useAxios(); + const axiosDefault = useAxios(true); + const refreshList = () => { setRefresh(true); axios.get(ROUTES_UPLOAD.LIST).then((response) => { @@ -51,26 +57,80 @@ const UploadContainer = () => { }); }; + const groupAndCountRows = (issueArray) => { + const groupedErrors = {}; + const groupedWarnings = {}; + const totalIssueCount = { + errors: 0, + warnings: 0, + }; + + issueArray.forEach((issue) => { + const column = Object.keys(issue)[0]; + const errorDetails = issue[column]; + + Object.keys(errorDetails).forEach((errorType) => { + const severity = errorDetails[errorType].Severity; + const expectedType = errorDetails[errorType]["Expected Type"]; + const expectedFormat = errorDetails[errorType]["Expected Format"]; + const rows = errorDetails[errorType].Rows; + const rowCount = rows.length; + + if (severity === "Error") { + totalIssueCount.errors += rowCount; + if (!groupedErrors[column]) { + groupedErrors[column] = {}; + } + if (!groupedErrors[column][errorType]) { + groupedErrors[column][errorType] = { + ExpectedType: expectedType, + Rows: rows, + }; + } + } else if (severity === "Warning") { + totalIssueCount.warnings += rowCount; + if (!groupedWarnings[column]) { + groupedWarnings[column] = {}; + } + if (!groupedWarnings[column][errorType]) { + groupedWarnings[column][errorType] = { + ExpectedFormat: expectedFormat, + Rows: rows, + }; + } + } + }); + }); + + return { groupedErrors, groupedWarnings, totalIssueCount }; + }; + const showError = (error) => { const { response: errorResponse } = error; - setAlertContent("There was an issue uploading the file.") + setAlertContent("There was an issue uploading the file."); if (errorResponse && errorResponse.data && errorResponse.data.message) { setAlertContent( `${errorResponse.data.message}\n${errorResponse.data.errors ? "Errors: " + errorResponse.data.errors.join("\n") : ""}`, - ) - } else if (errorResponse && errorResponse.data && errorResponse.status === 403) { - setAlertContent("There was an error. Please refresh page and ensure you are logged in.") + ); + } else if ( + errorResponse && + errorResponse.data && + errorResponse.status === 403 + ) { + setAlertContent( + "There was an error. Please refresh page and ensure you are logged in.", + ); } setAlertSeverity("error"); setAlert(true); }; - const doUpload = (checkForWarnings) => + const doUpload = (checkForWarnings) => { uploadFiles.forEach((file) => { let filepath = file.path; - setLoading(true); - if (datasetSelected !== 'Go Electric Rebates Program'){ - checkForWarnings = false + setLoading(true); + if (datasetSelected !== "Go Electric Rebates Program") { + checkForWarnings = false; } const uploadPromises = uploadFiles.map((file) => { return axios.get(ROUTES_UPLOAD.MINIO_URL).then((response) => { @@ -81,7 +141,7 @@ const UploadContainer = () => { datasetSelected, replaceData, filepath, - checkForWarnings + checkForWarnings, }); }); }); @@ -94,44 +154,98 @@ const UploadContainer = () => { setAlertSeverity(errorCheck ? "success" : "error"); const message = responses - .map( - (response) => - `${response.data.message}${response.data.errors ? "\nErrors: " + response.data.errors.join("\n") : ""}`, - ) - .join("\n"); + .map( + (response) => + `${response.data.message}${response.data.errors ? "\nErrors: " + response.data.errors.join("\n") : ""}`, + ) + .join("\n"); setAlert(true); setAlertContent(message); - const warnings = {} + const warnings = {}; for (const [index, response] of responses.entries()) { - const filename = uploadFiles[index].name - const responseWarnings = response.data.warnings + const filename = uploadFiles[index].name; + const responseWarnings = response.data.warnings; if (responseWarnings) { - warnings[filename] = responseWarnings + warnings[filename] = responseWarnings; } } setAlertContent(message); - if (Object.keys(warnings).length > 0 && checkForWarnings == true) { // ie it is the first attempt to upload (when upload is called from the dialog its set to false) - setOpenDialog(true) + if (Object.keys(warnings).length > 0 && checkForWarnings === true) { + // ie it is the first attempt to upload (when upload is called from the dialog its set to false) + const fakeResponse = [ + { + // 'Applicant Name': { + // "blank": { + // "Expected Type": "must not be blank", + // Severity: "Error", + // Rows: [ + // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + // 18, 19, 20, 21, 22, 23, 24, + // ], + // }, + // }, + 'Phone': { + "phone number not formatted correctly": { + "Expected Type": "213-1234-1231", + Severity: "Warning", + Rows: [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + 18, 19, 20, 21, 22, 23, 24, + ], + }, + }, + }, + { + "Company Name": { + "contains null values": { + "Expected Format": "Smith, John", + Severity: "Warning", + Rows: [ + 9, 12, 13, 14, 15, 16, 17, 28, 27, 43, 23, 2323, 24, 25, + 65, 342, 23, 7, 56, 53, 56, 67, 78, 89, 45, 3, 2, 1, 54, + 56, 76, 78, 79, 90, 34, 23, 22, 21, 255, 26, 27, 27, 28, + ], + }, + }, + }, + ]; + // Call groupAndCountRows to get data and pass to state + const { groupedErrors, groupedWarnings, totalIssueCount } = + groupAndCountRows(fakeResponse); + setGroupedErrors(groupedErrors); + setGroupedWarnings(groupedWarnings); + setTotalIssueCount(totalIssueCount); + //popup for showing issues setAlertDialogText({ - title: "Warning: There are errors in the data to review", - content:( + title: + "Your file has been processed and contains the following errors and warnings!", + content: ( <> -
-

- Click continue to insert these records as is, or click cancel - to exit out and no records will be inserted: -

- -
+ {totalIssueCount.errors >= 1 && ( +
+ + {totalIssueCount.errors} Errors + {" "} + - Must fix before uploading +
+ )} + {totalIssueCount.warnings >= 1 && ( +
+ + {totalIssueCount.warnings} Warnings + {" "} + - Can upload without fixing +
+ )} - ), - confirmText: "Continue (all records will be inserted as is)", - confirmAction: handleConfirmDataInsert, - cancelAction: handleReplaceDataCancel, - })} - - setUploadFiles([]); + ), + cancelAction: () => setOpenDialog(false), + confirmText: "View Details", + confirmAction: () => setOpenDialog(false), + }); + setOpenDialog(true); + } }) .catch((error) => { showError(error); @@ -140,6 +254,7 @@ const UploadContainer = () => { setLoading(false); }); }); + }; const downloadSpreadsheet = () => { axios @@ -170,24 +285,31 @@ const UploadContainer = () => { const choice = event.target.value; if (choice === "replace") { setOpenDialog(true); + //popup for replacing data setAlertDialogText({ title: "Replace existing data?", - content: "Selecting replace will delete all previously uploaded records for this dataset", - confirmText: "Replace existing data", + content: + "Selecting replace will delete all previously uploaded records for this dataset", + confirmText: "Replace existing data", confirmAction: handleReplaceDataConfirm, cancelAction: handleReplaceDataCancel, - }) + }); } else { setReplaceData(false); } }; + const handleConfirmDataInsert = () => { + setGroupedWarnings({}) + setGroupedErrors({}) + setTotalIssueCount({}) setOpenDialog(false); - showError(false); - setAlertContent("") - doUpload(false); //upload with the checkForWarnings flag set to false! + setAlert(false); + setAlertContent(""); + doUpload(false); // Upload with the checkForWarnings flag set to false! + setUploadFiles([]) + }; - } const handleReplaceDataConfirm = () => { setReplaceData(true); setOpenDialog(false); @@ -198,14 +320,13 @@ const UploadContainer = () => { }; useEffect(() => { - refreshList(true); + refreshList(); }, []); if (refresh) { return ; } - const alertElement = alert && alertContent && alertSeverity ? ( @@ -218,7 +339,6 @@ const UploadContainer = () => { ) : null; - return (
@@ -226,15 +346,23 @@ const UploadContainer = () => { + {(totalIssueCount.errors > 0 || totalIssueCount.warnings > 0) && ( + + + + )} { /> {adminUser && ( - + )} @@ -263,4 +391,5 @@ const UploadContainer = () => {
); }; + export default withRouter(UploadContainer); diff --git a/frontend/src/uploads/components/UploadIssues.js b/frontend/src/uploads/components/UploadIssues.js new file mode 100644 index 00000000..edcdc007 --- /dev/null +++ b/frontend/src/uploads/components/UploadIssues.js @@ -0,0 +1,122 @@ +import React, { useState } from "react"; +import { + Box, + Typography, + AccordionSummary, + AccordionDetails, + Accordion, + Button, +} from "@mui/material"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import UploadIssuesDetail from "./UploadIssuesDetail"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; + +const UploadIssues = ({ + confirmUpload, + groupedErrors, + groupedWarnings, + totalIssueCount, +}) => { + const [showAllIssues, setShowAllIssues] = useState(false); + + const toggleShowAllIssues = () => { + setShowAllIssues(!showAllIssues); + }; + + const errorMsg = "Must fix before uploading"; + const warningMsg = "Can upload without fixing"; + + return ( + <> + +

+ + Your file upload results +

+ + Your file has been processed and contains the following errors and + warnings. Please review them below: + + {totalIssueCount.errors >= 1 && ( + + + {totalIssueCount.errors} Errors   + + - {errorMsg} + + )} + {totalIssueCount.warnings >= 1 && ( + + + {totalIssueCount.warnings} Warnings   + + - {warningMsg} + + )} + + + + {showAllIssues ? "Show less" : "Show more"} + + + + + {totalIssueCount.errors >= 1 && ( + + )} + {totalIssueCount.warnings >= 1 && ( + + )} + + + {totalIssueCount.warnings >= 1 && totalIssueCount.errors === 0 && ( + +

Do you want to upload the file regardless of the warnings?

+ + + + +
+ )} +
+ + ); +}; + +export default UploadIssues; diff --git a/frontend/src/uploads/components/UploadIssuesDetail.js b/frontend/src/uploads/components/UploadIssuesDetail.js new file mode 100644 index 00000000..2a447862 --- /dev/null +++ b/frontend/src/uploads/components/UploadIssuesDetail.js @@ -0,0 +1,85 @@ +import PropTypes from "prop-types"; +import React, { useState } from "react"; +import { Box, Button } from "@mui/material"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; + +const UploadIssuesDetail = ({ type, issues, totalIssueCount, msg }) => { + const [showAllRowsMap, setShowAllRowsMap] = useState({}); // State to toggle showing all rows for each issue + const classname = type === "error" ? "error" : "warning"; + const toggleShowAllRows = (column, errorType) => { + const key = `${column}_${errorType}`; + setShowAllRowsMap((prevState) => ({ + ...prevState, + [key]: !prevState[key], + })); + }; + + return ( + + + + + {totalIssueCount} {type}  + + + ({msg}) + {Object.keys(issues).map((column) => ( + + Column: {column} + {Object.keys(issues[column]).map((errorType, index) => ( +
+
{type.charAt(0).toUpperCase() + type.slice(1)} Name: {errorType}
+
+ Expected value:{" "} + {issues[column][errorType].ExpectedType || + issues[column][errorType].ExpectedFormat} +
+
+ Rows with {type}:{" "} + + {issues[column][errorType].Rows.slice( + 0, + showAllRowsMap[`${column}_${errorType}`] ? undefined : 15, + ).join(", ")} + {issues[column][errorType].Rows.length > 15 && + !showAllRowsMap[`${column}_${errorType}`] && + "..."} + +
+ {issues[column][errorType].Rows.length > 15 && ( + + )} +
+ ))} +
+ ))} +
+ ); +}; + +UploadIssuesDetail.propTypes = { + type: PropTypes.string.isRequired, + issues: PropTypes.object.isRequired, + totalIssueCount: PropTypes.number.isRequired, + msg: PropTypes.string.isRequired, +}; + +export default UploadIssuesDetail;