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}
+
+ )}
+
+
+
+ {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;