fix(striker-ui): clean up old file manager

main
Tsu-ba-me 1 year ago
parent 3815cab856
commit 48dfc1b76c
  1. 334
      striker-ui/components/Files/FileEditForm.tsx
  2. 203
      striker-ui/components/Files/FileInfo.tsx
  3. 80
      striker-ui/components/Files/FileList.tsx
  4. 296
      striker-ui/components/Files/FileUploadForm.tsx
  5. 148
      striker-ui/components/Files/Files.tsx
  6. 4
      striker-ui/components/Files/index.tsx
  7. 2
      striker-ui/pages/file-manager/index.tsx
  8. 3
      striker-ui/types/FileDetailMetadata.d.ts
  9. 4
      striker-ui/types/FileInfoChangeHandler.d.ts
  10. 9
      striker-ui/types/FileLocation.d.ts
  11. 7
      striker-ui/types/FileOverviewMetadata.d.ts

@ -1,334 +0,0 @@
import { Box, Checkbox, checkboxClasses } from '@mui/material';
import { AxiosResponse } from 'axios';
import {
FormEventHandler,
MouseEventHandler,
useEffect,
useState,
} from 'react';
import API_BASE_URL from '../../lib/consts/API_BASE_URL';
import { GREY, RED, TEXT } from '../../lib/consts/DEFAULT_THEME';
import api from '../../lib/api';
import ConfirmDialog from '../ConfirmDialog';
import ContainedButton from '../ContainedButton';
import FileInfo from './FileInfo';
import Spinner from '../Spinner';
import fetchJSON from '../../lib/fetchers/fetchJSON';
type ReducedFileLocation = Partial<
Pick<FileLocation, 'fileLocationUUID' | 'isFileLocationActive'>
>;
type EditRequestContent = Partial<
Pick<FileDetailMetadata, 'fileName' | 'fileType' | 'fileUUID'>
> & {
fileLocations: ReducedFileLocation[];
};
type FileEditProps = {
filesOverview: FileOverviewMetadata[];
onEditFilesComplete?: () => void;
onPurgeFilesComplete?: () => void;
};
type FileToEdit = FileDetailMetadata & {
dataIncompleteError?: unknown;
isSelected?: boolean;
};
const FILE_EDIT_FORM_DEFAULT_PROPS = {
onEditFilesComplete: undefined,
onPurgeFilesComplete: undefined,
};
const FileEditForm = (
{
filesOverview,
onEditFilesComplete,
onPurgeFilesComplete,
}: FileEditProps = FILE_EDIT_FORM_DEFAULT_PROPS as FileEditProps,
): JSX.Element => {
const [editRequestContents, setEditRequestContents] = useState<
EditRequestContent[]
>([]);
const [filesToEdit, setFilesToEdit] = useState<FileToEdit[]>([]);
const [isLoadingFilesToEdit, setIsLoadingFilesToEdit] =
useState<boolean>(false);
const [isOpenPurgeConfirmDialog, setIsOpenConfirmPurgeDialog] =
useState<boolean>(false);
const [selectedFilesCount, setSelectedFilesCount] = useState<number>(0);
const purgeButtonStyleOverride = {
backgroundColor: RED,
color: TEXT,
'&:hover': { backgroundColor: RED },
};
const generateFileInfoChangeHandler =
(fileIndex: number): FileInfoChangeHandler =>
(inputValues, { fileLocationIndex } = {}) => {
if (fileLocationIndex !== undefined) {
editRequestContents[fileIndex].fileLocations[fileLocationIndex] = {
...editRequestContents[fileIndex].fileLocations[fileLocationIndex],
...inputValues,
};
} else {
editRequestContents[fileIndex] = {
...editRequestContents[fileIndex],
...inputValues,
};
}
};
const editFiles: FormEventHandler<HTMLFormElement> = (event) => {
event.preventDefault();
setIsLoadingFilesToEdit(true);
const editPromises = editRequestContents.reduce<Promise<AxiosResponse>[]>(
(
reducedEditPromises,
{ fileLocations, fileName, fileType, fileUUID },
) => {
const editRequestContent: Partial<EditRequestContent> = {};
if (fileName !== undefined) {
editRequestContent.fileName = fileName;
}
if (fileType !== undefined) {
editRequestContent.fileType = fileType;
}
const changedFileLocations = fileLocations.reduce<
ReducedFileLocation[]
>(
(
reducedFileLocations,
{ fileLocationUUID, isFileLocationActive },
) => {
if (isFileLocationActive !== undefined) {
reducedFileLocations.push({
fileLocationUUID,
isFileLocationActive,
});
}
return reducedFileLocations;
},
[],
);
if (changedFileLocations.length > 0) {
editRequestContent.fileLocations = changedFileLocations;
}
if (Object.keys(editRequestContent).length > 0) {
reducedEditPromises.push(
api.put(`/file/${fileUUID}`, editRequestContent),
);
}
return reducedEditPromises;
},
[],
);
Promise.all(editPromises)
.then(() => {
setIsLoadingFilesToEdit(false);
})
.then(onEditFilesComplete);
};
const purgeFiles: MouseEventHandler<HTMLButtonElement> = () => {
setIsOpenConfirmPurgeDialog(false);
setIsLoadingFilesToEdit(true);
const purgePromises = filesToEdit
.filter(({ isSelected }) => isSelected)
.map(({ fileUUID }) => api.delete(`/file/${fileUUID}`));
Promise.all(purgePromises)
.then(() => {
setIsLoadingFilesToEdit(false);
})
.then(onPurgeFilesComplete);
};
const cancelPurge: MouseEventHandler<HTMLButtonElement> = () => {
setIsOpenConfirmPurgeDialog(false);
};
const confirmPurge: MouseEventHandler<HTMLButtonElement> = () => {
// We need this local variable because setState functions are async; the
// changes won't reflect until the next render cycle.
// In this case, the user would have to click on the purge button twice to
// trigger the confirmation dialog without using this local variable.
const localSelectedFilesCount = filesToEdit.filter(
({ isSelected }) => isSelected,
).length;
setSelectedFilesCount(localSelectedFilesCount);
if (localSelectedFilesCount > 0) {
setIsOpenConfirmPurgeDialog(true);
}
};
useEffect(() => {
setIsLoadingFilesToEdit(true);
Promise.all(
filesOverview.map(async (fileOverview: FileOverviewMetadata) => {
const fileToEdit: FileToEdit = {
...fileOverview,
fileLocations: [],
};
try {
const data = await fetchJSON<string[][]>(
`${API_BASE_URL}/file/${fileOverview.fileUUID}`,
);
fileToEdit.fileLocations = data.map<FileLocation>(
({
5: fileLocationUUID,
6: fileLocationActive,
7: anvilUUID,
8: anvilName,
9: anvilDescription,
10: hostUUID,
11: hostName,
}) => ({
anvilDescription,
anvilName,
anvilUUID,
fileLocationUUID,
hostName,
hostUUID,
isFileLocationActive: parseInt(fileLocationActive, 10) === 1,
}),
);
} catch (fetchError) {
fileToEdit.dataIncompleteError = fetchError;
}
return fileToEdit;
}),
).then((fetchedFilesDetail) => {
setFilesToEdit(fetchedFilesDetail);
const initialEditRequestContents: EditRequestContent[] = [];
for (
let fileIndex = 0;
fileIndex < fetchedFilesDetail.length;
fileIndex += 1
) {
const fetchedFileDetail = fetchedFilesDetail[fileIndex];
initialEditRequestContents.push({
fileUUID: fetchedFileDetail.fileUUID,
fileLocations: fetchedFileDetail.fileLocations.map(
({ fileLocationUUID }) => ({
fileLocationUUID,
}),
),
});
}
setEditRequestContents(initialEditRequestContents);
setIsLoadingFilesToEdit(false);
});
}, [filesOverview]);
return (
<>
{isLoadingFilesToEdit ? (
<Spinner />
) : (
<form onSubmit={editFiles}>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
'& > :not(:first-child)': { marginTop: '1em' },
}}
>
{filesToEdit.map(
({ fileName, fileLocations, fileType, fileUUID }, fileIndex) => (
<Box
key={`file-edit-${fileUUID}`}
sx={{
display: 'flex',
flexDirection: 'row',
'& > :last-child': {
flexGrow: 1,
},
}}
>
<Box sx={{ marginTop: '.4em' }}>
<Checkbox
onChange={({ target: { checked } }) => {
filesToEdit[fileIndex].isSelected = checked;
}}
sx={{
color: GREY,
[`&.${checkboxClasses.checked}`]: {
color: TEXT,
},
}}
/>
</Box>
<FileInfo
{...{ fileName, fileType, fileLocations }}
onChange={generateFileInfoChangeHandler(fileIndex)}
/>
</Box>
),
)}
{filesToEdit.length > 0 && (
<Box
sx={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'flex-end',
'& > :not(:last-child)': {
marginRight: '.5em',
},
}}
>
<ContainedButton
onClick={confirmPurge}
sx={purgeButtonStyleOverride}
>
Purge
</ContainedButton>
<ContainedButton type="submit">Update</ContainedButton>
</Box>
)}
</Box>
<ConfirmDialog
actionProceedText="Purge"
content={`${selectedFilesCount} files will be removed from the system. You cannot undo this purge.`}
dialogProps={{ open: isOpenPurgeConfirmDialog }}
onCancelAppend={cancelPurge}
onProceedAppend={purgeFiles}
proceedButtonProps={{ sx: purgeButtonStyleOverride }}
titleText={`Are you sure you want to purge ${selectedFilesCount} selected files? `}
/>
</form>
)}
</>
);
};
FileEditForm.defaultProps = FILE_EDIT_FORM_DEFAULT_PROPS;
export default FileEditForm;

@ -1,203 +0,0 @@
import {
Checkbox,
checkboxClasses,
FormControl,
FormControlLabel,
FormGroup,
Grid,
styled,
} from '@mui/material';
import {
Sync as SyncIcon,
SyncDisabled as SyncDisabledIcon,
} from '@mui/icons-material';
import { ReactElement, useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { BLUE, RED, TEXT } from '../../lib/consts/DEFAULT_THEME';
import { UPLOAD_FILE_TYPES_ARRAY } from '../../lib/consts/UPLOAD_FILE_TYPES';
import List from '../List';
import MenuItem from '../MenuItem';
import OutlinedInput from '../OutlinedInput';
import OutlinedInputLabel from '../OutlinedInputLabel';
import { ExpandablePanel, InnerPanelBody } from '../Panels';
import Select from '../Select';
type FileInfoProps = Pick<FileDetailMetadata, 'fileName' | 'fileLocations'> &
Partial<Pick<FileDetailMetadata, 'fileType'>> & {
isReadonly?: boolean;
onChange?: FileInfoChangeHandler;
};
const FILE_INFO_DEFAULT_PROPS: Partial<FileInfoProps> = {
isReadonly: undefined,
onChange: undefined,
};
const FileLocationActiveCheckbox = styled(Checkbox)({
color: RED,
[`&.${checkboxClasses.checked}`]: {
color: BLUE,
},
});
const FileInfo = (
{
fileName,
fileType,
fileLocations,
isReadonly,
onChange,
}: FileInfoProps = FILE_INFO_DEFAULT_PROPS as FileInfoProps,
): JSX.Element => {
const idExtension = uuidv4();
const fileNameElementId = `file-name-${idExtension}`;
const fileNameElementLabel = 'File name';
const fileTypeElementId = `file-type-${idExtension}`;
const fileTypeElementLabel = 'File type';
const anFileLocations = useMemo(
() =>
fileLocations.reduce<
Record<
string,
Pick<FileLocation, 'anvilDescription' | 'anvilName' | 'anvilUUID'> & {
flocs: FileLocation[];
}
>
>((previous, fileLocation) => {
const { anvilDescription, anvilName, anvilUUID } = fileLocation;
if (!previous[anvilUUID]) {
previous[anvilUUID] = {
anvilDescription,
anvilName,
anvilUUID,
flocs: [],
};
}
previous[anvilUUID].flocs.push(fileLocation);
return previous;
}, {}),
[fileLocations],
);
return (
<FormGroup sx={{ '> :not(:first-child)': { marginTop: '1em' } }}>
<FormControl>
<OutlinedInputLabel htmlFor={fileNameElementId}>
{fileNameElementLabel}
</OutlinedInputLabel>
<OutlinedInput
defaultValue={fileName}
disabled={isReadonly}
id={fileNameElementId}
label={fileNameElementLabel}
onChange={({ target: { value } }) => {
onChange?.call(null, {
fileName: value === fileName ? undefined : value,
});
}}
/>
</FormControl>
{fileType && (
<FormControl>
<OutlinedInputLabel htmlFor={fileTypeElementId}>
{fileTypeElementLabel}
</OutlinedInputLabel>
<Select
defaultValue={fileType}
disabled={isReadonly}
id={fileTypeElementId}
input={<OutlinedInput label={fileTypeElementLabel} />}
onChange={({ target: { value } }) => {
onChange?.call(null, {
fileType: value === fileType ? undefined : (value as FileType),
});
}}
>
{UPLOAD_FILE_TYPES_ARRAY.map(
([fileTypeKey, [, fileTypeDisplayString]]) => (
<MenuItem key={fileTypeKey} value={fileTypeKey}>
{fileTypeDisplayString}
</MenuItem>
),
)}
</Select>
</FormControl>
)}
<List
listItems={anFileLocations}
listProps={{ dense: true, disablePadding: true }}
renderListItem={(anvilUUID, { anvilDescription, anvilName, flocs }) => (
<ExpandablePanel
header={`${anvilName}: ${anvilDescription}`}
panelProps={{ padding: 0, width: '100%' }}
>
<InnerPanelBody>
<Grid
columns={{ xs: 1, sm: 2, md: 3, lg: 4, xl: 5 }}
columnSpacing="1em"
container
direction="row"
>
{flocs.map<ReactElement>(
({
fileLocationUUID: flocUUID,
hostName,
hostUUID,
isFileLocationActive,
}) => (
<Grid item key={`floc-${anvilUUID}-${hostUUID}`} xs={1}>
<FormControlLabel
control={
<FileLocationActiveCheckbox
checkedIcon={<SyncIcon />}
defaultChecked={isFileLocationActive}
disabled={isReadonly}
edge="start"
icon={<SyncDisabledIcon />}
onChange={({ target: { checked } }) => {
onChange?.call(
null,
{
isFileLocationActive:
checked === isFileLocationActive
? undefined
: checked,
},
{
fileLocationIndex: fileLocations.findIndex(
({ fileLocationUUID }) =>
flocUUID === fileLocationUUID,
),
},
);
}}
/>
}
label={hostName}
sx={{ color: TEXT }}
value={`${hostUUID}-sync`}
/>
</Grid>
),
)}
</Grid>
</InnerPanelBody>
</ExpandablePanel>
)}
/>
</FormGroup>
);
};
FileInfo.defaultProps = FILE_INFO_DEFAULT_PROPS;
export default FileInfo;

@ -1,80 +0,0 @@
import { Box, Divider, List, ListItem } from '@mui/material';
import * as prettyBytes from 'pretty-bytes';
import { DIVIDER } from '../../lib/consts/DEFAULT_THEME';
import { UPLOAD_FILE_TYPES } from '../../lib/consts/UPLOAD_FILE_TYPES';
import { BodyText } from '../Text';
type FileListProps = {
filesOverview: FileOverviewMetadata[];
};
const FileList = ({ filesOverview }: FileListProps): JSX.Element => (
<List>
{filesOverview.map(
({ fileChecksum, fileName, fileSizeInBytes, fileType, fileUUID }) => {
const fileSize: string = prettyBytes.default(fileSizeInBytes, {
binary: true,
});
return (
<ListItem key={fileUUID} sx={{ padding: '.6em 0' }}>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
width: '100%',
}}
>
<Box sx={{ flexGrow: 1 }}>
<Box
sx={{
display: 'flex',
flexDirection: 'row',
}}
>
<BodyText
sx={{
fontFamily: 'Source Code Pro',
fontWeight: 400,
}}
text={fileName}
/>
<Divider
flexItem
orientation="vertical"
sx={{
backgroundColor: DIVIDER,
marginLeft: '.5em',
marginRight: '.5em',
}}
/>
<BodyText text={UPLOAD_FILE_TYPES.get(fileType)?.[1] ?? ''} />
</Box>
<BodyText text={fileSize} />
</Box>
<Box
sx={{
alignItems: 'center',
display: 'flex',
flexDirection: 'row',
}}
>
<BodyText
sx={{
fontFamily: 'Source Code Pro',
fontWeight: 400,
}}
text={fileChecksum}
/>
</Box>
</Box>
</ListItem>
);
},
)}
</List>
);
export default FileList;

@ -1,296 +0,0 @@
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;

@ -1,148 +0,0 @@
import { useEffect, useState } from 'react';
import { Box } from '@mui/material';
import {
Add as AddIcon,
Check as CheckIcon,
Edit as EditIcon,
} from '@mui/icons-material';
import EventEmitter from 'events';
import API_BASE_URL from '../../lib/consts/API_BASE_URL';
import { BLUE } from '../../lib/consts/DEFAULT_THEME';
import FileEditForm from './FileEditForm';
import FileList from './FileList';
import FileUploadForm from './FileUploadForm';
import IconButton from '../IconButton';
import { Panel } from '../Panels';
import MessageBox from '../MessageBox';
import Spinner from '../Spinner';
import { HeaderText } from '../Text';
import fetchJSON from '../../lib/fetchers/fetchJSON';
import periodicFetch from '../../lib/fetchers/periodicFetch';
const FILES_ENDPOINT_URL = `${API_BASE_URL}/file`;
const Files = (): JSX.Element => {
const [rawFilesOverview, setRawFilesOverview] = useState<string[][]>([]);
const [fetchRawFilesError, setFetchRawFilesError] = useState<string>();
const [isLoadingRawFilesOverview, setIsLoadingRawFilesOverview] =
useState<boolean>(false);
const [isEditMode, setIsEditMode] = useState<boolean>(false);
const fileUploadFormEventEmitter: EventEmitter = new EventEmitter();
const onAddFileButtonClick = () => {
fileUploadFormEventEmitter.emit('openFilePicker');
};
const onEditFileButtonClick = () => {
fileUploadFormEventEmitter.emit('clearSelectedFiles');
setIsEditMode(!isEditMode);
};
const fetchRawFilesOverview = async () => {
setIsLoadingRawFilesOverview(true);
try {
const data = await fetchJSON<string[][]>(FILES_ENDPOINT_URL);
setRawFilesOverview(data);
} catch (fetchError) {
setFetchRawFilesError('Failed to get files due to a network issue.');
}
setIsLoadingRawFilesOverview(false);
};
const buildFileList = (): JSX.Element => {
let elements: JSX.Element;
if (isLoadingRawFilesOverview) {
elements = <Spinner />;
} else {
const filesOverview: FileOverviewMetadata[] = rawFilesOverview.map(
([fileUUID, fileName, fileSizeInBytes, fileType, fileChecksum]) => ({
fileChecksum,
fileName,
fileSizeInBytes: parseInt(fileSizeInBytes, 10),
fileType: fileType as FileType,
fileUUID,
}),
);
elements = isEditMode ? (
<FileEditForm
{...{ filesOverview }}
onEditFilesComplete={fetchRawFilesOverview}
onPurgeFilesComplete={fetchRawFilesOverview}
/>
) : (
<FileList {...{ filesOverview }} />
);
}
return elements;
};
/**
* Check for new files periodically and update the file list.
*
* We need this because adding new files is done async; adding the file may
* not finish before the request returns.
*
* We don't care about edit because database updates are done before the
* edit request returns.
*/
periodicFetch<string[][]>(FILES_ENDPOINT_URL, {
onSuccess: (periodicFilesOverview) => {
if (periodicFilesOverview.length !== rawFilesOverview.length) {
setRawFilesOverview(periodicFilesOverview);
}
},
});
useEffect(() => {
if (!isEditMode) {
fetchRawFilesOverview();
}
}, [isEditMode]);
return (
<Panel>
<Box
sx={{
alignItems: 'center',
display: 'flex',
flexDirection: 'row',
marginBottom: '1em',
width: '100%',
'& > :first-child': { flexGrow: 1 },
'& > :not(:first-child, :last-child)': {
marginRight: '.3em',
},
}}
>
<HeaderText text="Files" />
{!isEditMode && (
<IconButton onClick={onAddFileButtonClick}>
<AddIcon />
</IconButton>
)}
<IconButton onClick={onEditFileButtonClick}>
{isEditMode ? <CheckIcon sx={{ color: BLUE }} /> : <EditIcon />}
</IconButton>
</Box>
{fetchRawFilesError && (
<MessageBox text={fetchRawFilesError} type="error" />
)}
<FileUploadForm
{...{ eventEmitter: fileUploadFormEventEmitter }}
onFileUploadComplete={fetchRawFilesOverview}
/>
{buildFileList()}
</Panel>
);
};
export default Files;

@ -1,3 +1,3 @@
import Files from './Files';
import ManageFilePanel from './ManageFilePanel';
export default Files;
export default ManageFilePanel;

@ -1,7 +1,7 @@
import Head from 'next/head';
import Header from '../../components/Header';
import ManageFilePanel from '../../components/Files/ManageFilePanel';
import ManageFilePanel from '../../components/Files';
const FileManager = (): JSX.Element => (
<>

@ -1,3 +0,0 @@
type FileDetailMetadata = FileOverviewMetadata & {
fileLocations: FileLocation[];
};

@ -1,4 +0,0 @@
type FileInfoChangeHandler = (
inputValues: Partial<FileDetailMetadata> | Partial<FileLocation>,
options?: { fileLocationIndex?: number },
) => void;

@ -1,9 +0,0 @@
type FileLocation = {
anvilName: string;
anvilDescription: string;
anvilUUID: string;
fileLocationUUID: string;
hostName: string;
hostUUID: string;
isFileLocationActive: boolean;
};

@ -1,7 +0,0 @@
type FileOverviewMetadata = {
fileChecksum: string;
fileName: string;
fileSizeInBytes: number;
fileType: FileType;
fileUUID: string;
};
Loading…
Cancel
Save