Skip to content

Commit

Permalink
feat: created component for rendering single CSV import dialogs
Browse files Browse the repository at this point in the history
  • Loading branch information
MacQSL committed Dec 23, 2024
1 parent b0a9d4b commit 0bee76d
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 134 deletions.
149 changes: 149 additions & 0 deletions app/src/components/csv/CSVSingleImportDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import LoadingButton from '@mui/lab/LoadingButton/LoadingButton';
import { Box, Dialog, DialogActions, DialogContent, Divider, Typography } from '@mui/material';
import { AxiosProgressEvent } from 'axios';
import { UploadFileStatus } from 'components/file-upload/FileUploadItem';
import { FileUploadSingleItem } from 'components/file-upload/FileUploadSingleItem';
import { DialogContext } from 'contexts/dialogContext';
import { useContext, useState } from 'react';
import { isCSVValidationError } from 'utils/csv-utils';
import { getAxiosProgress } from 'utils/Utils';
import { CSVDropzoneSection } from './CSVDropzoneSection';

interface CSVSingleImportDialogProps {
open: boolean;
dialogTitle: string;
dialogSummary: string;
onCancel: () => void;
onImport: (file: File, onProgress: (progressEvent: AxiosProgressEvent) => void) => Promise<void>;
onDownloadTemplate: () => void;
}

/**
* Dialog for importing a single CSV file.
*
* @param {CSVSingleImportDialogProps} props
* @return {*} {JSX.Element}
*/
export const CSVSingleImportDialog = (props: CSVSingleImportDialogProps) => {
const dialogContext = useContext(DialogContext);

Check warning on line 28 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L27-L28

Added lines #L27 - L28 were not covered by tests

// Dialog and import state
const [file, setFile] = useState<File | null>(null);
const [uploadStatus, setUploadStatus] = useState<UploadFileStatus>(UploadFileStatus.STAGED);
const [progress, setProgress] = useState<number>(0);
const [error, setError] = useState<Error | null>(null);

Check warning on line 34 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L31-L34

Added lines #L31 - L34 were not covered by tests

const isUploading = uploadStatus === UploadFileStatus.UPLOADING || uploadStatus === UploadFileStatus.FINISHING_UPLOAD;
const disableImportButton =
isUploading || !file || uploadStatus === UploadFileStatus.FAILED || uploadStatus === UploadFileStatus.COMPLETE;

/**
* Cancel the dialog and reset the file import state
*
* @returns {void}
*/
const handleCancel = (): void => {
props.onCancel();
handleResetFileImport();

Check warning on line 47 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L45-L47

Added lines #L45 - L47 were not covered by tests
};

/**
* Reset the file import state, independent of the dialog state
*
* @returns {void}
*/
const handleResetFileImport = (): void => {
setFile(null);
setUploadStatus(UploadFileStatus.STAGED);
setProgress(0);
setError(null);

Check warning on line 59 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L55-L59

Added lines #L55 - L59 were not covered by tests
};

/**
* Import the CSV file and update the status accordingly
*
* @param {File | null} file The CSV file to import
* @returns {Promise<void>}
*/
const handleCSVFileImport = async (file: File | null): Promise<void> => {

Check warning on line 68 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L68

Added line #L68 was not covered by tests
if (!file) {
return;

Check warning on line 70 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L70

Added line #L70 was not covered by tests
}

try {
setUploadStatus(UploadFileStatus.UPLOADING);

Check warning on line 74 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L73-L74

Added lines #L73 - L74 were not covered by tests

await props.onImport(file, (progressEvent) => {

Check warning on line 76 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L76

Added line #L76 was not covered by tests
// Update the progress state from the Axios progress event
setProgress(getAxiosProgress(progressEvent));

Check warning on line 78 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L78

Added line #L78 was not covered by tests

if (progressEvent.loaded === progressEvent.total) {
setUploadStatus(UploadFileStatus.FINISHING_UPLOAD);

Check warning on line 81 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L81

Added line #L81 was not covered by tests
}
});

setUploadStatus(UploadFileStatus.COMPLETE);

Check warning on line 85 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L85

Added line #L85 was not covered by tests

// Show a success snackbar message
dialogContext.setSnackbar({

Check warning on line 88 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L88

Added line #L88 was not covered by tests
open: true,
snackbarMessage: (
<Typography variant="body2" component="div">
CSV imported successfully.
</Typography>
)
});
} catch (err) {
if (err instanceof Error) {
setError(err);

Check warning on line 98 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L98

Added line #L98 was not covered by tests
}

setUploadStatus(UploadFileStatus.FAILED);

Check warning on line 101 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L101

Added line #L101 was not covered by tests
}
};

if (!props.open) {
return null;

Check warning on line 106 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L106

Added line #L106 was not covered by tests
}

return (

Check warning on line 109 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L109

Added line #L109 was not covered by tests
<Dialog open={props.open} maxWidth={false}>
<DialogContent sx={{ mt: 2 }}>
<Box>
<CSVDropzoneSection
title={props.dialogTitle}
summary={props.dialogSummary}
onDownloadTemplate={props.onDownloadTemplate}
errors={isCSVValidationError(error) ? error.errors : []}>
<FileUploadSingleItem
file={file}
status={uploadStatus}
error={error?.message}
onError={(message) => setError(new Error(message))}

Check warning on line 122 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L122

Added line #L122 was not covered by tests
progress={progress}
onFile={(file) => setFile(file)}

Check warning on line 124 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L124

Added line #L124 was not covered by tests
onCancel={handleResetFileImport}
/>
</CSVDropzoneSection>
</Box>
</DialogContent>
<Divider />

<DialogActions>
<LoadingButton
onClick={() => {
handleCSVFileImport(file);

Check warning on line 135 in app/src/components/csv/CSVSingleImportDialog.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/components/csv/CSVSingleImportDialog.tsx#L135

Added line #L135 was not covered by tests
}}
color="primary"
variant="contained"
disabled={disableImportButton}>
Import
</LoadingButton>

<LoadingButton onClick={handleCancel} color="primary" variant="outlined">
{uploadStatus === UploadFileStatus.COMPLETE ? 'Close' : 'Cancel'}
</LoadingButton>
</DialogActions>
</Dialog>
);
};
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import { getCSVTemplate } from 'utils/csv-utils';
import { CSVTemplateString, getCSVTemplate } from 'utils/csv-utils';

/**
* Get CSV template for measurements.
*
* @returns {string} Encoded CSV template
* @returns {CSVTemplateString} Encoded CSV template
*/
export const getMeasurementsCSVTemplate = (): string => {
export const getMeasurementsCSVTemplate = (): CSVTemplateString => {

Check warning on line 8 in app/src/features/surveys/animals/profile/captures/import-captures/utils/templates.ts

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/import-captures/utils/templates.ts#L8

Added line #L8 was not covered by tests
return getCSVTemplate(['ALIAS', 'CAPTURE_DATE', 'CAPTURE_TIME']);
};

/**
* Get CSV template for captures.
*
* @returns {string} Encoded CSV template
* @returns {CSVTemplateString} Encoded CSV template
*/
export const getCapturesCSVTemplate = (): string => {
export const getCapturesCSVTemplate = (): CSVTemplateString => {

Check warning on line 17 in app/src/features/surveys/animals/profile/captures/import-captures/utils/templates.ts

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/import-captures/utils/templates.ts#L17

Added line #L17 was not covered by tests
return getCSVTemplate([
'ALIAS',
'CAPTURE_DATE',
Expand All @@ -33,9 +33,9 @@ export const getCapturesCSVTemplate = (): string => {
/**
* Get CSV template for markings.
*
* @returns {string} Encoded CSV template
* @returns {CSVTemplateString} Encoded CSV template
*/
export const getMarkingsCSVTemplate = (): string => {
export const getMarkingsCSVTemplate = (): CSVTemplateString => {

Check warning on line 38 in app/src/features/surveys/animals/profile/captures/import-captures/utils/templates.ts

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/import-captures/utils/templates.ts#L38

Added line #L38 was not covered by tests
return getCSVTemplate([
'ALIAS',
'CAPTURE_DATE',
Expand All @@ -48,3 +48,12 @@ export const getMarkingsCSVTemplate = (): string => {
'COMMENT'
]);
};

/**
* Get CSV template for telemetry.
*
* @returns {CSVTemplateString} Encoded CSV template
*/
export const getTelemetryCSVTemplate = (): CSVTemplateString => {
return getCSVTemplate(['VENDOR', 'SERIAL', 'LATITUDE', 'LONGITUDE', 'DATE', 'TIME']);

Check warning on line 58 in app/src/features/surveys/animals/profile/captures/import-captures/utils/templates.ts

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/animals/profile/captures/import-captures/utils/templates.ts#L57-L58

Added lines #L57 - L58 were not covered by tests
};
138 changes: 16 additions & 122 deletions app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,23 @@ import Stack from '@mui/material/Stack';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import axios, { AxiosProgressEvent } from 'axios';
import { CSVErrorsTableContainer } from 'components/csv/CSVErrorsTableContainer';
import { CSVSingleImportDialog } from 'components/csv/CSVSingleImportDialog';
import DataGridValidationAlert from 'components/data-grid/DataGridValidationAlert';
import YesNoDialog from 'components/dialog/YesNoDialog';
import { UploadFileStatus } from 'components/file-upload/FileUploadItem';
import { FileUploadSingleItem } from 'components/file-upload/FileUploadSingleItem';
import { TelemetryTableI18N } from 'constants/i18n';
import { DialogContext, ISnackbarProps } from 'contexts/dialogContext';
import { SurveyContext } from 'contexts/surveyContext';
import { getTelemetryCSVTemplate } from 'features/surveys/animals/profile/captures/import-captures/utils/templates';
import { TelemetryDeviceKeysButton } from 'features/surveys/telemetry/manage/device-keys/TelemetryDeviceKeysButton';
import { TelemetryTable } from 'features/surveys/telemetry/table/TelemetryTable';
import { APIError } from 'hooks/api/useAxios';
import { useBiohubApi } from 'hooks/useBioHubApi';
import { useTelemetryTableContext } from 'hooks/useContext';
import { useContext, useDeferredValue, useState } from 'react';
import { CSVError, isCSVValidationError } from 'utils/csv-utils';
import { getAxiosProgress } from 'utils/Utils';
import { downloadFile } from 'utils/file-utils';

export const TelemetryTableContainer = () => {
const biohubApi = useBiohubApi();

const dialogContext = useContext(DialogContext);
//const dialogContext = useContext(DialogContext);
const telemetryTableContext = useTelemetryTableContext();
const surveyContext = useContext(SurveyContext);

Expand All @@ -47,23 +43,13 @@ export const TelemetryTableContainer = () => {

// Telemetry import dialog state
const [showImportDialog, setShowImportDialog] = useState(false);

Check warning on line 45 in app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx#L45

Added line #L45 was not covered by tests
const [importCSVErrors, setImportCSVErrors] = useState<CSVError[]>([]);
const [file, setFile] = useState<File | null>(null);
const [uploadStatus, setUploadStatus] = useState<UploadFileStatus>(UploadFileStatus.STAGED);
const [progress, setProgress] = useState<number>(0);

const isUploading = uploadStatus === UploadFileStatus.UPLOADING || uploadStatus === UploadFileStatus.FINISHING_UPLOAD;
const disableImportButton = isUploading || !file || uploadStatus === UploadFileStatus.FAILED;
const cancelToken = axios.CancelToken.source();

Check warning on line 47 in app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx#L47

Added line #L47 was not covered by tests

const deferredUnsavedChanges = useDeferredValue(telemetryTableContext.hasUnsavedChanges);

const numSelectedRows = telemetryTableContext.rowSelectionModel.length;

const showSnackBar = (textDialogProps?: Partial<ISnackbarProps>) => {
dialogContext.setSnackbar({ ...textDialogProps, open: true });
};

const handleCloseContextMenu = () => {
setContextMenuAnchorEl(null);
};
Expand All @@ -72,128 +58,36 @@ export const TelemetryTableContainer = () => {
setColumnVisibilityMenuAnchorEl(null);
};

const handleResetFileImport = () => {
setFile(null);
setImportCSVErrors([]);
setUploadStatus(UploadFileStatus.STAGED);
setProgress(0);
};

/**
* Handle the close of the import dialog.
*
* @returns {*} {void}
*/
const handleCloseImportDialog = () => {
setShowImportDialog(false);
handleResetFileImport();
};

/**
* Handle the import of telemetry data.
*
* Note: This will render a table in the dialog if CSVErrors are present
* @param {File} file
* @returns {*} {void}
*/
const handleImportTelemetry = async (file: File) => {
const handleImportTelemetryCSV = async (file: File, onProgress: (progressEvent: AxiosProgressEvent) => void) => {

Check warning on line 61 in app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx#L61

Added line #L61 was not covered by tests
try {
setUploadStatus(UploadFileStatus.UPLOADING);

await biohubApi.telemetry.importManualTelemetryCSV(

Check warning on line 63 in app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx#L63

Added line #L63 was not covered by tests
surveyContext.projectId,
surveyContext.surveyId,
file,
cancelToken,
async (progressEvent: AxiosProgressEvent) => {
setProgress(getAxiosProgress(progressEvent));
if (progressEvent.loaded === progressEvent.total) {
setUploadStatus(UploadFileStatus.FINISHING_UPLOAD);
}
}
onProgress
);

setShowImportDialog(false);

setProcessingRecords(true);

showSnackBar({
snackbarMessage: (
<Typography variant="body2" component="div">
Telemetry imported successfully.
</Typography>
)
});

telemetryTableContext.refreshRecords().then(() => {
setProcessingRecords(false);
});
setUploadStatus(UploadFileStatus.COMPLETE);
} catch (error) {
setUploadStatus(UploadFileStatus.FAILED);

if (isCSVValidationError(error)) {
setImportCSVErrors(error.errors);
return;
}

const apiError = error as APIError;

dialogContext.setErrorDialog({
dialogTitle: TelemetryTableI18N.importRecordsErrorDialogTitle,
dialogText: TelemetryTableI18N.importRecordsErrorDialogText,
dialogError: apiError.message,
dialogErrorDetails: apiError.errors,
open: true,
onClose: () => {
setProcessingRecords(false);
dialogContext.setErrorDialog({ open: false });
},
onOk: () => {
setProcessingRecords(false);
dialogContext.setErrorDialog({ open: false });
}
});
telemetryTableContext.refreshRecords();

Check warning on line 73 in app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx#L73

Added line #L73 was not covered by tests
} finally {
setProcessingRecords(false);

Check warning on line 75 in app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx#L75

Added line #L75 was not covered by tests
}
};

return (
<>
<YesNoDialog
dialogTitle={'Import Telemetry'}
dialogText={''}
<CSVSingleImportDialog
open={showImportDialog}
yesButtonLabel={'Import'}
noButtonLabel={'Cancel'}
onClose={handleCloseImportDialog}
onNo={handleCloseImportDialog}
onYes={() => {
if (file) {
handleImportTelemetry(file);
}
}}
dialogContent={
<>
<FileUploadSingleItem
file={file}
status={uploadStatus}
progress={progress}
onFile={(file) => setFile(file)}
onCancel={handleResetFileImport}
/>
{importCSVErrors.length > 0 ? (
<Box sx={{ mt: 2 }}>
<CSVErrorsTableContainer errors={importCSVErrors} />
</Box>
) : null}
</>
dialogTitle="Import Telemetry CSV"
dialogSummary="Import a CSV file containing telemetry records"
onCancel={() => setShowImportDialog(false)}

Check warning on line 85 in app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx#L85

Added line #L85 was not covered by tests
onImport={handleImportTelemetryCSV}
onDownloadTemplate={() =>
downloadFile(getTelemetryCSVTemplate(), `SIMS-telemetry-template-${new Date().getFullYear()}.csv`)

Check warning on line 88 in app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx

View check run for this annotation

Codecov / codecov/patch

app/src/features/surveys/telemetry/table/TelemetryTableContainer.tsx#L88

Added line #L88 was not covered by tests
}
yesButtonProps={{
loading: isUploading,
disabled: disableImportButton
}}
/>

<YesNoDialog
dialogTitle={TelemetryTableI18N.removeAllDialogTitle}
dialogText={TelemetryTableI18N.removeAllDialogText}
Expand Down
Loading

0 comments on commit 0bee76d

Please sign in to comment.