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