fix(striker-ui): add new file management components

main
Tsu-ba-me 1 year ago
parent d8e214715a
commit 34fc75b3d7
  1. 223
      striker-ui/components/Files/AddFileForm.tsx
  2. 154
      striker-ui/components/Files/EditFileForm.tsx
  3. 217
      striker-ui/components/Files/FileInputGroup.tsx
  4. 383
      striker-ui/components/Files/ManageFilePanel.tsx
  5. 42
      striker-ui/components/Files/UploadFileProgress.tsx
  6. 9
      striker-ui/types/APIAnvil.d.ts
  7. 11
      striker-ui/types/APIFile.d.ts
  8. 62
      striker-ui/types/ManageFile.d.ts

@ -0,0 +1,223 @@
import { useFormik } from 'formik';
import {
ChangeEventHandler,
FC,
ReactElement,
useCallback,
useMemo,
useRef,
} from 'react';
import { v4 as uuidv4 } from 'uuid';
import ActionGroup from '../ActionGroup';
import api from '../../lib/api';
import ContainedButton from '../ContainedButton';
import convertFormikErrorsToMessages from '../../lib/convertFormikErrorsToMessages';
import FileInputGroup from './FileInputGroup';
import FlexBox from '../FlexBox';
import handleAPIError from '../../lib/handleAPIError';
import MessageBox from '../MessageBox';
import MessageGroup from '../MessageGroup';
import fileListSchema from './schema';
import UploadFileProgress from './UploadFileProgress';
import useProtectedState from '../../hooks/useProtectedState';
const REQUEST_INCOMPLETE_UPLOAD_LIMIT = 99;
const setUploadProgress: (
previous: UploadFiles | undefined,
uuid: keyof UploadFiles,
progress: UploadFiles[string]['progress'],
) => UploadFiles | undefined = (previous, uuid, progress) => {
if (!previous) return previous;
previous[uuid].progress = progress;
return { ...previous };
};
const AddFileForm: FC<AddFileFormProps> = (props) => {
const { anvils, drHosts } = props;
const filePickerRef = useRef<HTMLInputElement>(null);
const [uploads, setUploads] = useProtectedState<UploadFiles | undefined>(
undefined,
);
const formik = useFormik<FileFormikValues>({
initialValues: {},
onSubmit: (values) => {
const files = Object.values(values);
setUploads(
files.reduce<UploadFiles>((previous, { file, name, uuid }) => {
if (!file) return previous;
previous[uuid] = { name, progress: 0, uuid };
return previous;
}, {}),
);
files.forEach(({ file, name, uuid }) => {
if (!file) return;
const data = new FormData();
data.append('file', new File([file], name, { ...file }));
api
.post('/file', data, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress: (
(fileUuid: string) =>
({ loaded, total }) => {
setUploads((previous) =>
setUploadProgress(
previous,
fileUuid,
Math.round(
(loaded / total) * REQUEST_INCOMPLETE_UPLOAD_LIMIT,
),
),
);
}
)(uuid),
})
.then(
((fileUuid: string) => () => {
setUploads((previous) =>
setUploadProgress(previous, fileUuid, 100),
);
})(uuid),
)
.catch((error) => {
handleAPIError(error);
});
});
},
validationSchema: fileListSchema,
});
const formikErrors = useMemo<Messages>(
() => convertFormikErrorsToMessages(formik.errors),
[formik.errors],
);
const disableProceed = useMemo<boolean>(
() =>
!formik.dirty ||
!formik.isValid ||
formik.isValidating ||
formik.isSubmitting,
[formik.dirty, formik.isSubmitting, formik.isValid, formik.isValidating],
);
const handleSelectFiles = useCallback<ChangeEventHandler<HTMLInputElement>>(
(event) => {
const {
target: { files },
} = event;
if (!files) return;
const values = Array.from(files).reduce<FileFormikValues>(
(previous, file) => {
const fileUuid = uuidv4();
previous[fileUuid] = {
file,
name: file.name,
uuid: fileUuid,
};
return previous;
},
{},
);
formik.setValues(values);
},
[formik],
);
const fileInputs = useMemo<ReactElement[]>(
() =>
formik.values &&
Object.values(formik.values).map((file) => {
const { uuid: fileUuid } = file;
return (
<FileInputGroup
anvils={anvils}
drHosts={drHosts}
fileUuid={fileUuid}
formik={formik}
key={fileUuid}
/>
);
}),
[anvils, drHosts, formik],
);
return (
<FlexBox>
<MessageBox>
Uploaded files will be listed automatically, but it may take a while for
larger files to finish uploading and appear on the list.
</MessageBox>
{uploads ? (
<>
<MessageBox>
This dialog can be closed after all uploads complete. Closing before
completion will stop the upload.
</MessageBox>
<UploadFileProgress uploads={uploads} />
</>
) : (
<FlexBox
component="form"
onSubmit={(event) => {
event.preventDefault();
formik.submitForm();
}}
>
<input
id="files"
multiple
name="files"
onChange={handleSelectFiles}
ref={filePickerRef}
style={{ display: 'none' }}
type="file"
/>
<ContainedButton
onClick={() => {
filePickerRef.current?.click();
}}
>
Browse
</ContainedButton>
{fileInputs}
<MessageGroup count={1} messages={formikErrors} />
<ActionGroup
actions={[
{
background: 'blue',
children: 'Add',
disabled: disableProceed,
type: 'submit',
},
]}
/>
</FlexBox>
)}
</FlexBox>
);
};
export default AddFileForm;

@ -0,0 +1,154 @@
import { useFormik } from 'formik';
import { FC, useMemo } from 'react';
import ActionGroup from '../ActionGroup';
import api from '../../lib/api';
import convertFormikErrorsToMessages from '../../lib/convertFormikErrorsToMessages';
import FileInputGroup from './FileInputGroup';
import FlexBox from '../FlexBox';
import MessageGroup from '../MessageGroup';
import fileListSchema from './schema';
const toEditFileRequestBody = (
file: FileFormikFile,
pfile: APIFileDetail,
): APIEditFileRequestBody | undefined => {
const { locations, name: fileName, type: fileType, uuid: fileUUID } = file;
if (!locations || !fileType) return undefined;
const fileLocations: APIEditFileRequestBody['fileLocations'] = [];
Object.entries(locations.anvils).reduce<
APIEditFileRequestBody['fileLocations']
>((previous, [anvilUuid, { active: isFileLocationActive }]) => {
const {
anvils: {
[anvilUuid]: { locationUuids },
},
} = pfile;
const current = locationUuids.map<
APIEditFileRequestBody['fileLocations'][number]
>((fileLocationUUID) => ({
fileLocationUUID,
isFileLocationActive,
}));
previous.push(...current);
return previous;
}, fileLocations);
Object.entries(locations.drHosts).reduce<
APIEditFileRequestBody['fileLocations']
>((previous, [drHostUuid, { active: isFileLocationActive }]) => {
const {
hosts: {
[drHostUuid]: { locationUuids },
},
} = pfile;
const current = locationUuids.map<
APIEditFileRequestBody['fileLocations'][number]
>((fileLocationUUID) => ({
fileLocationUUID,
isFileLocationActive,
}));
previous.push(...current);
return previous;
}, fileLocations);
return { fileLocations, fileName, fileType, fileUUID };
};
const EditFileForm: FC<EditFileFormProps> = (props) => {
const { anvils, drHosts, previous: file } = props;
const formikInitialValues = useMemo<FileFormikValues>(() => {
const { locations, name, type, uuid } = file;
return {
[uuid]: {
locations: Object.values(locations).reduce<FileFormikLocations>(
(previous, { active, anvilUuid, hostUuid }) => {
let category: keyof FileFormikLocations = 'anvils';
let id = anvilUuid;
if (hostUuid in drHosts) {
category = 'drHosts';
id = hostUuid;
}
previous[category][id] = { active };
return previous;
},
{ anvils: {}, drHosts: {} },
),
name,
type,
uuid,
},
};
}, [drHosts, file]);
const formik = useFormik<FileFormikValues>({
initialValues: formikInitialValues,
onSubmit: (values) => {
const body = toEditFileRequestBody(values[file.uuid], file);
api.put(`/file/${file.uuid}`, body);
},
validationSchema: fileListSchema,
});
const formikErrors = useMemo<Messages>(
() => convertFormikErrorsToMessages(formik.errors),
[formik.errors],
);
const disableProceed = useMemo<boolean>(
() =>
!formik.dirty ||
!formik.isValid ||
formik.isValidating ||
formik.isSubmitting,
[formik.dirty, formik.isSubmitting, formik.isValid, formik.isValidating],
);
return (
<FlexBox
component="form"
onSubmit={(event) => {
event.preventDefault();
formik.submitForm();
}}
>
<FileInputGroup
anvils={anvils}
drHosts={drHosts}
fileUuid={file.uuid}
formik={formik}
showSyncInputGroup
showTypeInput
/>
<MessageGroup count={1} messages={formikErrors} />
<ActionGroup
actions={[
{
background: 'blue',
children: 'Edit',
disabled: disableProceed,
type: 'submit',
},
]}
/>
</FlexBox>
);
};
export default EditFileForm;

@ -0,0 +1,217 @@
import { FormGroup } from '@mui/material';
import { cloneDeep, debounce } from 'lodash';
import { FC, useCallback, useMemo } from 'react';
import { UPLOAD_FILE_TYPES_ARRAY } from '../../lib/consts/UPLOAD_FILE_TYPES';
import FlexBox from '../FlexBox';
import List from '../List';
import OutlinedInputWithLabel from '../OutlinedInputWithLabel';
import { ExpandablePanel } from '../Panels';
import SelectWithLabel from '../SelectWithLabel';
import { BodyText } from '../Text';
import UncontrolledInput from '../UncontrolledInput';
const FileInputGroup: FC<FileInputGroupProps> = (props) => {
const {
anvils,
drHosts,
fileUuid: fuuid,
formik,
showSyncInputGroup,
showTypeInput,
} = props;
const { handleBlur, handleChange } = formik;
const debounceChangeEventHandler = useMemo(
() => debounce(handleChange, 500),
[handleChange],
);
const { nameChain, locationsChain, typeChain } = useMemo(
() => ({
nameChain: `${fuuid}.name`,
locationsChain: `${fuuid}.locations`,
typeChain: `${fuuid}.type`,
}),
[fuuid],
);
const handleCheckAllLocations = useCallback(
(type: keyof FileFormikLocations, checked: boolean) => {
formik.setValues((previous) => {
const current = cloneDeep(previous);
const locations = current[fuuid].locations?.[type];
if (!locations) return previous;
Object.keys(locations).forEach((key) => {
locations[key].active = checked;
});
return current;
});
},
[formik, fuuid],
);
const getAllLocationsCheckboxProps = useCallback(
(type: keyof FileFormikLocations): CheckboxProps => {
const locations = formik.values[fuuid].locations?.[type];
if (!locations) return {};
return {
checked: Object.values(locations).every(({ active }) => active),
onChange: (event, checked) => {
handleCheckAllLocations(type, checked);
},
};
},
[formik.values, fuuid, handleCheckAllLocations],
);
const getLocationCheckboxProps = useCallback(
(type: keyof FileFormikLocations, uuid: string): CheckboxProps => {
const gridChain = `${locationsChain}.${type}.${uuid}`;
const activeChain = `${gridChain}.active`;
return {
id: activeChain,
name: activeChain,
checked: formik.values[fuuid].locations?.[type][uuid].active,
onBlur: handleBlur,
onChange: handleChange,
};
},
[formik.values, fuuid, handleBlur, handleChange, locationsChain],
);
const enableCheckAllLocations = useCallback(
(type: keyof FileFormikLocations) => {
const locations = formik.values[fuuid].locations?.[type];
return locations && Object.keys(locations).length > 1;
},
[formik.values, fuuid],
);
const nameInput = useMemo(
() => (
<UncontrolledInput
input={
<OutlinedInputWithLabel
id={nameChain}
label="File name"
name={nameChain}
onBlur={handleBlur}
onChange={debounceChangeEventHandler}
value={formik.values[fuuid].name}
/>
}
/>
),
[debounceChangeEventHandler, formik.values, fuuid, handleBlur, nameChain],
);
const syncNodeInputGroup = useMemo(
() =>
showSyncInputGroup && (
<ExpandablePanel
header="Sync with node(s)"
panelProps={{ mb: 0, mt: 0, width: '100%' }}
>
<List
allowCheckAll={enableCheckAllLocations('anvils')}
allowCheckItem
edit
header
listItems={anvils}
getListCheckboxProps={() => getAllLocationsCheckboxProps('anvils')}
getListItemCheckboxProps={(uuid) =>
getLocationCheckboxProps('anvils', uuid)
}
renderListItem={(anvilUuid, { description, name }) => (
<BodyText>
{name}: {description}
</BodyText>
)}
/>
</ExpandablePanel>
),
[
anvils,
enableCheckAllLocations,
getAllLocationsCheckboxProps,
getLocationCheckboxProps,
showSyncInputGroup,
],
);
const syncDrHostInputGroup = useMemo(
() =>
showSyncInputGroup && (
<ExpandablePanel
header="Sync with DR host(s)"
panelProps={{ mb: 0, mt: 0, width: '100%' }}
>
<List
allowCheckAll={enableCheckAllLocations('drHosts')}
allowCheckItem
edit
header
listItems={drHosts}
getListCheckboxProps={() => getAllLocationsCheckboxProps('drHosts')}
getListItemCheckboxProps={(uuid) =>
getLocationCheckboxProps('drHosts', uuid)
}
renderListItem={(anvilUuid, { hostName }) => (
<BodyText>{hostName}</BodyText>
)}
/>
</ExpandablePanel>
),
[
drHosts,
enableCheckAllLocations,
getAllLocationsCheckboxProps,
getLocationCheckboxProps,
showSyncInputGroup,
],
);
const typeInput = useMemo(
() =>
showTypeInput && (
<SelectWithLabel
id={typeChain}
label="File type"
name={typeChain}
onBlur={handleBlur}
onChange={handleChange}
selectItems={UPLOAD_FILE_TYPES_ARRAY.map(
([value, [, displayValue]]) => ({
displayValue,
value,
}),
)}
value={formik.values[fuuid].type}
/>
),
[formik.values, fuuid, handleBlur, handleChange, showTypeInput, typeChain],
);
return (
<FormGroup sx={{ '& > :not(:first-child)': { marginTop: '1em' } }}>
<FlexBox sm="row" xs="column">
{nameInput}
{typeInput}
</FlexBox>
{syncNodeInputGroup}
{syncDrHostInputGroup}
</FormGroup>
);
};
export default FileInputGroup;

@ -0,0 +1,383 @@
import { dSizeStr } from 'format-data-size';
import { FC, useCallback, useMemo, useRef, useState } from 'react';
import API_BASE_URL from '../../lib/consts/API_BASE_URL';
import { UPLOAD_FILE_TYPES } from '../../lib/consts/UPLOAD_FILE_TYPES';
import AddFileForm from './AddFileForm';
import api from '../../lib/api';
import ConfirmDialog from '../ConfirmDialog';
import { DialogWithHeader } from '../Dialog';
import Divider from '../Divider';
import EditFileForm from './EditFileForm';
import FlexBox from '../FlexBox';
import handleAPIError from '../../lib/handleAPIError';
import List from '../List';
import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup';
import { Panel, PanelHeader } from '../Panels';
import periodicFetch from '../../lib/fetchers/periodicFetch';
import Spinner from '../Spinner';
import { BodyText, HeaderText, MonoText } from '../Text';
import useChecklist from '../../hooks/useChecklist';
import useConfirmDialogProps from '../../hooks/useConfirmDialogProps';
import useFetch from '../../hooks/useFetch';
import useProtectedState from '../../hooks/useProtectedState';
const toAnvilOverviewHostList = (
data: APIAnvilOverviewArray[number]['hosts'],
) =>
data.reduce<APIAnvilOverview['hosts']>(
(previous, { hostName: name, hostType: type, hostUUID: uuid }) => {
previous[uuid] = { name, type, uuid };
return previous;
},
{},
);
const toAnvilOverviewList = (data: APIAnvilOverviewArray) =>
data.reduce<APIAnvilOverviewList>(
(
previous,
{
anvilDescription: description,
anvilName: name,
anvilUUID: uuid,
hosts,
},
) => {
previous[uuid] = {
description,
hosts: toAnvilOverviewHostList(hosts),
name,
uuid,
};
return previous;
},
{},
);
const toFileOverviewList = (rows: string[][]) =>
rows.reduce<APIFileOverviewList>((previous, row) => {
const [uuid, name, size, type, checksum] = row;
previous[uuid] = {
checksum,
name,
size,
type: type as FileType,
uuid,
};
return previous;
}, {});
const toFileDetail = (rows: string[][]) => {
const { 0: first } = rows;
if (!first) return undefined;
const [uuid, name, size, type, checksum] = first;
return rows.reduce<APIFileDetail>(
(previous, row) => {
const {
5: locationUuid,
6: locationActive,
7: anvilUuid,
8: anvilName,
9: anvilDescription,
10: hostUuid,
11: hostName,
12: hostType,
} = row;
if (!previous.anvils[anvilUuid]) {
previous.anvils[anvilUuid] = {
description: anvilDescription,
locationUuids: [],
name: anvilName,
uuid: anvilUuid,
};
}
if (!previous.hosts[hostUuid]) {
previous.hosts[hostUuid] = {
locationUuids: [],
name: hostName,
type: hostType,
uuid: hostUuid,
};
}
if (hostType === 'dr') {
previous.hosts[hostUuid].locationUuids.push(locationUuid);
} else {
previous.anvils[anvilUuid].locationUuids.push(locationUuid);
}
const active = Number(locationActive) === 1;
previous.locations[locationUuid] = {
anvilUuid,
active,
hostUuid,
uuid: locationUuid,
};
return previous;
},
{
anvils: {},
checksum,
hosts: {},
locations: {},
name,
size,
type: type as FileType,
uuid,
},
);
};
const ManageFilePanel: FC = () => {
const addFormDialogRef = useRef<DialogForwardedRefContent>(null);
const confirmDialogRef = useRef<ConfirmDialogForwardedRefContent>({});
const editFormDialogRef = useRef<DialogForwardedRefContent>(null);
const messageGroupRef = useRef<MessageGroupForwardedRefContent>({});
const [confirmDialogProps, setConfirmDialogProps] = useConfirmDialogProps();
const [edit, setEdit] = useState<boolean>(false);
const [file, setFile] = useProtectedState<APIFileDetail | undefined>(
undefined,
);
const [loadingFile, setLoadingFile] = useProtectedState<boolean>(false);
const { data: rows, isLoading: loadingFiles } = periodicFetch<string[][]>(
`${API_BASE_URL}/file`,
);
const files = useMemo(
() => (rows ? toFileOverviewList(rows) : undefined),
[rows],
);
const {
buildDeleteDialogProps,
checks,
getCheck,
hasAllChecks,
hasChecks,
multipleItems,
setAllChecks,
setCheck,
} = useChecklist({
list: files,
});
const setApiMessage = useCallback(
(message: Message) =>
messageGroupRef.current.setMessage?.call(null, 'api', message),
[],
);
const getFileDetail = useCallback(
(fileUuid: string) => {
setLoadingFile(true);
api
.get<string[][]>(`file/${fileUuid}`)
.then(({ data }) => {
setFile(toFileDetail(data));
})
.catch((error) => {
const emsg = handleAPIError(error);
emsg.children = <>Failed to get file detail. {emsg.children}</>;
setApiMessage(emsg);
})
.finally(() => {
setLoadingFile(false);
});
},
[setApiMessage, setFile, setLoadingFile],
);
const { data: rawAnvils, loading: loadingAnvils } =
useFetch<APIAnvilOverviewArray>('/anvil', {
onError: (error) => {
setApiMessage({
children: <>Failed to get node list. {error}</>,
type: 'warning',
});
},
});
const anvils = useMemo(
() => rawAnvils && toAnvilOverviewList(rawAnvils),
[rawAnvils],
);
const { data: drHosts, loading: loadingDrHosts } =
useFetch<APIHostOverviewList>('/host?types=dr', {
onError: (error) => {
setApiMessage({
children: <>Failed to get DR host list. {error}</>,
type: 'warning',
});
},
});
const list = useMemo(
() => (
<List
allowCheckAll={multipleItems}
allowEdit
allowItemButton={edit}
disableDelete={!hasChecks}
edit={edit}
getListCheckboxProps={() => ({
checked: hasAllChecks,
onChange: (event, checked) => {
setAllChecks(checked);
},
})}
getListItemCheckboxProps={(uuid) => ({
checked: getCheck(uuid),
onChange: (event, checked) => {
setCheck(uuid, checked);
},
})}
header
listEmpty="No file(s) found."
listItems={files}
onAdd={() => {
addFormDialogRef.current?.setOpen(true);
}}
onDelete={() => {
setConfirmDialogProps(
buildDeleteDialogProps({
onProceedAppend: () => {
checks.forEach((fileUuid) => api.delete(`/file/${fileUuid}`));
},
getConfirmDialogTitle: (count) =>
`Delete the following ${count} file(s)?`,
renderEntry: ({ key }) => (
<BodyText>{files?.[key].name}</BodyText>
),
}),
);
confirmDialogRef.current.setOpen?.call(null, true);
}}
onEdit={() => {
setEdit((previous) => !previous);
}}
onItemClick={(value, uuid) => {
editFormDialogRef.current?.setOpen(true);
getFileDetail(uuid);
}}
renderListItem={(uuid, { checksum, name, size, type }) => (
<FlexBox columnSpacing={0} fullWidth md="row" xs="column">
<FlexBox spacing={0} flexGrow={1}>
<FlexBox row spacing=".5em">
<MonoText>{name}</MonoText>
<Divider flexItem orientation="vertical" />
<BodyText>{UPLOAD_FILE_TYPES.get(type)?.[1]}</BodyText>
</FlexBox>
<BodyText>{dSizeStr(size, { toUnit: 'ibyte' })}</BodyText>
</FlexBox>
<MonoText>{checksum}</MonoText>
</FlexBox>
)}
/>
),
[
buildDeleteDialogProps,
checks,
edit,
files,
getCheck,
getFileDetail,
hasAllChecks,
hasChecks,
multipleItems,
setAllChecks,
setCheck,
setConfirmDialogProps,
],
);
const panelContent = useMemo(
() => (loadingFiles ? <Spinner /> : list),
[loadingFiles, list],
);
const messageArea = useMemo(
() => (
<MessageGroup count={1} ref={messageGroupRef} usePlaceholder={false} />
),
[],
);
const loadingAddForm = useMemo<boolean>(
() => loadingFiles || loadingAnvils || loadingDrHosts,
[loadingAnvils, loadingDrHosts, loadingFiles],
);
const loadingEditForm = useMemo<boolean>(
() => loadingFiles || loadingAnvils || loadingDrHosts || loadingFile,
[loadingAnvils, loadingDrHosts, loadingFile, loadingFiles],
);
const addForm = useMemo(
() =>
anvils && drHosts && <AddFileForm anvils={anvils} drHosts={drHosts} />,
[anvils, drHosts],
);
const editForm = useMemo(
() =>
anvils &&
drHosts &&
file && (
<EditFileForm anvils={anvils} drHosts={drHosts} previous={file} />
),
[anvils, drHosts, file],
);
return (
<>
<Panel>
<PanelHeader>
<HeaderText>Files</HeaderText>
</PanelHeader>
{messageArea}
{panelContent}
</Panel>
<DialogWithHeader
header="Add file(s)"
loading={loadingAddForm}
ref={addFormDialogRef}
showClose
wide
>
{addForm}
</DialogWithHeader>
<DialogWithHeader
header={`Update file ${file?.name}`}
loading={loadingEditForm}
ref={editFormDialogRef}
showClose
wide
>
{editForm}
</DialogWithHeader>
<ConfirmDialog wide {...confirmDialogProps} ref={confirmDialogRef} />
</>
);
};
export default ManageFilePanel;

@ -0,0 +1,42 @@
import { Box as MuiBox } from '@mui/material';
import { FC } from 'react';
import { ProgressBar } from '../Bars';
import FlexBox from '../FlexBox';
import { BodyText } from '../Text';
const UploadFileProgress: FC<UploadFileProgressProps> = (props) => {
const { uploads } = props;
return (
<FlexBox columnSpacing=".2em">
{Object.values(uploads).map(({ name, progress, uuid }) => (
<MuiBox
key={`upload-${uuid}`}
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>{name}</BodyText>
<ProgressBar progressPercentage={progress} />
</MuiBox>
))}
</FlexBox>
);
};
export default UploadFileProgress;

@ -88,15 +88,22 @@ type AnvilList = {
};
type APIAnvilOverviewArray = Array<{
anvilDescription: string;
anvilName: string;
anvilUUID: string;
hosts: Array<{ hostName: string; hostUUID: string }>;
hosts: Array<{
hostName: string;
hostType: string;
hostUUID: string;
}>;
}>;
type APIAnvilOverview = {
description: string;
hosts: {
[uuid: string]: {
name: string;
type: string;
uuid: string;
};
};

@ -19,6 +19,7 @@ type APIFileDetail = APIFileOverview & {
[uuid: string]: {
locationUuids: string[];
name: string;
type: string;
uuid: string;
};
};
@ -35,3 +36,13 @@ type APIFileDetail = APIFileOverview & {
type APIFileOverviewList = {
[uuid: string]: APIFileOverview;
};
type APIEditFileRequestBody = {
fileName: string;
fileType: FileType;
fileUUID: string;
fileLocations: Array<{
fileLocationUUID: string;
isFileLocationActive: boolean;
}>;
};

@ -0,0 +1,62 @@
type FileFormikLocations = {
anvils: {
[anvilUuid: string]: {
active: boolean;
};
};
drHosts: {
[hostUuid: string]: {
active: boolean;
};
};
};
type FileFormikFile = {
file?: File;
locations?: FileFormikLocations;
name: string;
type?: FileType;
uuid: string;
};
type FileFormikValues = {
[fileUuid: string]: FileFormikFile;
};
/** ---------- Component types ---------- */
/** FileInputGroup */
type FileInputGroupOptionalProps = {
showSyncInputGroup?: boolean;
showTypeInput?: boolean;
};
type FileInputGroupProps = FileInputGroupOptionalProps & {
anvils: APIAnvilOverviewList;
drHosts: APIHostOverviewList;
fileUuid: string;
formik: ReturnType<typeof import('formik').useFormik<FileFormikValues>>;
};
/** AddFileForm */
type UploadFiles = {
[fileUuid: string]: Pick<FileFormikFile, 'name' | 'uuid'> & {
progress: number;
};
};
type AddFileFormProps = Pick<FileInputGroupProps, 'anvils' | 'drHosts'>;
/** EditFileForm */
type EditFileFormProps = Pick<FileInputGroupProps, 'anvils' | 'drHosts'> & {
previous: APIFileDetail;
};
/** UploadFileProgress */
type UploadFileProgressProps = {
uploads: UploadFiles;
};
Loading…
Cancel
Save