Skip to content

Commit

Permalink
feat: basic table rendering CSV Errors implemented
Browse files Browse the repository at this point in the history
  • Loading branch information
MacQSL committed Dec 17, 2024
1 parent 049a33c commit 85d74a5
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 123 deletions.
26 changes: 25 additions & 1 deletion api/src/errors/http-error.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DatabaseError } from 'pg';
import { CSVError } from '../utils/csv-utils/csv-config-validation.interface';
import { ApiError } from './api-error';
import { BaseError } from './base-error';

Expand All @@ -10,10 +11,20 @@ export enum HTTPErrorType {
INTERNAL_SERVER_ERROR = 'Internal Server Error'
}

export enum HTTPCustomErrorType {
CSV_VALIDATION_ERROR = 'CSV Validation Error'
}

export class HTTPError extends BaseError {
status: number;

constructor(name: HTTPErrorType, status: number, message: string, errors?: (string | object)[], stack?: string) {
constructor(
name: HTTPErrorType | HTTPCustomErrorType,
status: number,
message: string,
errors?: (string | object)[],
stack?: string
) {
super(name, message, errors, stack);

this.status = status;
Expand Down Expand Up @@ -85,6 +96,19 @@ export class HTTP500 extends HTTPError {
}
}

/**
* A HTTP `422 CSV Validation Error` error.
*
* @export
* @class CSVValidationError
* @extends {HTTPError}
*/
export class HTTP422CSVValidationError extends HTTPError {
constructor(message: string, errors: CSVError[]) {
super(HTTPCustomErrorType.CSV_VALIDATION_ERROR, 422, message, errors);
}
}

/**
* Ensures that the incoming error is converted into an `HTTPError` if it is not one already.
* If `error` is a `HTTPError`, then change nothing and return it.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,10 @@ import { v4 } from 'uuid';
import { WorkSheet } from 'xlsx';
import { IDBConnection } from '../../../database/db';
import { ApiGeneralError } from '../../../errors/api-error';
import { HTTP422CSVValidationError } from '../../../errors/http-error';
import { CSVConfigUtils } from '../../../utils/csv-utils/csv-config-utils';
import { validateCSVWorksheet } from '../../../utils/csv-utils/csv-config-validation';
import {
CSVConfig,
CSVHeaderConfig,
CSVRowValidated,
CSVValidationError
} from '../../../utils/csv-utils/csv-config-validation.interface';
import { CSVConfig, CSVHeaderConfig, CSVRowValidated } from '../../../utils/csv-utils/csv-config-validation.interface';
import { getDescriptionCellValidator, getTsnCellValidator } from '../../../utils/csv-utils/csv-header-configs';
import { getLogger } from '../../../utils/logger';
import { NestedRecord } from '../../../utils/nested-record';
Expand Down Expand Up @@ -98,7 +94,7 @@ export class ImportCrittersService extends DBService {
const { errors, rows } = validateCSVWorksheet(this.worksheet, config);

if (errors.length) {
throw new CSVValidationError('Failed to validate Critter CSV', errors);
throw new HTTP422CSVValidationError('Failed to validate Critter CSV', errors);
}

const payloads = this._getImportPayloads(rows);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import { WorkSheet } from 'xlsx';
import { IDBConnection } from '../../../database/db';
import { HTTP422CSVValidationError } from '../../../errors/http-error';
import { CSVConfigUtils } from '../../../utils/csv-utils/csv-config-utils';
import { validateCSVWorksheet } from '../../../utils/csv-utils/csv-config-validation';
import {
CSVConfig,
CSVValidationError,
CSV_ERROR_MESSAGE
} from '../../../utils/csv-utils/csv-config-validation.interface';
import { CSVConfig, CSV_ERROR_MESSAGE } from '../../../utils/csv-utils/csv-config-validation.interface';
import {
getDescriptionCellValidator,
getTimeCellSetter,
Expand Down Expand Up @@ -97,7 +94,7 @@ export class ImportMarkingsService extends DBService {
const { errors, rows } = validateCSVWorksheet(this.worksheet, config);

if (errors.length) {
throw new CSVValidationError(CSV_ERROR_MESSAGE, errors);
throw new HTTP422CSVValidationError(CSV_ERROR_MESSAGE, errors);
}

const markings = rows.map((row) => ({
Expand Down
15 changes: 0 additions & 15 deletions api/src/utils/csv-utils/csv-config-validation.interface.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,6 @@
import { HTTPError, HTTPErrorType } from '../../errors/http-error';

export const CSV_ERROR_MESSAGE =
'CSV contains validation errors. Please check for formatting issues, missing fields, or invalid values and try again.';

/**
* A HTTP `422 Unprocessable Entity` error specific to CSV validation.
*
* @export
* @class CSVValidationError
* @extends {HTTPError}
*/
export class CSVValidationError extends HTTPError {
constructor(message: string, errors: CSVError[]) {
super('CSV Validation Error' as HTTPErrorType, 422, message, errors);
}
}

/**
* The CSV configuration interface
*
Expand Down
70 changes: 70 additions & 0 deletions app/src/components/csv/CSVErrorsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { GridColDef } from '@mui/x-data-grid';
import { StyledDataGrid } from 'components/data-grid/StyledDataGrid';
import { useMemo } from 'react';
import { CSVError } from 'utils/file-utils';
import { v4 } from 'uuid';

interface CSVErrorsTableProps {
errors: CSVError[];
}

export const CSVErrorsTable = (props: CSVErrorsTableProps) => {
const columns: GridColDef[] = [
{
field: 'row',
headerName: 'Row',
description: 'Row number in the CSV file',
type: 'number'
},
{
field: 'header',
headerName: 'Header',
description: 'Column header in the CSV file',
minWidth: 200,
type: 'string'
},
{
field: 'error',
headerName: 'Error',
description: 'The error message',
flex: 2,
type: 'string'
},
{
field: 'solution',
headerName: 'Solution',
description: 'The solution to the error',
flex: 2,
type: 'string'
},
{
field: 'cell',
headerName: 'Cell',
description: 'The cell value in the CSV file',
type: 'string'
}
];

const rows = useMemo(() => {
return props.errors.map((error) => {
return {
id: v4(),
...error
};
});
}, [props.errors]);

return (
<StyledDataGrid
noRowsMessage={'No validation errors found'}
autoHeight
rows={rows}
getRowId={(row) => row.id}
columns={columns}
pageSizeOptions={[5, 10, 20, 100]}
rowSelection={false}
checkboxSelection={false}
sortingOrder={['asc', 'desc']}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Box } from '@mui/material';
import Button from '@mui/material/Button';
import { CSVErrorsTable } from 'components/csv/CSVErrorsTable';
import HorizontalSplitFormComponent from 'components/fields/HorizontalSplitFormComponent';
import { PropsWithChildren, useState } from 'react';
import { CSVError } from 'utils/file-utils';

interface CSVDropzoneSectionProps {
title: string;
summary: string;
onDownloadTemplate: () => void;
errors: CSVError[];
}

export const CSVDropzoneSection = (props: PropsWithChildren<CSVDropzoneSectionProps>) => {
const [showErrorsTable, setShowErrorsTable] = useState(false);

return (
<HorizontalSplitFormComponent title={props.title} summary={props.summary}>
<Box sx={{ display: 'flex', flexDirection: 'column' }} gap={2}>
<Box sx={{ display: 'flex', ml: 'auto' }}>
<Button
sx={{ textTransform: 'none', fontWeight: 'regular' }}
variant="outlined"
size="small"
onClick={props.onDownloadTemplate}>
Download Template
</Button>
{props.errors.length > 0 && (
<Button
sx={{ ml: 2, textTransform: 'none', fontWeight: 'regular' }}
variant="contained"
color="error"
size="small"
onClick={() => setShowErrorsTable((s) => !s)}>
View CSV Errors
</Button>
)}
</Box>
{props.children}
{showErrorsTable && <CSVErrorsTable errors={props.errors} />}
</Box>
</HorizontalSplitFormComponent>
);
};
Loading

0 comments on commit 85d74a5

Please sign in to comment.