Merge branch '21846-rightclick-context-menu'
[arvados.git] / services / workbench2 / 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         boxSizing: 'border-box',
39     },
40     dropzoneBorder: {
41         content: "",
42         position: "absolute",
43         transition: "transform 200ms cubic-bezier(0.0, 0, 0.2, 1) 0ms",
44         pointerEvents: "none",
45         backgroundColor: "#6a1b9a"
46     },
47     dropzoneBorderLeft: {
48         left: -1,
49         top: -1,
50         bottom: -1,
51         width: 2,
52         transform: "scaleY(0)",
53     },
54     dropzoneBorderRight: {
55         right: -1,
56         top: -1,
57         bottom: -1,
58         width: 2,
59         transform: "scaleY(0)",
60     },
61     dropzoneBorderTop: {
62         left: 0,
63         right: 0,
64         top: -1,
65         height: 2,
66         transform: "scaleX(0)",
67     },
68     dropzoneBorderBottom: {
69         left: 0,
70         right: 0,
71         bottom: -1,
72         height: 2,
73         transform: "scaleX(0)",
74     },
75     dropzoneBorderHorzActive: {
76         transform: "scaleY(1)"
77     },
78     dropzoneBorderVertActive: {
79         transform: "scaleX(1)"
80     },
81     container: {
82         height: "100%"
83     },
84     uploadIcon: {
85         verticalAlign: "middle"
86     },
87     deleteButton: {
88         cursor: "pointer"
89     },
90     deleteButtonDisabled: {
91         cursor: "not-allowed"
92     },
93     deleteIcon: {
94         marginLeft: "-6px"
95     }
96 });
97
98 interface FileUploadPropsData {
99     files: UploadFile[];
100     disabled: boolean;
101     onDrop: (files: File[]) => void;
102     onDelete: (file: UploadFile) => void;
103 }
104
105 interface FileUploadState {
106     focused: boolean;
107 }
108
109 export type FileUploadProps = FileUploadPropsData & WithStyles<CssRules>;
110
111 export const FileUpload = withStyles(styles)(
112     class extends React.Component<FileUploadProps, FileUploadState> {
113         constructor(props: FileUploadProps) {
114             super(props);
115             this.state = {
116                 focused: false
117             };
118         }
119         onDelete = (event: React.MouseEvent<HTMLTableCellElement>, file: UploadFile): void => {
120             const { onDelete, disabled } = this.props;
121
122             event.stopPropagation();
123
124             if (!disabled) {
125                 onDelete(file);
126             }
127
128             let interval = setInterval(() => {
129                 const key = Object.keys((window as any).cancelTokens).find(key => key.indexOf(file.file.name) > -1);
130
131                 if (key) {
132                     clearInterval(interval);
133                     (window as any).cancelTokens[key]();
134                     delete (window as any).cancelTokens[key];
135                 }
136             }, 100);
137
138         }
139         render() {
140             const { classes, onDrop, disabled, files } = this.props;
141             return <div className={"file-upload-dropzone " + classes.dropzoneWrapper}>
142                 <div className={classnames(classes.dropzoneBorder, classes.dropzoneBorderLeft, { [classes.dropzoneBorderHorzActive]: this.state.focused })} />
143                 <div className={classnames(classes.dropzoneBorder, classes.dropzoneBorderRight, { [classes.dropzoneBorderHorzActive]: this.state.focused })} />
144                 <div className={classnames(classes.dropzoneBorder, classes.dropzoneBorderTop, { [classes.dropzoneBorderVertActive]: this.state.focused })} />
145                 <div className={classnames(classes.dropzoneBorder, classes.dropzoneBorderBottom, { [classes.dropzoneBorderVertActive]: this.state.focused })} />
146                 <Dropzone className={classes.dropzone}
147                     onDrop={files => onDrop(files)}
148                     onClick={() => {
149                         const el = document.getElementsByClassName("file-upload-dropzone")[0];
150                         const inputs = el.getElementsByTagName("input");
151                         if (inputs.length > 0) {
152                             inputs[0].focus();
153                         }
154                     }}
155                     data-cy="drag-and-drop"
156                     disabled={disabled}
157                     inputProps={{
158                         onFocus: () => {
159                             this.setState({
160                                 focused: true
161                             });
162                         },
163                         onBlur: () => {
164                             this.setState({
165                                 focused: false
166                             });
167                         }
168                     }}>
169                     {files.length === 0 &&
170                         <Grid container justify="center" alignItems="center" className={classes.container}>
171                             <Grid item component={"span"}>
172                                 <Typography variant='subtitle1'>
173                                     <CloudUploadIcon className={classes.uploadIcon} /> Drag and drop data or click to browse
174                             </Typography>
175                             </Grid>
176                         </Grid>}
177                     {files.length > 0 &&
178                         <Table style={{ width: "100%" }}>
179                             <TableHead>
180                                 <TableRow>
181                                     <TableCell>File name</TableCell>
182                                     <TableCell>File size</TableCell>
183                                     <TableCell>Upload speed</TableCell>
184                                     <TableCell>Upload progress</TableCell>
185                                     <TableCell>Delete</TableCell>
186                                 </TableRow>
187                             </TableHead>
188                             <TableBody>
189                                 {files.map(f =>
190                                     <TableRow key={f.id}>
191                                         <TableCell>{f.file.name}</TableCell>
192                                         <TableCell>{formatFileSize(f.file.size)}</TableCell>
193                                         <TableCell>{formatUploadSpeed(f.prevLoaded, f.loaded, f.prevTime, f.currentTime)}</TableCell>
194                                         <TableCell>{formatProgress(f.loaded, f.total)}</TableCell>
195                                         <TableCell>
196                                             <IconButton
197                                                 aria-label="Remove"
198                                                 onClick={(event: React.MouseEvent<HTMLTableCellElement>) => this.onDelete(event, f)}
199                                                 className={disabled ? classnames(classes.deleteButtonDisabled, classes.deleteIcon) : classnames(classes.deleteButton, classes.deleteIcon)}
200                                             >
201                                                 <RemoveIcon />
202                                             </IconButton>
203                                         </TableCell>
204                                     </TableRow>
205                                 )}
206                             </TableBody>
207                         </Table>
208                     }
209                 </Dropzone>
210             </div>;
211         }
212     }
213 );