1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import React from 'react';
6 import classnames from 'classnames';
7 import { CustomStyleRulesCallback } from 'common/custom-theme';
17 } from '@mui/material';
18 import { WithStyles } from '@mui/styles';
19 import withStyles from '@mui/styles/withStyles';
20 import { CloudUploadIcon, RemoveIcon } from "../icon/icon";
21 import { formatFileSize, formatProgress, formatUploadSpeed } from "common/formatters";
22 import { UploadFile } from 'store/file-uploader/file-uploader-actions';
23 import { UploadInput, FileUploadType } from 'components/file-upload/upload-input';
25 type CssRules = "dropzoneWrapper" | "container" | "inputContainer" | "uploadIcon"
26 | "dropzoneBorder" | "dropzoneBorderLeft" | "dropzoneBorderRight" | "dropzoneBorderTop" | "dropzoneBorderBottom"
27 | "dropzoneBorderHorzActive" | "dropzoneBorderVertActive" | "deleteButton" | "deleteButtonDisabled" | "deleteIcon";
29 const styles: CustomStyleRulesCallback<CssRules> = theme => ({
34 border: "1px solid rgba(0, 0, 0, 0.42)",
35 boxSizing: 'border-box',
41 transition: "transform 200ms cubic-bezier(0.0, 0, 0.2, 1) 0ms",
42 pointerEvents: "none",
43 backgroundColor: "#6a1b9a"
50 transform: "scaleY(0)",
52 dropzoneBorderRight: {
57 transform: "scaleY(0)",
64 transform: "scaleX(0)",
66 dropzoneBorderBottom: {
71 transform: "scaleX(0)",
73 dropzoneBorderHorzActive: {
74 transform: "scaleY(1)"
76 dropzoneBorderVertActive: {
77 transform: "scaleX(1)"
83 flexDirection: 'column',
91 justifyContent: 'space-around',
94 verticalAlign: "middle"
99 deleteButtonDisabled: {
100 cursor: "not-allowed"
107 interface FileUploadPropsData {
110 onDrop: (files: File[]) => void;
111 onDelete: (file: UploadFile) => void;
114 interface FileUploadState {
118 export type FileUploadProps = FileUploadPropsData & WithStyles<CssRules>;
120 export const FileUpload = withStyles(styles)(
121 class extends React.Component<FileUploadProps, FileUploadState> {
122 constructor(props: FileUploadProps) {
128 onDelete = (event: React.MouseEvent<HTMLButtonElement>, file: UploadFile): void => {
129 const { onDelete, disabled } = this.props;
131 event.stopPropagation();
137 let interval = setInterval(() => {
138 const key = Object.keys((window as any).cancelTokens).find(key => key.indexOf(file.file.name) > -1);
141 clearInterval(interval);
142 (window as any).cancelTokens[key]();
143 delete (window as any).cancelTokens[key];
149 fileInputRef = React.createRef<HTMLInputElement>();
150 folderInputRef = React.createRef<HTMLInputElement>();
152 handleDrop = async (event) => {
153 event.preventDefault();
155 const items = event.dataTransfer.items;
156 const entries: any[] = [];
158 for (let i = 0; i < items.length; i++) {
159 const entry = items[i].webkitGetAsEntry?.();
160 if (entry) entries.push(entry);
163 const filesArrays = await Promise.all(entries.map((entry) => traverseFileTree(entry)));
164 const allFiles = filesArrays.flat();
166 this.props.onDrop(allFiles as any); // includes `file.relativePath` if needed
169 handleInputChange = (event) => {
170 const files = Array.from(event.target.files);
171 this.props.onDrop(files as any);
174 getInputProps = () => ({
175 disabled: this.props.disabled,
176 handleInputChange: this.handleInputChange,
177 onFocus: ()=>this.setState({ focused: true }),
178 onBlur: ()=>this.setState({ focused: false }),
182 const { classes, disabled, files } = this.props;
184 <div className={"file-upload-dropzone " + classes.dropzoneWrapper} >
185 <div className={classnames(classes.dropzoneBorder, classes.dropzoneBorderLeft, { [classes.dropzoneBorderHorzActive]: this.state.focused })} />
186 <div className={classnames(classes.dropzoneBorder, classes.dropzoneBorderRight, { [classes.dropzoneBorderHorzActive]: this.state.focused })} />
187 <div className={classnames(classes.dropzoneBorder, classes.dropzoneBorderTop, { [classes.dropzoneBorderVertActive]: this.state.focused })} />
188 <div className={classnames(classes.dropzoneBorder, classes.dropzoneBorderBottom, { [classes.dropzoneBorderVertActive]: this.state.focused })} />
190 onDrop={this.handleDrop}
191 onDragOver={(e) => e.preventDefault()}
193 const el = document.getElementsByClassName("file-upload-dropzone")[0];
194 const inputs = el.getElementsByTagName("input");
195 if (inputs.length > 0) {
199 data-cy="drag-and-drop"
201 {files.length === 0 &&
202 <Grid container justifyContent="center" alignItems="center" className={classes.container}>
203 <Grid item component={"span"}>
204 <Typography variant='subtitle1'>
205 <CloudUploadIcon className={classes.uploadIcon} /> Drag and drop data or click to browse
208 <Grid item component={"div"} className={classes.inputContainer}>
209 <UploadInput type={FileUploadType.FOLDER} inputRef={this.folderInputRef} {...this.getInputProps()} />
210 <UploadInput type={FileUploadType.FILE} inputRef={this.fileInputRef} {...this.getInputProps()} />
214 <Table style={{ width: "100%" }}>
217 <TableCell>File name</TableCell>
218 <TableCell>File size</TableCell>
219 <TableCell>Upload speed</TableCell>
220 <TableCell>Upload progress</TableCell>
221 <TableCell>Delete</TableCell>
226 <TableRow key={f.id}>
227 <TableCell>{f.file.name}</TableCell>
228 <TableCell>{formatFileSize(f.file.size)}</TableCell>
229 <TableCell>{formatUploadSpeed(f.prevLoaded, f.loaded, f.prevTime, f.currentTime)}</TableCell>
230 <TableCell>{formatProgress(f.loaded, f.total)}</TableCell>
234 onClick={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => this.onDelete(event, f)}
235 className={disabled ? classnames(classes.deleteButtonDisabled, classes.deleteIcon) : classnames(classes.deleteButton, classes.deleteIcon)}
252 function traverseFileTree(item, path = '') {
253 return new Promise((resolve) => {
255 item.file((file) => {
256 file.relativePath = path + file.name;
259 } else if (item.isDirectory) {
260 const dirReader = item.createReader();
261 dirReader.readEntries(async (entries) => {
262 const files = await Promise.all(entries.map((entry) => traverseFileTree(entry, path + item.name + '/')));
263 resolve(files.flat());