Skip to content

Commit

Permalink
feat: handle invalid csv errors
Browse files Browse the repository at this point in the history
closes #117

Co-Authored-By: steveoh <sgourley@utah.gov>
  • Loading branch information
stdavis and steveoh committed Feb 9, 2022
1 parent dab3df3 commit a6dd420
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 22 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"SGID",
"stringifier",
"tailwindcss",
"trimable",
"UGRC",
"vercel",
"Wkid",
Expand Down
48 changes: 48 additions & 0 deletions src/components/InvalidCsv.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const errors = {
CSV_RECORD_INCONSISTENT_COLUMNS:
'that a record did not contain the same amount of fields as the previous record. Somewhere in your file, a row is missing or has extra field delimiters.',
CSV_INVALID_CLOSING_QUOTE: 'a quote in an unexpected location. Please check the quotes in your CSV file.',
CSV_RECORD_INCONSISTENT_FIELDS_LENGTH:
'that a record did not contain the same amount of fields as the previous record. Somewhere in your file, a row is missing or has extra field delimiters.',
CSV_RECORD_DONT_MATCH_COLUMNS_LENGTH:
'that a record did not contain the same amount of columns. Somewhere in your file, a row is missing or has extra field delimiters.',
CSV_QUOTE_NOT_CLOSED: 'an open quote that was not closed. Please check the quotes in your CSV file.',
};

export const CSV_PARSE_ERROR = 'CSV_PARSE_ERROR';

export default function InvalidCsv({ errorDetails }) {
let message = 'something we have never seen before. Good luck and try again!';

const [code, stack] = errorDetails;

if (Object.keys(errors).includes(code)) {
message = errors[code];

window.ugrc.trackEvent({
category: 'invalid-file-type',
label: code,
});
} else {
window.ugrc.trackEvent({
category: 'unhandled-invalid-file-type',
label: code,
});
}

return (
<div className="w-full px-4 my-4 border rounded shadow border-amber-800 border-3 bg-amber-50">
<h2 className="text-center text-amber-500">Woops, that CSV is not valid</h2>
<p>The file you selected has some problems that you will need to correct before we can continue.</p>
<p>We found {message}</p>
{stack && (
<>
<label htmlFor="stack">Stack trace:</label>
<pre id="stack" className="overflow-auto text-base text-gray-400">
{stack}
</pre>
</>
)}
</div>
);
}
29 changes: 24 additions & 5 deletions src/pages/Data.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Link, useHistory } from 'react-router-dom';
import { useDropzone } from 'react-dropzone';
import { DocumentAddIcon, DocumentRemoveIcon } from '@heroicons/react/outline';
Expand All @@ -8,6 +8,7 @@ import FieldLinker from '../components/FieldLinker.jsx';
import { useGeocodeContext } from '../components/GeocodeContext.js';
import AddressParts from '../components/AddressParts.jsx';
import SampleFieldData from '../components/SampleFieldData.jsx';
import InvalidCsv, { CSV_PARSE_ERROR } from '../components/InvalidCsv.jsx';

const acceptableFileTypes = ['.csv'];

Expand All @@ -25,8 +26,9 @@ const chooseCommonFieldName = (fieldName, fieldsFromFile, commonFieldNames) => {

export default function Data() {
const { geocodeContext, geocodeDispatch } = useGeocodeContext();
const [error, setError] = useState();

const onDrop = async (files, rejectFiles, event) => {
const onDrop = async (files, _, event) => {
if (!files) {
geocodeDispatch({
type: 'RESET',
Expand All @@ -42,7 +44,24 @@ export default function Data() {
});

const file = files[0];
const newSample = await window.ugrc.getSampleFromFile(file.path).catch(handleError);
setError();
let newSample;
try {
newSample = await window.ugrc.getSampleFromFile(file.path);
} catch (e) {
const errorDetails = [];
if (e.message.includes(CSV_PARSE_ERROR)) {
e.message.replace(/\{(.*?)\}/g, (_, code) => {
errorDetails.push(code);
});

setError(errorDetails);

return;
}
handleError(e);
}

const fields = Object.keys(newSample);

geocodeDispatch({
Expand Down Expand Up @@ -78,7 +97,7 @@ export default function Data() {
open();
}
});
}, [open]);
}, [open, geocodeDispatch]);

useEffect(() => {
window.ugrc
Expand Down Expand Up @@ -117,6 +136,7 @@ export default function Data() {
</Link>
<h2>Add your data</h2>
<AddressParts />
{error && <InvalidCsv errorDetails={error} />}
<div
{...getRootProps()}
className="flex items-center justify-center w-full mb-4 bg-gray-100 border border-indigo-800 rounded shadow h-28"
Expand Down Expand Up @@ -151,7 +171,6 @@ export default function Data() {
/>
</>
) : null}

{geocodeContext.data.street && geocodeContext.data.zone ? (
<button className="mt-4" onClick={saveFieldPreferences} type="button">
Next
Expand Down
13 changes: 8 additions & 5 deletions src/pages/Geocoding.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect, useRef } from 'react';
import { useState, useEffect, useRef, useCallback } from 'react';
import { Link, Prompt } from 'react-router-dom';
import humanizeDuration from 'humanize-duration';
import { DocumentTextIcon } from '@heroicons/react/outline';
Expand Down Expand Up @@ -26,9 +26,12 @@ export default function Geocoding() {
window.ugrc.startDrag('ugrc_geocode_results.csv').catch(handleError);
};

const cancel = (reason) => {
window.ugrc.cancelGeocode(reason).catch(handleError);
};
const cancel = useCallback(
(reason) => {
window.ugrc.cancelGeocode(reason).catch(handleError);
},
[handleError]
);

useEffect(() => {
window.ugrc.subscribeToGeocodingUpdates((_, data) => {
Expand All @@ -54,7 +57,7 @@ export default function Geocoding() {
cancel('back');
window.ugrc.unsubscribeFromGeocodingUpdates();
};
}, [geocodeContext, handleError]);
}, [geocodeContext, handleError, cancel]);

const progress = stats.rowsProcessed / stats.totalRows || 0;
const elapsedTime = new Date().getTime() - startTime.current.getTime();
Expand Down
31 changes: 19 additions & 12 deletions src/services/csv.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
const { ipcMain } = require('electron');
const fs = require('fs');
import { parse } from 'csv-parse';
import { CSV_PARSE_ERROR } from '../components/InvalidCsv.jsx';

export const getDataSample = async (filePath) => {
const parser = fs.createReadStream(filePath).pipe(parse({ columns: true, skipEmptyLines: true }));
const parser = parse({ columns: true, skipEmptyLines: true });

//* read the first line to get the file structure
for await (const record of parser) {
return record;
try {
const parsed = fs.createReadStream(filePath).pipe(parser);

//* read the first line to get the file structure
for await (const record of parsed) {
return record;
}
} catch (parseError) {
throw new Error(`${CSV_PARSE_ERROR}: {${parseError.code}} {${parseError.message}}`);
}
};

export const getRecordCount = (filePath) => {
return new Promise((resolve, reject) => {
const rs = fs.createReadStream(filePath);
const parser = parse({ columns: true }, function (err, data) {
if (err) {
reject(err);

return;
}
const parser = parse({ columns: true }, function (parseError, data) {
reject(`${CSV_PARSE_ERROR}: {${parseError.code}} {${parseError.message}}`);

resolve(data.length);
});
rs.pipe(parser);

try {
fs.createReadStream(filePath).pipe(parser);
} catch (parseError) {
throw new Error(`${CSV_PARSE_ERROR}: {${parseError.code}} {${parseError.message}}`);
}
});
};

Expand Down

0 comments on commit a6dd420

Please sign in to comment.