diff --git a/api/src/services/import-services/telemetry/telemetry-header-configs.test.ts b/api/src/services/import-services/telemetry/telemetry-header-configs.test.ts
index 97871a7510..346e46b934 100644
--- a/api/src/services/import-services/telemetry/telemetry-header-configs.test.ts
+++ b/api/src/services/import-services/telemetry/telemetry-header-configs.test.ts
@@ -45,8 +45,8 @@ describe('TelemetryHeaderConfigs', () => {
const result = cellValidator({ cell: 5555 } as CSVParams);
expect(result).to.deep.equal([
{
- error: 'Device not found in the survey deployments',
- solution: 'Check the serial number and vendor are correct and the device is deployed in the survey'
+ error: 'Device not found in deployments',
+ solution: 'Check that the serial number and vendor match a deployment in the Survey'
}
]);
});
diff --git a/api/src/services/import-services/telemetry/telemetry-header-configs.ts b/api/src/services/import-services/telemetry/telemetry-header-configs.ts
index aad66c8f03..3fd151ed5f 100644
--- a/api/src/services/import-services/telemetry/telemetry-header-configs.ts
+++ b/api/src/services/import-services/telemetry/telemetry-header-configs.ts
@@ -59,8 +59,8 @@ export const getTelemetrySerialCellValidator = (
if (!deployment) {
return [
{
- error: `Device not found in the survey deployments`,
- solution: `Check the serial number and vendor are correct and the device is deployed in the survey`
+ error: `Device not found in deployments`,
+ solution: `Check that the serial number and vendor match a deployment in the Survey`
}
];
}
diff --git a/api/src/utils/csv-utils/csv-config-validation.test.ts b/api/src/utils/csv-utils/csv-config-validation.test.ts
index 776cd47e67..345d829609 100644
--- a/api/src/utils/csv-utils/csv-config-validation.test.ts
+++ b/api/src/utils/csv-utils/csv-config-validation.test.ts
@@ -93,7 +93,7 @@ describe('csv-config-validation', () => {
errors: [
{
error: 'A required column is missing',
- solution: `Add all required columns to the file.`,
+ solution: `Add the ALIAS column to the file.`,
header: 'ALIAS',
values: ['ALIAS', 'ALIAS_2'],
cell: null,
@@ -217,7 +217,7 @@ describe('csv-config-validation', () => {
{
row: 1,
error: 'A required column is missing',
- solution: `Add all required columns to the file.`,
+ solution: `Add the ALIAS column to the file.`,
header: 'ALIAS',
values: ['ALIAS'],
cell: null
@@ -247,7 +247,7 @@ describe('csv-config-validation', () => {
{
row: 1,
error: 'An unknown column is included in the file',
- solution: `Remove extra columns from the file.`,
+ solution: `Remove the UNKNOWN_HEADER column from the file.`,
header: 'UNKNOWN_HEADER',
cell: null,
values: null
diff --git a/api/src/utils/csv-utils/csv-config-validation.ts b/api/src/utils/csv-utils/csv-config-validation.ts
index ef27392863..f2d72c787c 100644
--- a/api/src/utils/csv-utils/csv-config-validation.ts
+++ b/api/src/utils/csv-utils/csv-config-validation.ts
@@ -127,7 +127,7 @@ export const validateCSVHeaders = (worksheet: WorkSheet, config: CSVConfig): CSV
if (!headerConfig.optional && !worksheetHasStaticHeader) {
csvErrors.push({
error: 'A required column is missing',
- solution: `Add all required columns to the file.`,
+ solution: `Add the ${staticHeader} column to the file.`,
values: [staticHeader, ...config.staticHeadersConfig[staticHeader].aliases],
header: staticHeader,
cell: null,
@@ -141,7 +141,7 @@ export const validateCSVHeaders = (worksheet: WorkSheet, config: CSVConfig): CSV
for (const unknownHeader of configUtils.worksheetDynamicHeaders) {
csvErrors.push({
error: 'An unknown column is included in the file',
- solution: `Remove extra columns from the file.`,
+ solution: `Remove the ${unknownHeader} column from the file.`,
values: null,
header: unknownHeader,
cell: null,
diff --git a/app/src/components/alert/AlertBar.tsx b/app/src/components/alert/AlertBar.tsx
index fff13a5ff2..d1b4814dcb 100644
--- a/app/src/components/alert/AlertBar.tsx
+++ b/app/src/components/alert/AlertBar.tsx
@@ -1,11 +1,13 @@
import Alert, { AlertProps } from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle';
+import Typography from '@mui/material/Typography';
interface IAlertBarProps extends AlertProps {
severity: 'error' | 'warning' | 'info' | 'success';
variant: 'filled' | 'outlined' | 'standard';
title: string;
text: string | JSX.Element;
+ ornament?: JSX.Element;
}
/**
@@ -15,7 +17,7 @@ interface IAlertBarProps extends AlertProps {
* @returns
*/
const AlertBar = (props: IAlertBarProps) => {
- const { severity, variant, title, text, ...alertProps } = props;
+ const { severity, variant, title, text, ornament, ...alertProps } = props;
const defaultProps = {
severity: 'success',
@@ -30,8 +32,13 @@ const AlertBar = (props: IAlertBarProps) => {
{...alertProps}
variant={variant}
severity={severity}
- sx={{ flex: '1 1 auto', ...alertProps.sx }}>
- {title}
+ sx={{ flex: '1 1 auto', '& .MuiAlert-message': { flex: '1 1 auto' }, ...alertProps.sx }}>
+
+ {title}
+
+ {ornament}
+
+
{text}
);
diff --git a/app/src/components/csv/CSVDropzoneSection.tsx b/app/src/components/csv/CSVDropzoneSection.tsx
index 030773bb93..5eb8d1c46a 100644
--- a/app/src/components/csv/CSVDropzoneSection.tsx
+++ b/app/src/components/csv/CSVDropzoneSection.tsx
@@ -1,9 +1,8 @@
-import { Box } from '@mui/material';
+import { Box, Typography } from '@mui/material';
import Button from '@mui/material/Button';
-import { CSVErrorsTableContainer } from 'components/csv/CSVErrorsTableContainer';
-import HorizontalSplitFormComponent from 'components/fields/HorizontalSplitFormComponent';
import { PropsWithChildren } from 'react';
import { CSVError } from 'utils/csv-utils';
+import { CSVErrorsCardStackContainer } from './CSVErrorsCardStackContainer';
interface CSVDropzoneSectionProps {
title: string;
@@ -21,20 +20,22 @@ interface CSVDropzoneSectionProps {
*/
export const CSVDropzoneSection = (props: PropsWithChildren) => {
return (
-
-
-
-
-
- {props.children}
- {props.errors.length > 0 ? : null}
+
+
+ {props.title}
+
-
+
+ {props.summary}
+
+ {props.children}
+ {props.errors.length > 0 ? : null}
+
);
};
diff --git a/app/src/components/csv/CSVErrorsCardStack.tsx b/app/src/components/csv/CSVErrorsCardStack.tsx
new file mode 100644
index 0000000000..21e12a6a3c
--- /dev/null
+++ b/app/src/components/csv/CSVErrorsCardStack.tsx
@@ -0,0 +1,109 @@
+import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
+import Icon from '@mdi/react';
+import IconButton from '@mui/material/IconButton';
+import Stack from '@mui/material/Stack';
+import Typography from '@mui/material/Typography';
+import AlertBar from 'components/alert/AlertBar';
+import { useMemo, useState } from 'react';
+import { CSVError } from 'utils/csv-utils';
+import { v4 } from 'uuid';
+
+const MAX_ERRORS_SHOWN = 10;
+
+interface CSVErrorsCardStackProps {
+ errors: CSVError[];
+}
+
+/**
+ * Returns a stack of CSV errors with information about solutions and pagination
+ *
+ * @param {CSVErrorsCardStackProps} props
+ * @returns {*}
+ */
+export const CSVErrorsCardStack = (props: CSVErrorsCardStackProps) => {
+ const [currentPage, setCurrentPage] = useState(0);
+
+ const pageCount = Math.ceil(props.errors.length / MAX_ERRORS_SHOWN);
+
+ const rows: (CSVError & { id: string })[] = useMemo(() => {
+ return props.errors.slice(currentPage * MAX_ERRORS_SHOWN, (currentPage + 1) * MAX_ERRORS_SHOWN).map((error) => {
+ return {
+ id: v4(),
+ ...error
+ };
+ });
+ }, [props.errors, currentPage]);
+
+ const handleNextPage = () => {
+ if ((currentPage + 1) * MAX_ERRORS_SHOWN < props.errors.length) {
+ setCurrentPage(currentPage + 1);
+ }
+ };
+
+ const handlePreviousPage = () => {
+ if (currentPage > 0) {
+ setCurrentPage(currentPage - 1);
+ }
+ };
+
+ return (
+
+ {rows.map((error) => {
+ return (
+
+ {error.solution}
+
+
+
+ Row
+
+ {error.row ?? 'N/A'}
+
+
+
+ Column
+
+ {error.header ?? 'N/A'}
+
+
+
+ Value
+
+ {error.cell ?? 'N/A'}
+
+ {error.cell && error.values && (
+
+
+ Allowed Values
+
+ {error.values.join(', ')}
+
+ )}
+
+
+ }
+ />
+ );
+ })}
+ {props.errors.length > MAX_ERRORS_SHOWN && (
+
+
+
+
+
+ Page {currentPage + 1} of {pageCount}
+
+
+
+
+
+ )}
+
+ );
+};
diff --git a/app/src/components/csv/CSVErrorsTableContainer.tsx b/app/src/components/csv/CSVErrorsCardStackContainer.tsx
similarity index 52%
rename from app/src/components/csv/CSVErrorsTableContainer.tsx
rename to app/src/components/csv/CSVErrorsCardStackContainer.tsx
index 954b3b8d74..d9c9b4d3ac 100644
--- a/app/src/components/csv/CSVErrorsTableContainer.tsx
+++ b/app/src/components/csv/CSVErrorsCardStackContainer.tsx
@@ -1,10 +1,10 @@
-import { Divider, Paper, Toolbar, Typography } from '@mui/material';
+import { Toolbar, Typography } from '@mui/material';
import { Box, Stack } from '@mui/system';
import { ReactElement } from 'react';
import { CSVError } from 'utils/csv-utils';
-import { CSVErrorsTable } from './CSVErrorsTable';
+import { CSVErrorsCardStack } from './CSVErrorsCardStack';
-interface CSVErrorsTableContainerProps {
+interface CSVErrorsCardStackContainerProps {
errors: CSVError[];
title?: ReactElement;
}
@@ -12,36 +12,29 @@ interface CSVErrorsTableContainerProps {
/**
* Renders a CSV errors table with toolbar.
*
- * @param {CSVErrorsTableContainerProps} props
+ * @param {CSVErrorsCardStackContainerProps} props
* @returns {*} {JSX.Element}
*/
-export const CSVErrorsTableContainer = (props: CSVErrorsTableContainerProps) => {
+export const CSVErrorsCardStackContainer = (props: CSVErrorsCardStackContainerProps) => {
return (
-
-
+
+
{props.title ?? (
- CSV Errors Detected
+ Errors
({props.errors.length})
)}
-
-
+
-
+
);
};
diff --git a/app/src/components/csv/CSVErrorsTable.tsx b/app/src/components/csv/CSVErrorsTable.tsx
deleted file mode 100644
index 9ff5cff794..0000000000
--- a/app/src/components/csv/CSVErrorsTable.tsx
+++ /dev/null
@@ -1,99 +0,0 @@
-import { GridColDef } from '@mui/x-data-grid';
-import { StyledDataGrid } from 'components/data-grid/StyledDataGrid';
-import { useMemo } from 'react';
-import { CSVError } from 'utils/csv-utils';
-import { v4 } from 'uuid';
-import { CSVErrorsTableOptionsMenu } from './CSVErrorsTableOptionsMenu';
-
-interface CSVErrorsTableProps {
- errors: CSVError[];
-}
-
-/**
- * Renders a CSV errors table.
- *
- * @param {CSVErrorsTableProps} props
- * @returns {*} {JSX.Element}
- */
-export const CSVErrorsTable = (props: CSVErrorsTableProps) => {
- const columns: GridColDef[] = [
- {
- field: 'row',
- headerName: 'Row',
- description: 'Row number in the CSV file',
- minWidth: 85
- },
- {
- field: 'header',
- headerName: 'Header',
- description: 'Column header in the CSV file',
- minWidth: 150,
- maxWidth: 250,
- renderCell: (params) => {
- return params.value?.toUpperCase();
- }
- },
- {
- field: 'cell',
- headerName: 'Cell',
- description: 'The cell value in the CSV file',
- minWidth: 85
- },
- {
- field: 'error',
- headerName: 'Error',
- description: 'The error message',
- flex: 1,
- minWidth: 250,
- resizable: true
- },
- {
- field: 'solution',
- headerName: 'Solution',
- description: 'The solution to the error',
- flex: 1,
- minWidth: 250,
- resizable: true
- },
- {
- field: 'values',
- headerName: 'Options',
- description: 'The applicable cell values',
- minWidth: 85,
- renderCell: (params) => {
- return params.value?.length ? : 'N/A';
- }
- }
- ];
-
- const rows = useMemo(() => {
- return props.errors.map((error) => {
- return {
- id: v4(),
- ...error
- };
- });
- }, [props.errors]);
-
- return (
- 'auto'}
- rows={rows}
- getRowId={(row) => row.id}
- columns={columns}
- pageSizeOptions={[5, 10, 25, 50]}
- rowSelection={false}
- checkboxSelection={false}
- sortingOrder={['asc', 'desc']}
- initialState={{
- pagination: {
- paginationModel: {
- pageSize: 5
- }
- }
- }}
- />
- );
-};
diff --git a/app/src/components/csv/CSVErrorsTableOptionsMenu.tsx b/app/src/components/csv/CSVErrorsTableOptionsMenu.tsx
deleted file mode 100644
index 78f18a03a4..0000000000
--- a/app/src/components/csv/CSVErrorsTableOptionsMenu.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { mdiChevronDown } from '@mdi/js';
-import Icon from '@mdi/react';
-import { Button, Menu, MenuItem } from '@mui/material';
-import { useState } from 'react';
-
-interface CSVErrorsTableOptionsMenuProps {
- options: string[];
-}
-
-/**
- * Renders a CSV errors table options menu.
- *
- * @param {CSVErrorsTableOptionsMenuProps} props
- * @returns {*} {JSX.Element}
- */
-export const CSVErrorsTableOptionsMenu = (props: CSVErrorsTableOptionsMenuProps) => {
- const [anchorEl, setAnchorEl] = useState(null);
-
- return (
- <>
-
-
- >
- );
-};
diff --git a/app/src/components/csv/CSVSingleImportDialog.tsx b/app/src/components/csv/CSVSingleImportDialog.tsx
index 9c149db552..709af2d87e 100644
--- a/app/src/components/csv/CSVSingleImportDialog.tsx
+++ b/app/src/components/csv/CSVSingleImportDialog.tsx
@@ -1,5 +1,5 @@
import LoadingButton from '@mui/lab/LoadingButton/LoadingButton';
-import { Box, Dialog, DialogActions, DialogContent, Divider, Typography, useMediaQuery, useTheme } from '@mui/material';
+import { Box, Dialog, DialogActions, DialogContent, Divider, useMediaQuery, useTheme } from '@mui/material';
import { AxiosProgressEvent } from 'axios';
import { UploadFileStatus } from 'components/file-upload/FileUploadItem';
import { FileUploadSingleItem } from 'components/file-upload/FileUploadSingleItem';
@@ -90,24 +90,27 @@ export const CSVSingleImportDialog = (props: CSVSingleImportDialogProps) => {
// Wait for the complete status to be rendered + 500ms before closing the dialog
await waitForRenderCycle(500);
+ // Show a success snackbar message
+ dialogContext.setSnackbar({
+ open: true,
+ snackbarAutoCloseMs: 2000,
+ snackbarMessage: 'Successfully imported telemetry'
+ });
+
handleClose();
} catch (err) {
if (err instanceof Error) {
setError(err);
}
- setUploadStatus(UploadFileStatus.FAILED);
- } finally {
- // Show a success snackbar message
+ // Show a failure snackbar message
dialogContext.setSnackbar({
open: true,
snackbarAutoCloseMs: 2000,
- snackbarMessage: (
-
- {uploadStatus === UploadFileStatus.FAILED ? 'CSV failed to import' : 'CSV imported'}
-
- )
+ snackbarMessage: 'Failed to import telemetry'
});
+
+ setUploadStatus(UploadFileStatus.FAILED);
}
};
diff --git a/app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx b/app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx
index 3416535444..dbf6ca4672 100644
--- a/app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx
+++ b/app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx
@@ -76,8 +76,8 @@ export const TelemetryTableContainer = () => {
<>
setShowImportDialog(false)}
onImport={handleImportTelemetryCSV}
onDownloadTemplate={() =>