Local modifications to ClusterLabs/Anvil by Alteeve
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

297 lines
8.4 KiB

import { Box, Input, InputLabel } from '@mui/material';
import EventEmitter from 'events';
import {
ChangeEventHandler,
FormEventHandler,
ReactElement,
useEffect,
useRef,
useState,
} from 'react';
import { UPLOAD_FILE_TYPES } from '../../lib/consts/UPLOAD_FILE_TYPES';
import api from '../../lib/api';
import { ProgressBar } from '../Bars';
import ContainedButton from '../ContainedButton';
import FileInfo from './FileInfo';
import MessageBox from '../MessageBox';
import { BodyText } from '../Text';
type FileUploadFormProps = {
onFileUploadComplete?: () => void;
eventEmitter?: EventEmitter;
};
type SelectedFile = Pick<
FileDetailMetadata,
'fileName' | 'fileLocations' | 'fileType'
> & {
file: File;
};
type InUploadFile = Pick<FileDetailMetadata, 'fileName'> & {
progressValue: number;
};
const FILE_UPLOAD_FORM_DEFAULT_PROPS: Partial<FileUploadFormProps> = {
onFileUploadComplete: undefined,
eventEmitter: undefined,
};
const FILE_UPLOAD_PERCENT_MAX = 99;
const FileUploadForm = (
{
onFileUploadComplete,
eventEmitter,
}: FileUploadFormProps = FILE_UPLOAD_FORM_DEFAULT_PROPS as FileUploadFormProps,
): JSX.Element => {
const selectFileRef = useRef<HTMLInputElement>();
const [selectedFiles, setSelectedFiles] = useState<SelectedFile[]>([]);
const [inUploadFiles, setInUploadFiles] = useState<
(InUploadFile | undefined)[]
>([]);
const convertMIMETypeToFileTypeKey = (fileMIMEType: string): FileType => {
const fileTypesIterator = UPLOAD_FILE_TYPES.entries();
let fileType: FileType | undefined;
do {
const fileTypesResult = fileTypesIterator.next();
if (fileTypesResult.value) {
const [fileTypeKey, [mimeTypeToUse]] = fileTypesResult.value;
if (fileMIMEType === mimeTypeToUse) {
fileType = fileTypeKey;
}
} else {
fileType = 'other';
}
} while (!fileType);
return fileType;
};
const autocompleteAfterSelectFile: ChangeEventHandler<HTMLInputElement> = ({
target: { files },
}) => {
if (files) {
setSelectedFiles(
Array.from(files).map(
(file): SelectedFile => ({
file,
fileName: file.name,
fileLocations: [],
fileType: convertMIMETypeToFileTypeKey(file.type),
}),
),
);
}
};
const generateFileInfoOnChangeHandler =
(fileIndex: number): FileInfoChangeHandler =>
(inputValues) => {
selectedFiles[fileIndex] = {
...selectedFiles[fileIndex],
...inputValues,
};
};
const uploadFiles: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
while (selectedFiles.length > 0) {
const selectedFile = selectedFiles.shift();
if (selectedFile) {
const { file, fileName } = selectedFile;
const fileFormData = new FormData();
fileFormData.append('file', new File([file], fileName, { ...file }));
// Re-add when the back-end tools can support changing file type on file upload.
// Note: get file type from destructuring selectedFile.
// fileFormData.append('file-type', fileType);
const inUploadFile: InUploadFile = { fileName, progressValue: 0 };
inUploadFiles.push(inUploadFile);
api
.post('/file', fileFormData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (
(fName: string) =>
({ loaded, total }) => {
setInUploadFiles((previous) => {
const fInfo = previous.find((f) => f?.fileName === fName);
if (!fInfo) return previous;
/**
* Use 99 as upper limit because progress event doesn't
* represent the "bytes out of total" written to the remote
* disk. The write completes when the request completes.
*/
fInfo.progressValue = Math.round(
(loaded / total) * FILE_UPLOAD_PERCENT_MAX,
);
return [...previous];
});
}
)(fileName),
})
.then(
((fName: string) => () => {
setInUploadFiles((previous) => {
const fInfo = previous.find((f) => f?.fileName === fName);
if (!fInfo) return previous;
fInfo.progressValue = 100;
return [...previous];
});
setTimeout(() => {
setInUploadFiles((previous) => {
const fIndex = previous.findIndex(
(f) => f?.fileName === fName,
);
if (fIndex === -1) return previous;
delete previous[fIndex];
return [...previous];
});
}, 5000);
onFileUploadComplete?.call(null);
})(fileName),
);
}
}
// Clears "staging area" (selected files) and populates "in-progress area" (in-upload files).
setSelectedFiles([]);
setInUploadFiles([...inUploadFiles]);
};
useEffect(() => {
eventEmitter?.addListener('openFilePicker', () => {
selectFileRef.current?.click();
});
eventEmitter?.addListener('clearSelectedFiles', () => {
setSelectedFiles([]);
});
}, [eventEmitter]);
return (
<form onSubmit={uploadFiles}>
<InputLabel htmlFor="select-file">
<Input
id="select-file"
inputProps={{ multiple: true }}
onChange={autocompleteAfterSelectFile}
ref={selectFileRef}
sx={{ display: 'none' }}
type="file"
/>
</InputLabel>
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
{inUploadFiles.reduce<ReactElement[]>((previous, upFile) => {
if (!upFile) return previous;
const { fileName, progressValue } = upFile;
previous.push(
<Box
key={`in-upload-${fileName}`}
sx={{
alignItems: { md: 'center' },
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
'& > :first-child': {
minWidth: 100,
overflow: 'hidden',
overflowWrap: 'normal',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
width: { xs: '100%', md: 200 },
wordBreak: 'keep-all',
},
'& > :last-child': { flexGrow: 1 },
}}
>
<BodyText text={fileName} />
<ProgressBar progressPercentage={progressValue} />
</Box>,
);
return previous;
}, [])}
</Box>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
'& > :not(:first-child)': { marginTop: '1em' },
}}
>
{selectedFiles.length > 0 && (
<MessageBox type="info">
Uploaded files will be listed automatically, but it may take a while
for larger files to finish uploading and appear on the list.
</MessageBox>
)}
{selectedFiles.map(
(
{
file: { name: originalFileName },
fileName,
// Re-add when the back-end tools can support changing file type on file upload.
// Note: file type must be supplied to FileInfo.
// fileType,
fileLocations,
},
fileIndex,
) => (
<FileInfo
{...{ fileName, fileLocations }}
// Use a non-changing key to prevent recreating the component.
// fileName holds the string from the file-name input, thus it changes when users makes a change.
key={`selected-${originalFileName}`}
onChange={generateFileInfoOnChangeHandler(fileIndex)}
/>
),
)}
{selectedFiles.length > 0 && (
<Box
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
}}
>
<ContainedButton type="submit">Upload</ContainedButton>
</Box>
)}
</Box>
</form>
);
};
FileUploadForm.defaultProps = FILE_UPLOAD_FORM_DEFAULT_PROPS;
export default FileUploadForm;