Merge pull request #473 from ylei-tsubame/replace-form-validation
Web UI: replace file manager with external form management and validation librariesmain
commit
74d8e6d76a
91 changed files with 2300 additions and 379 deletions
File diff suppressed because one or more lines are too long
@ -0,0 +1,36 @@ |
||||
import { styled } from '@mui/material'; |
||||
import { FC, ReactElement, useMemo } from 'react'; |
||||
import { v4 as uuidv4 } from 'uuid'; |
||||
|
||||
import ContainedButton from './ContainedButton'; |
||||
import FlexBox from './FlexBox'; |
||||
import Spinner from './Spinner'; |
||||
|
||||
const FlexEndBox = styled(FlexBox)({ |
||||
justifyContent: 'flex-end', |
||||
width: '100%', |
||||
}); |
||||
|
||||
const ActionGroup: FC<ActionGroupProps> = (props) => { |
||||
const { actions = [], loading } = props; |
||||
|
||||
const elements = useMemo( |
||||
() => |
||||
actions.map<ReactElement>((actionProps) => ( |
||||
<ContainedButton key={uuidv4()} {...actionProps}> |
||||
{actionProps.children} |
||||
</ContainedButton> |
||||
)), |
||||
[actions], |
||||
); |
||||
|
||||
return loading ? ( |
||||
<Spinner mt={0} /> |
||||
) : ( |
||||
<FlexEndBox row spacing=".5em"> |
||||
{elements} |
||||
</FlexEndBox> |
||||
); |
||||
}; |
||||
|
||||
export default ActionGroup; |
@ -1,34 +1,62 @@ |
||||
import { |
||||
Button as MUIButton, |
||||
Button as MuiButton, |
||||
buttonClasses as muiButtonClasses, |
||||
SxProps, |
||||
Theme, |
||||
styled, |
||||
} from '@mui/material'; |
||||
import { FC, useMemo } from 'react'; |
||||
import { FC } from 'react'; |
||||
|
||||
import { BLACK, DISABLED, GREY } from '../lib/consts/DEFAULT_THEME'; |
||||
import { |
||||
BLACK, |
||||
BLUE, |
||||
DISABLED, |
||||
GREY, |
||||
RED, |
||||
TEXT, |
||||
} from '../lib/consts/DEFAULT_THEME'; |
||||
|
||||
const MAP_TO_COLOUR: Record<ContainedButtonBackground, string> = { |
||||
blue: BLUE, |
||||
normal: GREY, |
||||
red: RED, |
||||
}; |
||||
|
||||
const ContainedButton: FC<ContainedButtonProps> = ({ sx, ...restProps }) => { |
||||
const combinedSx = useMemo<SxProps<Theme>>( |
||||
() => ({ |
||||
backgroundColor: GREY, |
||||
color: BLACK, |
||||
textTransform: 'none', |
||||
const BaseStyle = styled(MuiButton)({ |
||||
backgroundColor: GREY, |
||||
color: BLACK, |
||||
textTransform: 'none', |
||||
|
||||
'&:hover': { |
||||
backgroundColor: `${GREY}F0`, |
||||
}, |
||||
'&:hover': { |
||||
backgroundColor: `${GREY}F0`, |
||||
}, |
||||
|
||||
[`&.${muiButtonClasses.disabled}`]: { |
||||
backgroundColor: DISABLED, |
||||
}, |
||||
[`&.${muiButtonClasses.disabled}`]: { |
||||
backgroundColor: DISABLED, |
||||
}, |
||||
}); |
||||
|
||||
...sx, |
||||
}), |
||||
[sx], |
||||
); |
||||
const Base: FC<ContainedButtonProps> = (props) => ( |
||||
<BaseStyle variant="contained" {...props} /> |
||||
); |
||||
|
||||
return <MUIButton variant="contained" {...restProps} sx={combinedSx} />; |
||||
}; |
||||
const ContainedButton = styled(Base)((props) => { |
||||
const { background = 'normal' } = props; |
||||
|
||||
let bg: string | undefined; |
||||
let color: string | undefined; |
||||
|
||||
if (background !== 'normal') { |
||||
bg = MAP_TO_COLOUR[background]; |
||||
color = TEXT; |
||||
} |
||||
|
||||
return { |
||||
backgroundColor: bg, |
||||
color, |
||||
|
||||
'&:hover': { |
||||
backgroundColor: `${bg}F0`, |
||||
}, |
||||
}; |
||||
}); |
||||
|
||||
export default ContainedButton; |
||||
|
@ -0,0 +1,90 @@ |
||||
import { Dialog as MuiDialog, SxProps, Theme } from '@mui/material'; |
||||
import { |
||||
ForwardRefExoticComponent, |
||||
PropsWithChildren, |
||||
ReactNode, |
||||
RefAttributes, |
||||
createContext, |
||||
forwardRef, |
||||
useImperativeHandle, |
||||
useMemo, |
||||
useState, |
||||
} from 'react'; |
||||
|
||||
import { Panel } from '../Panels'; |
||||
import Spinner from '../Spinner'; |
||||
|
||||
const DialogContext = createContext<DialogContextContent | undefined>( |
||||
undefined, |
||||
); |
||||
|
||||
const Dialog: ForwardRefExoticComponent< |
||||
PropsWithChildren<DialogProps> & RefAttributes<DialogForwardedRefContent> |
||||
> = forwardRef<DialogForwardedRefContent, DialogProps>((props, ref) => { |
||||
const { |
||||
children: externalChildren, |
||||
dialogProps = {}, |
||||
loading, |
||||
openInitially = false, |
||||
wide, |
||||
} = props; |
||||
|
||||
const { |
||||
// Do not initialize the external open state because we need it to
|
||||
// determine whether the dialog is controlled or uncontrolled.
|
||||
open: externalOpen, |
||||
PaperProps: paperProps = {}, |
||||
...restDialogProps |
||||
} = dialogProps; |
||||
|
||||
const { sx: externalPaperSx, ...restPaperProps } = paperProps; |
||||
|
||||
const [controlOpen, setControlOpen] = useState<boolean>(openInitially); |
||||
|
||||
const open = useMemo<boolean>( |
||||
() => externalOpen ?? controlOpen, |
||||
[controlOpen, externalOpen], |
||||
); |
||||
|
||||
const children = useMemo<ReactNode>( |
||||
() => (loading ? <Spinner mt={0} /> : externalChildren), |
||||
[externalChildren, loading], |
||||
); |
||||
|
||||
const paperSx = useMemo<SxProps<Theme>>( |
||||
() => ({ |
||||
minWidth: wide ? { xs: 'calc(100%)', md: '50em' } : null, |
||||
overflow: 'visible', |
||||
...externalPaperSx, |
||||
}), |
||||
[externalPaperSx, wide], |
||||
); |
||||
|
||||
useImperativeHandle( |
||||
ref, |
||||
() => ({ |
||||
open, |
||||
setOpen: setControlOpen, |
||||
}), |
||||
[open], |
||||
); |
||||
|
||||
return ( |
||||
<MuiDialog |
||||
open={open} |
||||
PaperComponent={Panel} |
||||
PaperProps={{ ...restPaperProps, sx: paperSx }} |
||||
{...restDialogProps} |
||||
> |
||||
<DialogContext.Provider value={{ open, setOpen: setControlOpen }}> |
||||
{children} |
||||
</DialogContext.Provider> |
||||
</MuiDialog> |
||||
); |
||||
}); |
||||
|
||||
Dialog.displayName = 'Dialog'; |
||||
|
||||
export { DialogContext }; |
||||
|
||||
export default Dialog; |
@ -0,0 +1,98 @@ |
||||
import { FC, useCallback, useContext, useMemo } from 'react'; |
||||
|
||||
import ActionGroup from '../ActionGroup'; |
||||
import { DialogContext } from './Dialog'; |
||||
|
||||
const handleAction: ExtendableEventHandler<ButtonClickEventHandler> = ( |
||||
{ handlers: { base, origin } }, |
||||
...args |
||||
) => { |
||||
base?.call(null, ...args); |
||||
origin?.call(null, ...args); |
||||
}; |
||||
|
||||
const DialogActionGroup: FC<DialogActionGroupProps> = (props) => { |
||||
const { |
||||
cancelProps, |
||||
closeOnProceed, |
||||
loading = false, |
||||
onCancel = handleAction, |
||||
onProceed = handleAction, |
||||
proceedColour, |
||||
proceedProps, |
||||
// Dependents
|
||||
cancelChildren = cancelProps?.children, |
||||
proceedChildren = proceedProps?.children, |
||||
} = props; |
||||
|
||||
const dialogContext = useContext(DialogContext); |
||||
|
||||
const cancelHandler = useCallback<ButtonClickEventHandler>( |
||||
(...args) => |
||||
onCancel( |
||||
{ |
||||
handlers: { |
||||
base: () => { |
||||
dialogContext?.setOpen(false); |
||||
}, |
||||
origin: cancelProps?.onClick, |
||||
}, |
||||
}, |
||||
...args, |
||||
), |
||||
[cancelProps?.onClick, dialogContext, onCancel], |
||||
); |
||||
|
||||
const proceedHandler = useCallback<ButtonClickEventHandler>( |
||||
(...args) => |
||||
onProceed( |
||||
{ |
||||
handlers: { |
||||
base: () => { |
||||
if (closeOnProceed) { |
||||
dialogContext?.setOpen(false); |
||||
} |
||||
}, |
||||
origin: proceedProps?.onClick, |
||||
}, |
||||
}, |
||||
...args, |
||||
), |
||||
[closeOnProceed, dialogContext, onProceed, proceedProps?.onClick], |
||||
); |
||||
|
||||
const actions = useMemo( |
||||
() => ( |
||||
<ActionGroup |
||||
actions={[ |
||||
{ |
||||
...cancelProps, |
||||
children: cancelChildren, |
||||
onClick: cancelHandler, |
||||
}, |
||||
{ |
||||
background: proceedColour, |
||||
...proceedProps, |
||||
children: proceedChildren, |
||||
onClick: proceedHandler, |
||||
}, |
||||
]} |
||||
loading={loading} |
||||
/> |
||||
), |
||||
[ |
||||
cancelChildren, |
||||
cancelHandler, |
||||
cancelProps, |
||||
loading, |
||||
proceedChildren, |
||||
proceedColour, |
||||
proceedHandler, |
||||
proceedProps, |
||||
], |
||||
); |
||||
|
||||
return actions; |
||||
}; |
||||
|
||||
export default DialogActionGroup; |
@ -0,0 +1,41 @@ |
||||
import { FC, ReactNode, useContext, useMemo } from 'react'; |
||||
|
||||
import { DialogContext } from './Dialog'; |
||||
import IconButton from '../IconButton'; |
||||
import { PanelHeader } from '../Panels'; |
||||
import sxstring from '../../lib/sxstring'; |
||||
import { HeaderText } from '../Text'; |
||||
|
||||
const DialogHeader: FC<DialogHeaderProps> = (props) => { |
||||
const { children, showClose } = props; |
||||
|
||||
const dialogContext = useContext(DialogContext); |
||||
|
||||
const title = useMemo<ReactNode>( |
||||
() => sxstring(children, HeaderText), |
||||
[children], |
||||
); |
||||
|
||||
const close = useMemo<ReactNode>( |
||||
() => |
||||
showClose && ( |
||||
<IconButton |
||||
mapPreset="close" |
||||
onClick={() => { |
||||
dialogContext?.setOpen(false); |
||||
}} |
||||
size="small" |
||||
/> |
||||
), |
||||
[dialogContext, showClose], |
||||
); |
||||
|
||||
return ( |
||||
<PanelHeader> |
||||
{title} |
||||
{close} |
||||
</PanelHeader> |
||||
); |
||||
}; |
||||
|
||||
export default DialogHeader; |
@ -0,0 +1,9 @@ |
||||
import { styled } from '@mui/material'; |
||||
|
||||
import ScrollBox from '../ScrollBox'; |
||||
|
||||
const DialogScrollBox = styled(ScrollBox)({ |
||||
maxHeight: '60vh', |
||||
}); |
||||
|
||||
export default DialogScrollBox; |
@ -0,0 +1,43 @@ |
||||
import { |
||||
ForwardRefExoticComponent, |
||||
PropsWithChildren, |
||||
RefAttributes, |
||||
forwardRef, |
||||
} from 'react'; |
||||
|
||||
import Dialog from './Dialog'; |
||||
import DialogHeader from './DialogHeader'; |
||||
|
||||
const DialogWithHeader: ForwardRefExoticComponent< |
||||
PropsWithChildren<DialogWithHeaderProps> & |
||||
RefAttributes<DialogForwardedRefContent> |
||||
> = forwardRef<DialogForwardedRefContent, DialogWithHeaderProps>( |
||||
(props, ref) => { |
||||
const { |
||||
children, |
||||
dialogProps, |
||||
header, |
||||
loading, |
||||
openInitially, |
||||
showClose, |
||||
wide, |
||||
} = props; |
||||
|
||||
return ( |
||||
<Dialog |
||||
dialogProps={dialogProps} |
||||
loading={loading} |
||||
openInitially={openInitially} |
||||
ref={ref} |
||||
wide={wide} |
||||
> |
||||
<DialogHeader showClose={showClose}>{header}</DialogHeader> |
||||
{children} |
||||
</Dialog> |
||||
); |
||||
}, |
||||
); |
||||
|
||||
DialogWithHeader.displayName = 'DialogWithHeader'; |
||||
|
||||
export default DialogWithHeader; |
@ -0,0 +1,13 @@ |
||||
import Dialog from './Dialog'; |
||||
import DialogActionGroup from './DialogActionGroup'; |
||||
import DialogHeader from './DialogHeader'; |
||||
import DialogScrollBox from './DialogScrollBox'; |
||||
import DialogWithHeader from './DialogWithHeader'; |
||||
|
||||
export { |
||||
Dialog, |
||||
DialogActionGroup as DialogActionArea, |
||||
DialogHeader, |
||||
DialogScrollBox, |
||||
DialogWithHeader, |
||||
}; |
@ -0,0 +1,233 @@ |
||||
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; |
||||
}, {}), |
||||
); |
||||
|
||||
const promises = files.reduce<Promise<void>[]>( |
||||
(chain, { file, name, uuid }) => { |
||||
if (!file) return chain; |
||||
|
||||
const data = new FormData(); |
||||
|
||||
data.append('file', new File([file], name, { ...file })); |
||||
|
||||
const promise = 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), |
||||
); |
||||
|
||||
chain.push(promise); |
||||
|
||||
return chain; |
||||
}, |
||||
[], |
||||
); |
||||
|
||||
Promise.all(promises).catch((error) => { |
||||
const emsg = handleAPIError(error); |
||||
|
||||
emsg.children = <>Failed to add file. {emsg.children}</>; |
||||
}); |
||||
}, |
||||
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,175 @@ |
||||
import { useFormik } from 'formik'; |
||||
import { FC, useCallback, useMemo, useRef } 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 handleAPIError from '../../lib/handleAPIError'; |
||||
import MessageGroup, { MessageGroupForwardedRefContent } 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 messageGroupRef = useRef<MessageGroupForwardedRefContent>({}); |
||||
|
||||
const setApiMessage = useCallback( |
||||
(message?: Message) => |
||||
messageGroupRef.current.setMessage?.call(null, 'api', message), |
||||
[], |
||||
); |
||||
|
||||
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, { setSubmitting }) => { |
||||
const body = toEditFileRequestBody(values[file.uuid], file); |
||||
|
||||
api |
||||
.put(`/file/${file.uuid}`, body) |
||||
.catch((error) => { |
||||
const emsg = handleAPIError(error); |
||||
|
||||
emsg.children = <>Failed to modify file. {emsg.children}</>; |
||||
|
||||
setApiMessage(emsg); |
||||
}) |
||||
.finally(() => { |
||||
setSubmitting(false); |
||||
}); |
||||
}, |
||||
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} ref={messageGroupRef} /> |
||||
<ActionGroup |
||||
loading={formik.isSubmitting} |
||||
actions={[ |
||||
{ |
||||
background: 'blue', |
||||
children: 'Edit', |
||||
disabled: disableProceed, |
||||
type: 'submit', |
||||
}, |
||||
]} |
||||
/> |
||||
</FlexBox> |
||||
); |
||||
}; |
||||
|
||||
export default EditFileForm; |
@ -0,0 +1,219 @@ |
||||
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: FileFormikValues) => { |
||||
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] as { |
||||
[uuid: string]: { active: boolean }; |
||||
}; |
||||
|
||||
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,388 @@ |
||||
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 |
||||
closeOnProceed |
||||
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; |
@ -0,0 +1,29 @@ |
||||
import * as yup from 'yup'; |
||||
|
||||
import buildYupDynamicObject from '../../lib/buildYupDynamicObject'; |
||||
|
||||
const fileLocationSchema = yup.object({ active: yup.boolean().required() }); |
||||
|
||||
const fileLocationAnvilSchema = yup.lazy((anvils) => |
||||
yup.object(buildYupDynamicObject(anvils, fileLocationSchema)), |
||||
); |
||||
|
||||
const fileLocationDrHostSchema = yup.lazy((drHosts) => |
||||
yup.object(buildYupDynamicObject(drHosts, fileLocationSchema)), |
||||
); |
||||
|
||||
const fileSchema = yup.object({ |
||||
locations: yup.object({ |
||||
anvils: fileLocationAnvilSchema, |
||||
drHosts: fileLocationDrHostSchema, |
||||
}), |
||||
name: yup.string().required(), |
||||
type: yup.string().oneOf(['iso', 'other', 'script']), |
||||
uuid: yup.string().uuid().required(), |
||||
}); |
||||
|
||||
const fileListSchema = yup.lazy((files) => |
||||
yup.object(buildYupDynamicObject(files, fileSchema)), |
||||
); |
||||
|
||||
export default fileListSchema; |
@ -0,0 +1,8 @@ |
||||
import { Box as MuiBox, styled } from '@mui/material'; |
||||
|
||||
const ScrollBox = styled(MuiBox)({ |
||||
overflowY: 'scroll', |
||||
paddingRight: '.4em', |
||||
}); |
||||
|
||||
export default ScrollBox; |
@ -0,0 +1,106 @@ |
||||
import { |
||||
ForwardedRef, |
||||
ReactElement, |
||||
cloneElement, |
||||
forwardRef, |
||||
useCallback, |
||||
useEffect, |
||||
useImperativeHandle, |
||||
useMemo, |
||||
useState, |
||||
} from 'react'; |
||||
|
||||
import INPUT_TYPES from '../lib/consts/INPUT_TYPES'; |
||||
import MAP_TO_VALUE_CONVERTER from '../lib/consts/MAP_TO_VALUE_CONVERTER'; |
||||
|
||||
const UncontrolledInput = forwardRef( |
||||
<ValueType extends keyof MapToInputType, InputElement extends ReactElement>( |
||||
props: UncontrolledInputProps<InputElement>, |
||||
ref: ForwardedRef<UncontrolledInputForwardedRefContent<ValueType>>, |
||||
) => { |
||||
const { |
||||
input, |
||||
onChange = ({ handlers: { base, origin } }, ...args) => { |
||||
base?.call(null, ...args); |
||||
origin?.call(null, ...args); |
||||
}, |
||||
onMount, |
||||
onUnmount, |
||||
} = props; |
||||
const { props: inputProps } = input; |
||||
|
||||
const { valueKey, valueType } = useMemo(() => { |
||||
const { type } = inputProps; |
||||
|
||||
let vkey: 'checked' | 'value' = 'value'; |
||||
let vtype: keyof MapToInputType = 'string'; |
||||
|
||||
if (type === INPUT_TYPES.checkbox) { |
||||
vkey = 'checked'; |
||||
vtype = 'boolean'; |
||||
} |
||||
|
||||
return { |
||||
valueKey: vkey, |
||||
valueType: vtype, |
||||
}; |
||||
}, [inputProps]); |
||||
|
||||
const { |
||||
onChange: inputOnChange, |
||||
[valueKey]: inputValue, |
||||
...restInputProps |
||||
} = inputProps; |
||||
|
||||
const [value, setValue] = useState<MapToInputType[ValueType]>(inputValue); |
||||
|
||||
const baseChangeEventHandler = useCallback<ReactChangeEventHandler>( |
||||
({ target: { [valueKey]: changed } }) => { |
||||
const converted = MAP_TO_VALUE_CONVERTER[valueType]( |
||||
changed, |
||||
) as MapToInputType[ValueType]; |
||||
|
||||
setValue(converted); |
||||
}, |
||||
[valueKey, valueType], |
||||
); |
||||
|
||||
const changeEventHandler = useCallback<ReactChangeEventHandler>( |
||||
(...args) => |
||||
onChange?.call( |
||||
null, |
||||
{ handlers: { base: baseChangeEventHandler, origin: inputOnChange } }, |
||||
...args, |
||||
), |
||||
[baseChangeEventHandler, inputOnChange, onChange], |
||||
); |
||||
|
||||
// Handle mount/unmount events; these only happen once hence no deps
|
||||
useEffect(() => { |
||||
onMount?.call(null); |
||||
|
||||
return onUnmount; |
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []); |
||||
|
||||
useImperativeHandle( |
||||
ref, |
||||
() => ({ |
||||
get: () => value, |
||||
set: setValue, |
||||
}), |
||||
[value], |
||||
); |
||||
|
||||
return cloneElement(input, { |
||||
...restInputProps, |
||||
onChange: changeEventHandler, |
||||
[valueKey]: value, |
||||
}); |
||||
}, |
||||
); |
||||
|
||||
UncontrolledInput.displayName = 'UncontrolledInput'; |
||||
|
||||
export default UncontrolledInput; |
@ -0,0 +1,29 @@ |
||||
import useSWR, { BareFetcher, SWRConfiguration } from 'swr'; |
||||
|
||||
import API_BASE_URL from '../lib/consts/API_BASE_URL'; |
||||
|
||||
import fetchJSON from '../lib/fetchers/fetchJSON'; |
||||
|
||||
type FetchHookResponse<D, E extends Error = Error> = { |
||||
data?: D; |
||||
error?: E; |
||||
loading: boolean; |
||||
}; |
||||
|
||||
const useFetch = <Data,>( |
||||
url: string, |
||||
options: SWRConfiguration<Data> & { |
||||
fetcher?: BareFetcher<Data>; |
||||
baseUrl?: string; |
||||
} = {}, |
||||
): FetchHookResponse<Data> => { |
||||
const { fetcher = fetchJSON, baseUrl = API_BASE_URL, ...config } = options; |
||||
|
||||
const { data, error } = useSWR<Data>(`${baseUrl}${url}`, fetcher, config); |
||||
|
||||
const loading = !error && !data; |
||||
|
||||
return { data, error, loading }; |
||||
}; |
||||
|
||||
export default useFetch; |
@ -0,0 +1,14 @@ |
||||
const buildYupDynamicObject = <S>( |
||||
obj: Record<string, S> | undefined, |
||||
schema: S, |
||||
): Record<string, S> | undefined => |
||||
obj && |
||||
Object.keys(obj).reduce<Record<string, S>>( |
||||
(previous, key) => ({ |
||||
...previous, |
||||
[key]: schema, |
||||
}), |
||||
{}, |
||||
); |
||||
|
||||
export default buildYupDynamicObject; |
@ -0,0 +1,26 @@ |
||||
const convertFormikErrorsToMessages = <Leaf extends string | undefined>( |
||||
errors: Tree<Leaf>, |
||||
{ |
||||
build = (mkey, err) => ({ children: err, type: 'warning' }), |
||||
chain = '', |
||||
}: { |
||||
build?: (msgkey: keyof Tree, error: Leaf) => Messages[keyof Messages]; |
||||
chain?: keyof Tree<Leaf>; |
||||
} = {}, |
||||
): Messages => |
||||
Object.entries(errors).reduce<Messages>((previous, [key, value]) => { |
||||
const extended = String(chain).length ? [chain, key].join('.') : key; |
||||
|
||||
if (typeof value === 'object') { |
||||
return { |
||||
...previous, |
||||
...convertFormikErrorsToMessages(value, { chain: extended }), |
||||
}; |
||||
} |
||||
|
||||
previous[extended] = build(extended, value); |
||||
|
||||
return previous; |
||||
}, {}); |
||||
|
||||
export default convertFormikErrorsToMessages; |
@ -0,0 +1,14 @@ |
||||
import { ReactNode, createElement } from 'react'; |
||||
|
||||
/** |
||||
* "jsx"/"tsx" + "string"; wraps input with wrapper if input is a string. |
||||
*/ |
||||
const sxstring = ( |
||||
children: ReactNode, |
||||
wrapper: CreatableComponent, |
||||
): ReactNode => |
||||
typeof children === 'string' |
||||
? createElement(wrapper, null, children) |
||||
: children; |
||||
|
||||
export default sxstring; |
@ -1 +0,0 @@ |
||||
self.__BUILD_MANIFEST=function(s,a,c,e,t,n,i,f,d,b,u,k,h,j,r,g){return{__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":[s,c,e,i,f,k,"static/chunks/717-8bd60b96d67fd464.js",a,t,n,d,h,j,"static/chunks/pages/index-7c2cb48473145987.js"],"/_error":["static/chunks/pages/_error-2280fa386d040b66.js"],"/anvil":[s,c,e,i,f,k,a,t,n,d,h,"static/chunks/pages/anvil-c0917177269e4c45.js"],"/config":[s,c,e,b,a,t,n,u,"static/chunks/pages/config-cb5dcd774a7f13bc.js"],"/file-manager":[s,c,e,i,"static/chunks/768-9ee3dcb62beecb53.js",a,t,"static/chunks/pages/file-manager-843b3cb0cc1119f6.js"],"/init":[s,c,i,f,b,r,a,t,n,d,g,"static/chunks/pages/init-124696b2707615f8.js"],"/login":[s,c,e,a,t,n,u,"static/chunks/pages/login-b5de0cd2f49998d6.js"],"/manage-element":[s,c,e,i,f,b,r,"static/chunks/195-d5fd184cc249f755.js",a,t,n,d,u,g,"static/chunks/pages/manage-element-c5172fe1e4c11fba.js"],"/server":[s,e,"static/chunks/227-a3756585a7ef09ae.js",a,j,"static/chunks/pages/server-db52258419acacf3.js"],sortedPages:["/","/_app","/_error","/anvil","/config","/file-manager","/init","/login","/manage-element","/server"]}}("static/chunks/382-f51344f6f9208507.js","static/chunks/62-532ed713980da8db.js","static/chunks/483-f8013e38dca1620d.js","static/chunks/894-e57948de523bcf96.js","static/chunks/780-e8b3396d257460a4.js","static/chunks/899-ec535b0f0a173e21.js","static/chunks/182-08683bbe95fbb010.js","static/chunks/614-0ce04fd295045ffe.js","static/chunks/140-ec935fb15330b98a.js","static/chunks/644-c7c6e21c71345aed.js","static/chunks/903-dc2a40be612a10c3.js","static/chunks/485-77798bccc4308d0e.js","static/chunks/825-1bb2d128cccc0e41.js","static/chunks/94-e103c3735f0e061b.js","static/chunks/676-6159ce853338cc1f.js","static/chunks/86-a6f7430ac8a027ff.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB(); |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@ |
||||
!function(){"use strict";var e={},t={};function n(r){var o=t[r];if(void 0!==o)return o.exports;var i=t[r]={id:r,loaded:!1,exports:{}},u=!0;try{e[r].call(i.exports,i,i.exports,n),u=!1}finally{u&&delete t[r]}return i.loaded=!0,i.exports}n.m=e,function(){var e=[];n.O=function(t,r,o,i){if(!r){var u=1/0;for(l=0;l<e.length;l++){r=e[l][0],o=e[l][1],i=e[l][2];for(var c=!0,f=0;f<r.length;f++)(!1&i||u>=i)&&Object.keys(n.O).every((function(e){return n.O[e](r[f])}))?r.splice(f--,1):(c=!1,i<u&&(u=i));if(c){e.splice(l--,1);var a=o();void 0!==a&&(t=a)}}return t}i=i||0;for(var l=e.length;l>0&&e[l-1][2]>i;l--)e[l]=e[l-1];e[l]=[r,o,i]}}(),n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,{a:t}),t},function(){var e,t=Object.getPrototypeOf?function(e){return Object.getPrototypeOf(e)}:function(e){return e.__proto__};n.t=function(r,o){if(1&o&&(r=this(r)),8&o)return r;if("object"===typeof r&&r){if(4&o&&r.__esModule)return r;if(16&o&&"function"===typeof r.then)return r}var i=Object.create(null);n.r(i);var u={};e=e||[null,t({}),t([]),t(t)];for(var c=2&o&&r;"object"==typeof c&&!~e.indexOf(c);c=t(c))Object.getOwnPropertyNames(c).forEach((function(e){u[e]=function(){return r[e]}}));return u.default=function(){return r},n.d(i,u),i}}(),n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.f={},n.e=function(e){return Promise.all(Object.keys(n.f).reduce((function(t,r){return n.f[r](e,t),t}),[]))},n.u=function(e){return"static/chunks/"+e+"."+{460:"91d31c8392f2cdc4",665:"ae67dcf3c1b6f7f6"}[e]+".js"},n.miniCssF=function(e){return"static/css/fc4c5db74ac4baf3.css"},n.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"===typeof window)return window}}(),n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){var e={},t="_N_E:";n.l=function(r,o,i,u){if(e[r])e[r].push(o);else{var c,f;if(void 0!==i)for(var a=document.getElementsByTagName("script"),l=0;l<a.length;l++){var d=a[l];if(d.getAttribute("src")==r||d.getAttribute("data-webpack")==t+i){c=d;break}}c||(f=!0,(c=document.createElement("script")).charset="utf-8",c.timeout=120,n.nc&&c.setAttribute("nonce",n.nc),c.setAttribute("data-webpack",t+i),c.src=r),e[r]=[o];var s=function(t,n){c.onerror=c.onload=null,clearTimeout(p);var o=e[r];if(delete e[r],c.parentNode&&c.parentNode.removeChild(c),o&&o.forEach((function(e){return e(n)})),t)return t(n)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:c}),12e4);c.onerror=s.bind(null,c.onerror),c.onload=s.bind(null,c.onload),f&&document.head.appendChild(c)}}}(),n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.nmd=function(e){return e.paths=[],e.children||(e.children=[]),e},n.p="/_next/",function(){var e={272:0};n.f.j=function(t,r){var o=n.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else if(272!=t){var i=new Promise((function(n,r){o=e[t]=[n,r]}));r.push(o[2]=i);var u=n.p+n.u(t),c=new Error;n.l(u,(function(r){if(n.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var i=r&&("load"===r.type?"missing":r.type),u=r&&r.target&&r.target.src;c.message="Loading chunk "+t+" failed.\n("+i+": "+u+")",c.name="ChunkLoadError",c.type=i,c.request=u,o[1](c)}}),"chunk-"+t,t)}else e[t]=0},n.O.j=function(t){return 0===e[t]};var t=function(t,r){var o,i,u=r[0],c=r[1],f=r[2],a=0;if(u.some((function(t){return 0!==e[t]}))){for(o in c)n.o(c,o)&&(n.m[o]=c[o]);if(f)var l=f(n)}for(t&&t(r);a<u.length;a++)i=u[a],n.o(e,i)&&e[i]&&e[i][0](),e[i]=0;return n.O(l)},r=self.webpackChunk_N_E=self.webpackChunk_N_E||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))}()}(); |
@ -1 +0,0 @@ |
||||
!function(){"use strict";var e={},t={};function n(r){var o=t[r];if(void 0!==o)return o.exports;var u=t[r]={exports:{}},i=!0;try{e[r].call(u.exports,u,u.exports,n),i=!1}finally{i&&delete t[r]}return u.exports}n.m=e,function(){var e=[];n.O=function(t,r,o,u){if(!r){var i=1/0;for(l=0;l<e.length;l++){r=e[l][0],o=e[l][1],u=e[l][2];for(var f=!0,c=0;c<r.length;c++)(!1&u||i>=u)&&Object.keys(n.O).every((function(e){return n.O[e](r[c])}))?r.splice(c--,1):(f=!1,u<i&&(i=u));if(f){e.splice(l--,1);var a=o();void 0!==a&&(t=a)}}return t}u=u||0;for(var l=e.length;l>0&&e[l-1][2]>u;l--)e[l]=e[l-1];e[l]=[r,o,u]}}(),n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,{a:t}),t},function(){var e,t=Object.getPrototypeOf?function(e){return Object.getPrototypeOf(e)}:function(e){return e.__proto__};n.t=function(r,o){if(1&o&&(r=this(r)),8&o)return r;if("object"===typeof r&&r){if(4&o&&r.__esModule)return r;if(16&o&&"function"===typeof r.then)return r}var u=Object.create(null);n.r(u);var i={};e=e||[null,t({}),t([]),t(t)];for(var f=2&o&&r;"object"==typeof f&&!~e.indexOf(f);f=t(f))Object.getOwnPropertyNames(f).forEach((function(e){i[e]=function(){return r[e]}}));return i.default=function(){return r},n.d(u,i),u}}(),n.d=function(e,t){for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.f={},n.e=function(e){return Promise.all(Object.keys(n.f).reduce((function(t,r){return n.f[r](e,t),t}),[]))},n.u=function(e){return"static/chunks/"+e+"."+{460:"91d31c8392f2cdc4",665:"ae67dcf3c1b6f7f6"}[e]+".js"},n.miniCssF=function(e){return"static/css/fc4c5db74ac4baf3.css"},n.g=function(){if("object"===typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"===typeof window)return window}}(),n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){var e={},t="_N_E:";n.l=function(r,o,u,i){if(e[r])e[r].push(o);else{var f,c;if(void 0!==u)for(var a=document.getElementsByTagName("script"),l=0;l<a.length;l++){var s=a[l];if(s.getAttribute("src")==r||s.getAttribute("data-webpack")==t+u){f=s;break}}f||(c=!0,(f=document.createElement("script")).charset="utf-8",f.timeout=120,n.nc&&f.setAttribute("nonce",n.nc),f.setAttribute("data-webpack",t+u),f.src=r),e[r]=[o];var d=function(t,n){f.onerror=f.onload=null,clearTimeout(p);var o=e[r];if(delete e[r],f.parentNode&&f.parentNode.removeChild(f),o&&o.forEach((function(e){return e(n)})),t)return t(n)},p=setTimeout(d.bind(null,void 0,{type:"timeout",target:f}),12e4);f.onerror=d.bind(null,f.onerror),f.onload=d.bind(null,f.onload),c&&document.head.appendChild(f)}}}(),n.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.p="/_next/",function(){var e={272:0};n.f.j=function(t,r){var o=n.o(e,t)?e[t]:void 0;if(0!==o)if(o)r.push(o[2]);else if(272!=t){var u=new Promise((function(n,r){o=e[t]=[n,r]}));r.push(o[2]=u);var i=n.p+n.u(t),f=new Error;n.l(i,(function(r){if(n.o(e,t)&&(0!==(o=e[t])&&(e[t]=void 0),o)){var u=r&&("load"===r.type?"missing":r.type),i=r&&r.target&&r.target.src;f.message="Loading chunk "+t+" failed.\n("+u+": "+i+")",f.name="ChunkLoadError",f.type=u,f.request=i,o[1](f)}}),"chunk-"+t,t)}else e[t]=0},n.O.j=function(t){return 0===e[t]};var t=function(t,r){var o,u,i=r[0],f=r[1],c=r[2],a=0;if(i.some((function(t){return 0!==e[t]}))){for(o in f)n.o(f,o)&&(n.m[o]=f[o]);if(c)var l=c(n)}for(t&&t(r);a<i.length;a++)u=i[a],n.o(e,u)&&e[u]&&e[u][0](),e[u]=0;return n.O(l)},r=self.webpackChunk_N_E=self.webpackChunk_N_E||[];r.forEach(t.bind(null,0)),r.push=t.bind(null,r.push.bind(r))}()}(); |
@ -0,0 +1 @@ |
||||
self.__BUILD_MANIFEST=function(s,c,a,t,e,n,i,d,f,b,u,k,h,j,r,g,l,_){return{__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":[s,a,t,d,f,h,"static/chunks/717-8bd60b96d67fd464.js",c,e,n,i,j,r,"static/chunks/pages/index-03c43a0be65dfb49.js"],"/_error":["static/chunks/pages/_error-2280fa386d040b66.js"],"/anvil":[s,a,t,d,f,h,c,e,n,i,j,"static/chunks/pages/anvil-5058ba8058633c3d.js"],"/config":[s,a,t,u,"static/chunks/586-4e70511cf6d7632f.js",c,e,n,i,b,k,g,"static/chunks/pages/config-0cb597caf390573f.js"],"/file-manager":["static/chunks/29107295-fbcfe2172188e46f.js",s,a,t,d,"static/chunks/176-7308c25ba374961e.js",c,e,i,b,"static/chunks/pages/file-manager-1ae01a78e266275a.js"],"/init":[s,a,d,f,u,l,c,e,n,i,_,"static/chunks/pages/init-053607258b5d7d64.js"],"/login":[s,a,t,c,e,n,b,k,"static/chunks/pages/login-1b987b077ffc3420.js"],"/manage-element":[s,a,t,d,f,u,l,"static/chunks/111-2605129c170ed35d.js",c,e,n,i,b,k,_,g,"static/chunks/pages/manage-element-6b42a013966413d3.js"],"/server":[s,t,"static/chunks/227-a3756585a7ef09ae.js",c,r,"static/chunks/pages/server-db52258419acacf3.js"],sortedPages:["/","/_app","/_error","/anvil","/config","/file-manager","/init","/login","/manage-element","/server"]}}("static/chunks/382-f51344f6f9208507.js","static/chunks/62-532ed713980da8db.js","static/chunks/438-0147a63d98e89439.js","static/chunks/894-e57948de523bcf96.js","static/chunks/195-fa06e61dd4339031.js","static/chunks/987-1ff0d82724b0e58b.js","static/chunks/157-d1418743accab385.js","static/chunks/182-08683bbe95fbb010.js","static/chunks/434-07ec1dcc649bdd0c.js","static/chunks/248-749f2bec4cb43d28.js","static/chunks/644-c7c6e21c71345aed.js","static/chunks/336-8a7866afcf131f68.js","static/chunks/485-77798bccc4308d0e.js","static/chunks/825-0b3ee47570192a02.js","static/chunks/94-e103c3735f0e061b.js","static/chunks/560-0ed707609765e23a.js","static/chunks/676-6159ce853338cc1f.js","static/chunks/86-447b52c8195dea3d.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB(); |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,48 @@ |
||||
type APIFileOverview = { |
||||
checksum: string; |
||||
name: string; |
||||
size: string; |
||||
type: FileType; |
||||
uuid: string; |
||||
}; |
||||
|
||||
type APIFileDetail = APIFileOverview & { |
||||
anvils: { |
||||
[uuid: string]: { |
||||
description: string; |
||||
locationUuids: string[]; |
||||
name: string; |
||||
uuid: string; |
||||
}; |
||||
}; |
||||
hosts: { |
||||
[uuid: string]: { |
||||
locationUuids: string[]; |
||||
name: string; |
||||
type: string; |
||||
uuid: string; |
||||
}; |
||||
}; |
||||
locations: { |
||||
[uuid: string]: { |
||||
active: boolean; |
||||
anvilUuid: string; |
||||
hostUuid: string; |
||||
uuid: string; |
||||
}; |
||||
}; |
||||
}; |
||||
|
||||
type APIFileOverviewList = { |
||||
[uuid: string]: APIFileOverview; |
||||
}; |
||||
|
||||
type APIEditFileRequestBody = { |
||||
fileName: string; |
||||
fileType: FileType; |
||||
fileUUID: string; |
||||
fileLocations: Array<{ |
||||
fileLocationUUID: string; |
||||
isFileLocationActive: boolean; |
||||
}>; |
||||
}; |
@ -0,0 +1,6 @@ |
||||
type ActionGroupOptionalProps = { |
||||
actions?: ContainedButtonProps[]; |
||||
loading?: boolean; |
||||
}; |
||||
|
||||
type ActionGroupProps = ActionGroupOptionalProps; |
@ -1 +1,8 @@ |
||||
type ContainedButtonProps = import('@mui/material').ButtonProps; |
||||
type ContainedButtonBackground = 'blue' | 'normal' | 'red'; |
||||
|
||||
type ContainedButtonOptionalProps = { |
||||
background?: ContainedButtonBackground; |
||||
}; |
||||
|
||||
type ContainedButtonProps = import('@mui/material').ButtonProps & |
||||
ContainedButtonOptionalProps; |
||||
|
@ -0,0 +1,51 @@ |
||||
type DialogContextContent = { |
||||
open: boolean; |
||||
setOpen: (open: boolean) => void; |
||||
}; |
||||
|
||||
type DialogOptionalProps = { |
||||
dialogProps?: Partial<import('@mui/material').DialogProps>; |
||||
loading?: boolean; |
||||
openInitially?: boolean; |
||||
wide?: boolean; |
||||
}; |
||||
|
||||
type DialogProps = DialogOptionalProps; |
||||
|
||||
type DialogForwardedRefContent = DialogContextContent; |
||||
|
||||
/** DialogActionGroup */ |
||||
|
||||
type ButtonClickEventHandler = Exclude< |
||||
ContainedButtonProps['onClick'], |
||||
undefined |
||||
>; |
||||
|
||||
type DialogActionGroupOptionalProps = { |
||||
cancelChildren?: ContainedButtonProps['children']; |
||||
cancelProps?: Partial<ContainedButtonProps>; |
||||
closeOnProceed?: boolean; |
||||
loading?: boolean; |
||||
onCancel?: ExtendableEventHandler<ButtonClickEventHandler>; |
||||
onProceed?: ExtendableEventHandler<ButtonClickEventHandler>; |
||||
proceedChildren?: ContainedButtonProps['children']; |
||||
proceedColour?: ContainedButtonProps['background']; |
||||
proceedProps?: Partial<ContainedButtonProps>; |
||||
}; |
||||
|
||||
type DialogActionGroupProps = DialogActionGroupOptionalProps; |
||||
|
||||
/** DialogHeader */ |
||||
|
||||
type DialogHeaderOptionalProps = { |
||||
showClose?: boolean; |
||||
}; |
||||
|
||||
type DialogHeaderProps = DialogHeaderOptionalProps; |
||||
|
||||
/** DialogWithHeader */ |
||||
|
||||
type DialogWithHeaderProps = DialogProps & |
||||
DialogHeaderProps & { |
||||
header: import('react').ReactNode; |
||||
}; |
@ -0,0 +1,4 @@ |
||||
type ExtendableEventHandler<T> = ( |
||||
toolbox: { handlers: { base?: T; origin?: T } }, |
||||
...rest: Parameters<T> |
||||
) => ReturnType<T>; |
@ -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; |
||||
}; |
@ -0,0 +1,5 @@ |
||||
type Message = import('../components/MessageBox').Message; |
||||
|
||||
type Messages = { |
||||
[messageKey: string]: Message; |
||||
}; |
@ -0,0 +1,3 @@ |
||||
type Tree<T = string> = { |
||||
[k: string]: Tree<T> | T; |
||||
}; |
@ -0,0 +1,38 @@ |
||||
type MuiInputBaseProps = import('@mui/material').InputBaseProps; |
||||
|
||||
type ReactChangeEventHandler = |
||||
import('react').ChangeEventHandler<HTMLInputElement>; |
||||
|
||||
type MuiInputBasePropsBlurEventHandler = Exclude< |
||||
MuiInputBaseProps['onBlur'], |
||||
undefined |
||||
>; |
||||
|
||||
type MuiInputBasePropsFocusEventHandler = Exclude< |
||||
MuiInputBaseProps['onFocus'], |
||||
undefined |
||||
>; |
||||
|
||||
type UncontrolledInputComponentMountEventHandler = () => void; |
||||
|
||||
type UncontrolledInputComponentUnmountEventHandler = () => void; |
||||
|
||||
type UncontrolledInputOptionalProps = { |
||||
onBlur?: ExtendableEventHandler<MuiInputBasePropsBlurEventHandler>; |
||||
onChange?: ExtendableEventHandler<ReactChangeEventHandler>; |
||||
onFocus?: ExtendableEventHandler<MuiInputBasePropsFocusEventHandler>; |
||||
onMount?: UncontrolledInputComponentMountEventHandler; |
||||
onUnmount?: UncontrolledInputComponentUnmountEventHandler; |
||||
}; |
||||
|
||||
type UncontrolledInputProps<InputElement extends import('react').ReactElement> = |
||||
UncontrolledInputOptionalProps & { |
||||
input: InputElement; |
||||
}; |
||||
|
||||
type UncontrolledInputForwardedRefContent< |
||||
ValueType extends keyof MapToInputType, |
||||
> = { |
||||
get: () => MapToInputType[ValueType]; |
||||
set: (value: MapToInputType[ValueType]) => void; |
||||
}; |
Loading…
Reference in new issue