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 ( - <> - - setAnchorEl(null)}> - {props.options.map((value) => ( - {value} - ))} - - - ); -}; 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={() =>