frontend/app/DeliverableUploader/UploaderMain.tsx (225 lines of code) (raw):

import React, { ChangeEvent } from "react"; import { CircularProgress, Fab, Grid, IconButton, LinearProgress, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Tooltip, Typography, } from "@material-ui/core"; import { Add, CloudUploadOutlined } from "@material-ui/icons"; import { makeStyles } from "@material-ui/core/styles"; import { FileEntry } from "./FileEntry"; import { InitiateUpload, UploadAndValidate } from "./UploadService"; interface UploaderMainProps { projectId: string; dropFolder: string; } interface UploaderMainState { loading: boolean; uploadInProgress: boolean; dialogError?: string; files: FileEntry[]; } class UploaderMain extends React.Component< UploaderMainProps, UploaderMainState > { constructor(props: any) { super(props); this.state = { loading: false, uploadInProgress: false, dialogError: undefined, files: [], }; this.newFileAdded = this.newFileAdded.bind(this); this.uploadButtonClicked = this.uploadButtonClicked.bind(this); } static getDerivedStateFromError(err: Error) { return { dialogError: err.toString(), }; } componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error("File uploader caught an error: ", error, errorInfo); } newFileAdded(evt: any) { console.log( `Selected ${evt.target.files ? evt.target.files.length : 0} files` ); console.log(evt.target.files); if (!evt.target.files) return; //evt.target.files is a FileList type, not an Array, so we can't map over it :( let newEntries: FileEntry[] = []; for (let i = 0; i < evt.target.files.length; ++i) { newEntries[i] = { filename: evt.target.files[i].name, progress: 0, lastError: "", rawFile: evt.target.files[i], }; } this.setState((prevState) => ({ files: prevState.files.concat(...newEntries), })); } async uploadButtonClicked() { this.setState({ uploadInProgress: true }, async () => { try { const projectIdInt = parseInt(this.props.projectId); const uploadId = await InitiateUpload( projectIdInt, this.props.dropFolder ); console.debug("upload id is ", uploadId); for (let i = 0; i < this.state.files.length; ++i) { const didValidate = await UploadAndValidate( this.state.files[i], i, uploadId, 5242880, //5Mb chunk size (updatedEntry, index) => { this.setState((prevState) => { let updatedFileState: FileEntry[] = Object.assign( [], prevState.files ); updatedFileState[index] = updatedEntry; return { files: updatedFileState }; }); } ); this.setState((prevState) => { let updatedFileState: FileEntry[] = Object.assign( [], prevState.files ); const updatedEntry = Object.assign({}, this.state.files[i]); updatedEntry.lastError = didValidate ? "Completed OK" : "Upload was corrupted"; updatedFileState[i] = updatedEntry; return { files: updatedFileState }; }); console.log( `Upload for ${i + 1} / ${ this.state.files.length } completed or failed` ); } this.setState({ uploadInProgress: false }); console.log("All done!"); } catch (err) { console.error(err); this.setState({ uploadInProgress: false }); } }); } render() { return ( <Grid container spacing={3}> <Grid item xs={6}> <label htmlFor="upload-selector"> <input style={{ display: "none" }} id="upload-selector" name="upload-selector" type="file" disabled={this.state.uploadInProgress} onChange={this.newFileAdded} /> <Fab size="small" component="span" aria-label="add" variant="extended" disabled={this.state.uploadInProgress} color="primary" > <Add /> Add more files </Fab> </label> </Grid> <Grid item xs={4}> <Fab size="small" component="span" aria-label="add" variant="extended" onClick={this.uploadButtonClicked} disabled={this.state.uploadInProgress} color="primary" > <CloudUploadOutlined /> &nbsp;&nbsp; {this.state.uploadInProgress ? "Uploading..." : "Start upload"} </Fab> </Grid> <Grid item xs={2}> {this.state.uploadInProgress ? <CircularProgress size={32} /> : null} </Grid> <Grid item xs={12}> {this.state.dialogError ? ( <> <Typography> An internal error occurred: {this.state.dialogError} </Typography> <Typography>Please refresh and try again</Typography> </> ) : ( <Paper elevation={3}> <TableContainer> <Table style={{ width: "100%" }}> <TableHead style={{ height: "2em" }}> <TableRow> <TableCell>File</TableCell> <TableCell>Progress</TableCell> <TableCell>Status</TableCell> <TableCell>Problems</TableCell> </TableRow> </TableHead> <TableBody> {this.state.files.map((entry, idx) => ( <TableRow key={idx}> <TableCell>{entry.filename}</TableCell> <TableCell> <LinearProgress variant="determinate" value={entry.progress > 100 ? 100 : entry.progress} /> </TableCell> <TableCell> {entry.progress == 0 ? ( <div>Not uploaded</div> ) : ( [ entry.progress < 100 ? ( <div>Uploading</div> ) : ( <div>Uploaded</div> ), ] )} </TableCell> <TableCell>{entry.lastError}</TableCell> </TableRow> ))} </TableBody> </Table> </TableContainer> </Paper> )} </Grid> </Grid> ); } } export default UploaderMain;