Merge remote-tracking branch 'origin/main' into 18169-cancel-button-not-working
[arvados-workbench2.git] / src / components / file-upload / file-upload.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from 'react';
6 import classnames from 'classnames';
7 import {
8     Grid,
9     StyleRulesCallback,
10     Table, TableBody, TableCell, TableHead, TableRow,
11     Typography,
12     WithStyles,
13     IconButton
14 } from '@material-ui/core';
15 import { withStyles } from '@material-ui/core';
16 import Dropzone from 'react-dropzone';
17 import { CloudUploadIcon, RemoveIcon } from "../icon/icon";
18 import { formatFileSize, formatProgress, formatUploadSpeed } from "common/formatters";
19 import { UploadFile } from 'store/file-uploader/file-uploader-actions';
20
21 type CssRules = "root" | "dropzone" | "dropzoneWrapper" | "container" | "uploadIcon"
22     | "dropzoneBorder" | "dropzoneBorderLeft" | "dropzoneBorderRight" | "dropzoneBorderTop" | "dropzoneBorderBottom"
23     | "dropzoneBorderHorzActive" | "dropzoneBorderVertActive" | "deleteButton" | "deleteButtonDisabled" | "deleteIcon";
24
25 const styles: StyleRulesCallback<CssRules> = theme => ({
26     root: {
27     },
28     dropzone: {
29         width: "100%",
30         height: "100%",
31         overflow: "auto"
32     },
33     dropzoneWrapper: {
34         width: "100%",
35         height: "200px",
36         position: "relative",
37         border: "1px solid rgba(0, 0, 0, 0.42)"
38     },
39     dropzoneBorder: {
40         content: "",
41         position: "absolute",
42         transition: "transform 200ms cubic-bezier(0.0, 0, 0.2, 1) 0ms",
43         pointerEvents: "none",
44         backgroundColor: "#6a1b9a"
45     },
46     dropzoneBorderLeft: {
47         left: -1,
48         top: -1,
49         bottom: -1,
50         width: 2,
51         transform: "scaleY(0)",
52     },
53     dropzoneBorderRight: {
54         right: -1,
55         top: -1,
56         bottom: -1,
57         width: 2,
58         transform: "scaleY(0)",
59     },
60     dropzoneBorderTop: {
61         left: 0,
62         right: 0,
63         top: -1,
64         height: 2,
65         transform: "scaleX(0)",
66     },
67     dropzoneBorderBottom: {
68         left: 0,
69         right: 0,
70         bottom: -1,
71         height: 2,
72         transform: "scaleX(0)",
73     },
74     dropzoneBorderHorzActive: {
75         transform: "scaleY(1)"
76     },
77     dropzoneBorderVertActive: {
78         transform: "scaleX(1)"
79     },
80     container: {
81         height: "100%"
82     },
83     uploadIcon: {
84         verticalAlign: "middle"
85     },
86     deleteButton: {
87         cursor: "pointer"
88     },
89     deleteButtonDisabled: {
90         cursor: "not-allowed"
91     },
92     deleteIcon: {
93         marginLeft: "-6px"
94     }
95 });
96
97 interface FileUploadPropsData {
98     files: UploadFile[];
99     disabled: boolean;
100     onDrop: (files: File[]) => void;
101     onDelete: (file: UploadFile) => void;
102 }
103
104 interface FileUploadState {
105     focused: boolean;
106 }
107
108 export type FileUploadProps = FileUploadPropsData & WithStyles<CssRules>;
109
110 export const FileUpload = withStyles(styles)(
111     class extends React.Component<FileUploadProps, FileUploadState> {
112         constructor(props: FileUploadProps) {
113             super(props);
114             this.state = {
115                 focused: false
116             };
117         }
118         onDelete = (event: React.MouseEvent<HTMLTableCellElement>, file: UploadFile): void => {
119             const { onDelete, disabled } = this.props;
120
121             event.stopPropagation();
122
123             if (!disabled) {
124                 onDelete(file);
125             }
126         }
127         render() {
128             const { classes, onDrop, disabled, files } = this.props;
129             return <div className={"file-upload-dropzone " + classes.dropzoneWrapper}>
130                 <div className={classnames(classes.dropzoneBorder, classes.dropzoneBorderLeft, { [classes.dropzoneBorderHorzActive]: this.state.focused })} />
131                 <div className={classnames(classes.dropzoneBorder, classes.dropzoneBorderRight, { [classes.dropzoneBorderHorzActive]: this.state.focused })} />
132                 <div className={classnames(classes.dropzoneBorder, classes.dropzoneBorderTop, { [classes.dropzoneBorderVertActive]: this.state.focused })} />
133                 <div className={classnames(classes.dropzoneBorder, classes.dropzoneBorderBottom, { [classes.dropzoneBorderVertActive]: this.state.focused })} />
134                 <Dropzone className={classes.dropzone}
135                     onDrop={files => onDrop(files)}
136                     onClick={() => {
137                         const el = document.getElementsByClassName("file-upload-dropzone")[0];
138                         const inputs = el.getElementsByTagName("input");
139                         if (inputs.length > 0) {
140                             inputs[0].focus();
141                         }
142                     }}
143                     data-cy="drag-and-drop"
144                     disabled={disabled}
145                     inputProps={{
146                         onFocus: () => {
147                             this.setState({
148                                 focused: true
149                             });
150                         },
151                         onBlur: () => {
152                             this.setState({
153                                 focused: false
154                             });
155                         }
156                     }}>
157                     {files.length === 0 &&
158                         <Grid container justify="center" alignItems="center" className={classes.container}>
159                             <Grid item component={"span"}>
160                                 <Typography variant='subtitle1'>
161                                     <CloudUploadIcon className={classes.uploadIcon} /> Drag and drop data or click to browse
162                             </Typography>
163                             </Grid>
164                         </Grid>}
165                     {files.length > 0 &&
166                         <Table style={{ width: "100%" }}>
167                             <TableHead>
168                                 <TableRow>
169                                     <TableCell>File name</TableCell>
170                                     <TableCell>File size</TableCell>
171                                     <TableCell>Upload speed</TableCell>
172                                     <TableCell>Upload progress</TableCell>
173                                     <TableCell>Delete</TableCell>
174                                 </TableRow>
175                             </TableHead>
176                             <TableBody>
177                                 {files.map(f =>
178                                     <TableRow key={f.id}>
179                                         <TableCell>{f.file.name}</TableCell>
180                                         <TableCell>{formatFileSize(f.file.size)}</TableCell>
181                                         <TableCell>{formatUploadSpeed(f.prevLoaded, f.loaded, f.prevTime, f.currentTime)}</TableCell>
182                                         <TableCell>{formatProgress(f.loaded, f.total)}</TableCell>
183                                         <TableCell>
184                                             <IconButton
185                                                 aria-label="Remove"
186                                                 onClick={(event: React.MouseEvent<HTMLTableCellElement>) => this.onDelete(event, f)}
187                                                 className={disabled ? classnames(classes.deleteButtonDisabled, classes.deleteIcon) : classnames(classes.deleteButton, classes.deleteIcon)}
188                                             >
189                                                 <RemoveIcon />
190                                             </IconButton>
191                                         </TableCell>
192                                     </TableRow>
193                                 )}
194                             </TableBody>
195                         </Table>
196                     }
197                 </Dropzone>
198             </div>;
199         }
200     }
201 );