Merge pull request #473 from ylei-tsubame/replace-form-validation

Web UI: replace file manager with external form management and validation libraries
main
Digimer 1 year ago committed by GitHub
commit 74d8e6d76a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      striker-ui-api/out/index.js
  2. 23
      striker-ui-api/src/lib/request_handlers/anvil/getAnvil.ts
  3. 3
      striker-ui-api/src/lib/request_handlers/file/buildQueryFileDetail.ts
  4. 2
      striker-ui-api/src/types/ApiAn.d.ts
  5. 36
      striker-ui/components/ActionGroup.tsx
  6. 312
      striker-ui/components/ConfirmDialog.tsx
  7. 74
      striker-ui/components/ContainedButton.tsx
  8. 90
      striker-ui/components/Dialog/Dialog.tsx
  9. 98
      striker-ui/components/Dialog/DialogActionGroup.tsx
  10. 41
      striker-ui/components/Dialog/DialogHeader.tsx
  11. 9
      striker-ui/components/Dialog/DialogScrollBox.tsx
  12. 43
      striker-ui/components/Dialog/DialogWithHeader.tsx
  13. 13
      striker-ui/components/Dialog/index.tsx
  14. 233
      striker-ui/components/Files/AddFileForm.tsx
  15. 175
      striker-ui/components/Files/EditFileForm.tsx
  16. 219
      striker-ui/components/Files/FileInputGroup.tsx
  17. 388
      striker-ui/components/Files/ManageFilePanel.tsx
  18. 42
      striker-ui/components/Files/UploadFileProgress.tsx
  19. 29
      striker-ui/components/Files/schema.ts
  20. 119
      striker-ui/components/FormDialog.tsx
  21. 21
      striker-ui/components/MessageGroup.tsx
  22. 8
      striker-ui/components/ScrollBox.tsx
  23. 106
      striker-ui/components/UncontrolledInput.tsx
  24. 29
      striker-ui/hooks/useFetch.tsx
  25. 14
      striker-ui/lib/buildYupDynamicObject.ts
  26. 1
      striker-ui/lib/consts/INPUT_TYPES.ts
  27. 26
      striker-ui/lib/convertFormikErrorsToMessages.ts
  28. 14
      striker-ui/lib/sxstring.ts
  29. 1
      striker-ui/out/_next/static/4YSdIu7KT3DenHgC9VrZp/_buildManifest.js
  30. 2
      striker-ui/out/_next/static/chunks/111-2605129c170ed35d.js
  31. 1
      striker-ui/out/_next/static/chunks/140-ec935fb15330b98a.js
  32. 1
      striker-ui/out/_next/static/chunks/157-d1418743accab385.js
  33. 1
      striker-ui/out/_next/static/chunks/176-7308c25ba374961e.js
  34. 1
      striker-ui/out/_next/static/chunks/195-fa06e61dd4339031.js
  35. 1
      striker-ui/out/_next/static/chunks/248-749f2bec4cb43d28.js
  36. 1
      striker-ui/out/_next/static/chunks/29107295-fbcfe2172188e46f.js
  37. 1
      striker-ui/out/_next/static/chunks/336-8a7866afcf131f68.js
  38. 1
      striker-ui/out/_next/static/chunks/434-07ec1dcc649bdd0c.js
  39. 1
      striker-ui/out/_next/static/chunks/438-0147a63d98e89439.js
  40. 1
      striker-ui/out/_next/static/chunks/483-f8013e38dca1620d.js
  41. 1
      striker-ui/out/_next/static/chunks/560-0ed707609765e23a.js
  42. 1
      striker-ui/out/_next/static/chunks/586-4e70511cf6d7632f.js
  43. 1
      striker-ui/out/_next/static/chunks/614-0ce04fd295045ffe.js
  44. 1
      striker-ui/out/_next/static/chunks/768-9ee3dcb62beecb53.js
  45. 1
      striker-ui/out/_next/static/chunks/780-e8b3396d257460a4.js
  46. 1
      striker-ui/out/_next/static/chunks/825-0b3ee47570192a02.js
  47. 1
      striker-ui/out/_next/static/chunks/825-1bb2d128cccc0e41.js
  48. 1
      striker-ui/out/_next/static/chunks/86-447b52c8195dea3d.js
  49. 1
      striker-ui/out/_next/static/chunks/86-a6f7430ac8a027ff.js
  50. 1
      striker-ui/out/_next/static/chunks/899-ec535b0f0a173e21.js
  51. 1
      striker-ui/out/_next/static/chunks/903-dc2a40be612a10c3.js
  52. 1
      striker-ui/out/_next/static/chunks/987-1ff0d82724b0e58b.js
  53. 2
      striker-ui/out/_next/static/chunks/pages/anvil-5058ba8058633c3d.js
  54. 1
      striker-ui/out/_next/static/chunks/pages/config-0cb597caf390573f.js
  55. 1
      striker-ui/out/_next/static/chunks/pages/config-cb5dcd774a7f13bc.js
  56. 1
      striker-ui/out/_next/static/chunks/pages/file-manager-1ae01a78e266275a.js
  57. 1
      striker-ui/out/_next/static/chunks/pages/file-manager-843b3cb0cc1119f6.js
  58. 2
      striker-ui/out/_next/static/chunks/pages/index-03c43a0be65dfb49.js
  59. 1
      striker-ui/out/_next/static/chunks/pages/init-053607258b5d7d64.js
  60. 1
      striker-ui/out/_next/static/chunks/pages/init-124696b2707615f8.js
  61. 1
      striker-ui/out/_next/static/chunks/pages/login-1b987b077ffc3420.js
  62. 1
      striker-ui/out/_next/static/chunks/pages/login-b5de0cd2f49998d6.js
  63. 1
      striker-ui/out/_next/static/chunks/pages/manage-element-6b42a013966413d3.js
  64. 1
      striker-ui/out/_next/static/chunks/pages/manage-element-c5172fe1e4c11fba.js
  65. 1
      striker-ui/out/_next/static/chunks/webpack-72941b1e9d6516e5.js
  66. 1
      striker-ui/out/_next/static/chunks/webpack-b267a65404defb57.js
  67. 1
      striker-ui/out/_next/static/ojQxwcdZmWLlLwOd-3drO/_buildManifest.js
  68. 0
      striker-ui/out/_next/static/ojQxwcdZmWLlLwOd-3drO/_middlewareManifest.js
  69. 0
      striker-ui/out/_next/static/ojQxwcdZmWLlLwOd-3drO/_ssgManifest.js
  70. 2
      striker-ui/out/anvil.html
  71. 2
      striker-ui/out/config.html
  72. 2
      striker-ui/out/file-manager.html
  73. 2
      striker-ui/out/index.html
  74. 2
      striker-ui/out/init.html
  75. 2
      striker-ui/out/login.html
  76. 2
      striker-ui/out/manage-element.html
  77. 2
      striker-ui/out/server.html
  78. 170
      striker-ui/package-lock.json
  79. 6
      striker-ui/package.json
  80. 4
      striker-ui/pages/file-manager/index.tsx
  81. 28
      striker-ui/types/APIAnvil.d.ts
  82. 48
      striker-ui/types/APIFile.d.ts
  83. 6
      striker-ui/types/ActionGroup.d.ts
  84. 17
      striker-ui/types/ConfirmDialog.d.ts
  85. 9
      striker-ui/types/ContainedButton.d.ts
  86. 51
      striker-ui/types/Dialog.d.ts
  87. 4
      striker-ui/types/ExtendableEventHandler.d.ts
  88. 62
      striker-ui/types/ManageFile.d.ts
  89. 5
      striker-ui/types/Message.d.ts
  90. 3
      striker-ui/types/Tree.d.ts
  91. 38
      striker-ui/types/UncontrolledInput.d.ts

File diff suppressed because one or more lines are too long

@ -12,8 +12,10 @@ export const getAnvil: RequestHandler = buildGetRequestHandler(
SELECT
anv.anvil_name,
anv.anvil_uuid,
anv.anvil_description,
hos.host_name,
hos.host_uuid
hos.host_uuid,
hos.host_type
FROM anvils AS anv
JOIN hosts AS hos
ON hos.host_uuid IN (
@ -31,10 +33,21 @@ export const getAnvil: RequestHandler = buildGetRequestHandler(
let rowStage: AnvilOverview | undefined;
results = queryStdout.reduce<AnvilOverview[]>(
(reducedRows, [anvilName, anvilUUID, hostName, hostUUID]) => {
(
reducedRows,
[
anvilName,
anvilUUID,
anvilDescription,
hostName,
hostUUID,
hostType,
],
) => {
if (!rowStage || anvilUUID !== rowStage.anvilUUID) {
{
rowStage = {
anvilDescription,
anvilName,
anvilUUID,
hosts: [],
@ -44,7 +57,11 @@ export const getAnvil: RequestHandler = buildGetRequestHandler(
}
}
rowStage.hosts.push({ hostName, hostUUID });
rowStage.hosts.push({
hostName,
hostType,
hostUUID,
});
return reducedRows;
},

@ -32,7 +32,8 @@ export const buildQueryFileDetail = ({
anv.anvil_name,
anv.anvil_description,
hos.host_uuid,
hos.host_name
hos.host_name,
hos.host_type
FROM files AS fil
JOIN file_locations AS fil_loc
ON fil.file_uuid = fil_loc.file_location_file_uuid

@ -112,10 +112,12 @@ type AnvilDetailStoreSummary = {
};
type AnvilOverview = {
anvilDescription: string;
anvilName: string;
anvilUUID: string;
hosts: Array<{
hostName: string;
hostType: string;
hostUUID: string;
}>;
};

@ -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,283 +1,113 @@
import { Box, Dialog as MUIDialog, SxProps, Theme } from '@mui/material';
import { Box as MuiBox } from '@mui/material';
import {
ButtonHTMLAttributes,
ElementType,
FormEventHandler,
ForwardRefExoticComponent,
PropsWithChildren,
RefAttributes,
createElement,
forwardRef,
MouseEventHandler,
useImperativeHandle,
useMemo,
useState,
useRef,
} from 'react';
import { BLUE, RED, TEXT } from '../lib/consts/DEFAULT_THEME';
import ContainedButton from './ContainedButton';
import { DialogActionArea, DialogScrollBox, DialogWithHeader } from './Dialog';
import FlexBox from './FlexBox';
import { Panel, PanelHeader } from './Panels';
import Spinner from './Spinner';
import { BodyText, HeaderText } from './Text';
const MAP_TO_COLOUR: Record<
Exclude<ConfirmDialogProps['proceedColour'], undefined>,
string
> = {
blue: BLUE,
red: RED,
};
import sxstring from '../lib/sxstring';
import { BodyText } from './Text';
const ConfirmDialog = forwardRef<
ConfirmDialogForwardedRefContent,
ConfirmDialogProps
>(
const ConfirmDialog: ForwardRefExoticComponent<
PropsWithChildren<ConfirmDialogProps> &
RefAttributes<ConfirmDialogForwardedRefContent>
> = forwardRef<ConfirmDialogForwardedRefContent, ConfirmDialogProps>(
(
{
actionCancelText = 'Cancel',
actionProceedText,
contentContainerProps = {},
closeOnProceed: isCloseOnProceed = false,
content,
dialogProps: {
open: baseOpen = false,
PaperProps: paperProps = {},
...restDialogProps
} = {},
disableProceed: isDisableProceed,
formContent: isFormContent,
loading: isLoading = false,
loadingAction: isLoadingAction = false,
children,
closeOnProceed = false,
contentContainerProps,
dialogProps,
disableProceed,
loading,
loadingAction = false,
onActionAppend,
onCancelAppend,
onProceedAppend,
onSubmitAppend,
openInitially = false,
openInitially,
preActionArea,
proceedButtonProps = {},
proceedColour: proceedColourKey = 'blue',
scrollContent: isScrollContent = false,
scrollBoxProps: { sx: scrollBoxSx, ...restScrollBoxProps } = {},
proceedButtonProps,
proceedColour = 'blue',
scrollContent = false,
scrollBoxProps,
showClose,
titleText,
wide,
// Dependents
content = children,
},
ref,
) => {
const { sx: paperSx, ...restPaperProps } = paperProps;
const {
disabled: proceedButtonDisabled = isDisableProceed,
sx: proceedButtonSx,
...restProceedButtonProps
} = proceedButtonProps;
const [isOpen, setIsOpen] = useState<boolean>(openInitially);
// TODO: using base open is depreciated; use internal state once all
// dependent components finish the migrate.
const open = useMemo(
() => (ref ? isOpen : baseOpen),
[baseOpen, isOpen, ref],
);
const proceedColour = useMemo(
() => MAP_TO_COLOUR[proceedColourKey],
[proceedColourKey],
);
const {
contentContainerComponent,
contentContainerSubmitEventHandler,
proceedButtonClickEventHandler,
proceedButtonType,
} = useMemo(() => {
let ccComponent: ElementType | undefined;
let ccSubmitEventHandler: FormEventHandler<HTMLDivElement> | undefined;
let pbClickEventHandler:
| MouseEventHandler<HTMLButtonElement>
| undefined = (...args) => {
if (isCloseOnProceed) {
setIsOpen(false);
}
onActionAppend?.call(null, ...args);
onProceedAppend?.call(null, ...args);
};
let pbType: ButtonHTMLAttributes<HTMLButtonElement>['type'] | undefined;
if (isFormContent) {
ccComponent = 'form';
ccSubmitEventHandler = (event, ...restArgs) => {
event.preventDefault();
if (isCloseOnProceed) {
setIsOpen(false);
}
onSubmitAppend?.call(null, event, ...restArgs);
};
pbClickEventHandler = undefined;
pbType = 'submit';
}
return {
contentContainerComponent: ccComponent,
contentContainerSubmitEventHandler: ccSubmitEventHandler,
proceedButtonClickEventHandler: pbClickEventHandler,
proceedButtonType: pbType,
};
}, [
isCloseOnProceed,
isFormContent,
onActionAppend,
onProceedAppend,
onSubmitAppend,
]);
const cancelButtonElement = useMemo(
() => (
<ContainedButton
onClick={(...args) => {
setIsOpen(false);
onActionAppend?.call(null, ...args);
onCancelAppend?.call(null, ...args);
}}
>
{actionCancelText}
</ContainedButton>
),
[actionCancelText, onActionAppend, onCancelAppend],
);
const proceedButtonElement = useMemo(
() => (
<ContainedButton
disabled={proceedButtonDisabled}
onClick={proceedButtonClickEventHandler}
type={proceedButtonType}
{...restProceedButtonProps}
sx={{
backgroundColor: proceedColour,
color: TEXT,
'&:hover': { backgroundColor: `${proceedColour}F0` },
...proceedButtonSx,
}}
>
{actionProceedText}
</ContainedButton>
),
[
actionProceedText,
proceedButtonClickEventHandler,
proceedButtonDisabled,
proceedButtonSx,
proceedButtonType,
proceedColour,
restProceedButtonProps,
],
);
const dialogRef = useRef<DialogForwardedRefContent>(null);
const actionAreaElement = useMemo(
() =>
isLoadingAction ? (
<Spinner mt={0} />
) : (
<FlexBox
row
spacing=".5em"
sx={{ justifyContent: 'flex-end', width: '100%' }}
>
{cancelButtonElement}
{proceedButtonElement}
</FlexBox>
),
[cancelButtonElement, isLoadingAction, proceedButtonElement],
);
const contentElement = useMemo(
() =>
typeof content === 'string' ? <BodyText text={content} /> : content,
() => sxstring(content, BodyText),
[content],
);
const headerElement = useMemo(
() =>
typeof titleText === 'string' ? (
<HeaderText>{titleText}</HeaderText>
) : (
titleText
),
[titleText],
);
const combinedScrollBoxSx = useMemo(() => {
let result: SxProps<Theme> | undefined;
if (isScrollContent) {
let overflowX: 'hidden' | undefined;
let paddingTop: string | undefined;
if (isFormContent) {
overflowX = 'hidden';
paddingTop = '.6em';
}
result = {
maxHeight: '60vh',
overflowX,
overflowY: 'scroll',
paddingRight: '.4em',
paddingTop,
...scrollBoxSx,
};
}
return result;
}, [isFormContent, isScrollContent, scrollBoxSx]);
const contentAreaElement = useMemo(
const bodyElement = useMemo(
() =>
isLoading ? (
<Spinner />
) : (
<>
<Box {...restScrollBoxProps} sx={combinedScrollBoxSx}>
{contentElement}
</Box>
{preActionArea}
{actionAreaElement}
</>
createElement(
scrollContent ? DialogScrollBox : MuiBox,
scrollBoxProps,
contentElement,
),
[
actionAreaElement,
combinedScrollBoxSx,
contentElement,
isLoading,
preActionArea,
restScrollBoxProps,
],
[contentElement, scrollBoxProps, scrollContent],
);
useImperativeHandle(
ref,
() => ({
setOpen: (value) => setIsOpen(value),
setOpen: (open) => dialogRef.current?.setOpen(open),
}),
[],
);
return (
<MUIDialog
open={open}
PaperComponent={Panel}
PaperProps={{
...restPaperProps,
sx: { overflow: 'visible', ...paperSx },
}}
{...restDialogProps}
<DialogWithHeader
dialogProps={dialogProps}
header={titleText}
loading={loading}
openInitially={openInitially}
ref={dialogRef}
showClose={showClose}
wide={wide}
>
<PanelHeader>{headerElement}</PanelHeader>
<FlexBox
component={contentContainerComponent}
onSubmit={contentContainerSubmitEventHandler}
{...contentContainerProps}
>
{contentAreaElement}
<FlexBox {...contentContainerProps}>
{bodyElement}
{preActionArea}
<DialogActionArea
cancelProps={{
children: actionCancelText,
onClick: (...args) => {
onActionAppend?.call(null, ...args);
onCancelAppend?.call(null, ...args);
},
}}
closeOnProceed={closeOnProceed}
loading={loadingAction}
proceedProps={{
background: proceedColour,
children: actionProceedText,
disabled: disableProceed,
onClick: (...args) => {
onActionAppend?.call(null, ...args);
onProceedAppend?.call(null, ...args);
},
...proceedButtonProps,
}}
/>
</FlexBox>
</MUIDialog>
</DialogWithHeader>
);
},
);

@ -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;

@ -1,67 +1,76 @@
import { forwardRef, useMemo } from 'react';
import { BoxProps as MuiBoxProps } from '@mui/material';
import {
ForwardRefExoticComponent,
PropsWithChildren,
RefAttributes,
forwardRef,
useMemo,
} from 'react';
import ConfirmDialog from './ConfirmDialog';
import IconButton from './IconButton';
import { HeaderText } from './Text';
import { FlexBoxProps } from './FlexBox';
const FormDialog = forwardRef<
ConfirmDialogForwardedRefContent,
ConfirmDialogProps & { showClose?: boolean }
>((props, ref) => {
const { scrollContent, showClose, titleText, ...restProps } = props;
const FormDialog: ForwardRefExoticComponent<
PropsWithChildren<ConfirmDialogProps> &
RefAttributes<ConfirmDialogForwardedRefContent>
> = forwardRef<ConfirmDialogForwardedRefContent, ConfirmDialogProps>(
(props, ref) => {
const {
children,
contentContainerProps,
dialogProps,
onSubmitAppend,
proceedButtonProps,
scrollBoxProps,
scrollContent,
...restProps
} = props;
const scrollBoxPaddingRight = useMemo(
() => (scrollContent ? '.5em' : undefined),
[scrollContent],
);
const formBodyProps = useMemo<FlexBoxProps>(
() => ({
...contentContainerProps,
component: 'form',
onSubmit: (...args) => {
const [event] = args;
const titleElement = useMemo(() => {
const title =
typeof titleText === 'string' ? (
<HeaderText>{titleText}</HeaderText>
) : (
titleText
);
event.preventDefault();
return showClose ? (
<>
{title}
<IconButton
mapPreset="close"
onClick={() => {
if (ref && 'current' in ref) {
ref.current?.setOpen?.call(null, false);
}
}}
variant="redcontained"
/>
</>
) : (
title
onSubmitAppend?.call(null, ...args);
},
}),
[contentContainerProps, onSubmitAppend],
);
}, [ref, showClose, titleText]);
return (
<ConfirmDialog
dialogProps={{
PaperProps: { sx: { minWidth: { xs: '90%', md: '50em' } } },
}}
formContent
scrollBoxProps={{
paddingRight: scrollBoxPaddingRight,
paddingTop: '.3em',
}}
scrollContent={scrollContent}
titleText={titleElement}
{...restProps}
ref={ref}
/>
);
});
const formScrollBoxProps = useMemo<MuiBoxProps>(
() => ({
...scrollBoxProps,
sx: scrollContent
? {
overflowX: 'hidden',
paddingTop: '.6em',
...scrollBoxProps?.sx,
}
: scrollBoxProps?.sx,
}),
[scrollBoxProps, scrollContent],
);
FormDialog.defaultProps = {
showClose: false,
};
return (
<ConfirmDialog
dialogProps={dialogProps}
contentContainerProps={formBodyProps}
proceedButtonProps={{ ...proceedButtonProps, type: 'submit' }}
scrollContent={scrollContent}
scrollBoxProps={formScrollBoxProps}
wide
{...restProps}
ref={ref}
>
{children}
</ConfirmDialog>
);
},
);
FormDialog.displayName = 'FormDialog';

@ -16,6 +16,7 @@ type Messages = {
type MessageGroupOptionalProps = {
count?: number;
defaultMessageType?: MessageBoxProps['type'];
messages?: Messages;
onSet?: (length: number) => void;
usePlaceholder?: boolean;
};
@ -29,11 +30,12 @@ type MessageGroupForwardedRefContent = {
};
const MESSAGE_GROUP_DEFAULT_PROPS: Required<
Omit<MessageGroupOptionalProps, 'onSet'>
Omit<MessageGroupOptionalProps, 'messages' | 'onSet'>
> &
Pick<MessageGroupOptionalProps, 'onSet'> = {
Pick<MessageGroupOptionalProps, 'messages' | 'onSet'> = {
count: 0,
defaultMessageType: 'info',
messages: undefined,
onSet: undefined,
usePlaceholder: true,
};
@ -46,13 +48,22 @@ const MessageGroup = forwardRef<
{
count = MESSAGE_GROUP_DEFAULT_PROPS.count,
defaultMessageType = MESSAGE_GROUP_DEFAULT_PROPS.defaultMessageType,
messages: externalMessages,
onSet,
usePlaceholder:
isUsePlaceholder = MESSAGE_GROUP_DEFAULT_PROPS.usePlaceholder,
},
ref,
) => {
const [messages, setMessages] = useState<Messages>({});
const [internalMessages, setInternalMessages] = useState<Messages>({});
const messages = useMemo<Messages>(
() => ({
...externalMessages,
...internalMessages,
}),
[externalMessages, internalMessages],
);
const exists = useCallback(
(key: string) => messages[key] !== undefined,
@ -62,7 +73,7 @@ const MessageGroup = forwardRef<
(key: string, message?: Message) => {
let length = 0;
setMessages((previous) => {
setInternalMessages((previous) => {
const { [key]: unused, ...rest } = previous;
const result: Messages = rest;
@ -90,7 +101,7 @@ const MessageGroup = forwardRef<
}
: undefined;
setMessages((previous) => {
setInternalMessages((previous) => {
const result: Messages = {};
Object.keys(previous).forEach((key: string) => {

@ -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;

@ -1,4 +1,5 @@
const INPUT_TYPES = {
checkbox: 'checkbox',
number: 'number',
password: 'password',
text: 'text',

@ -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

@ -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

@ -22,17 +22,21 @@
"@novnc/novnc": "^1.2.0",
"axios": "^0.24.0",
"format-data-size": "^0.1.0",
"formik": "^2.4.3",
"lodash": "^4.17.21",
"netmask": "^2.0.2",
"next": "^12.1.0",
"pretty-bytes": "^5.6.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"swr": "^1.2.2",
"uuid": "^8.3.2"
"uuid": "^8.3.2",
"yup": "^1.2.0"
},
"devDependencies": {
"@commitlint/cli": "^17.7.0",
"@commitlint/config-conventional": "^12.1.4",
"@types/lodash": "^4.14.198",
"@types/netmask": "^1.0.30",
"@types/node": "^15.12.2",
"@types/novnc-core": "^0.1.3",
@ -1550,6 +1554,12 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.14.198",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.198.tgz",
"integrity": "sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg==",
"dev": true
},
"node_modules/@types/minimist": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
@ -2504,6 +2514,14 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"node_modules/deepmerge": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@ -3410,6 +3428,29 @@
"format-data-size": "dist/cli.js"
}
},
"node_modules/formik": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/formik/-/formik-2.4.3.tgz",
"integrity": "sha512-2Dy79Szw3zlXmZiokUdKsn+n1ow4G8hRrC/n92cOWHNTWXCRpQXlyvz6HcjW7aSQZrldytvDOavYjhfmDnUq8Q==",
"funding": [
{
"type": "individual",
"url": "https://opencollective.com/formik"
}
],
"dependencies": {
"deepmerge": "^2.1.1",
"hoist-non-react-statics": "^3.3.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"react-fast-compare": "^2.0.1",
"tiny-warning": "^1.0.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/fs-extra": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz",
@ -4469,8 +4510,12 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"node_modules/lodash.camelcase": {
"version": "4.3.0",
@ -5238,6 +5283,11 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/property-expr": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz",
"integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA=="
},
"node_modules/punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@ -5311,6 +5361,11 @@
"react": "17.0.2"
}
},
"node_modules/react-fast-compare": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
},
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@ -6141,6 +6196,11 @@
"readable-stream": "3"
}
},
"node_modules/tiny-case": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
@ -6166,6 +6226,11 @@
"node": ">=8.0"
}
},
"node_modules/toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
},
"node_modules/trim-newlines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz",
@ -6190,8 +6255,7 @@
"node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/tsutils": {
"version": "3.21.0",
@ -6473,6 +6537,28 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yup": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/yup/-/yup-1.2.0.tgz",
"integrity": "sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==",
"dependencies": {
"property-expr": "^2.0.5",
"tiny-case": "^1.0.3",
"toposort": "^2.0.2",
"type-fest": "^2.19.0"
}
},
"node_modules/yup/node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}
},
"dependencies": {
@ -7520,6 +7606,12 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true
},
"@types/lodash": {
"version": "4.14.198",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.198.tgz",
"integrity": "sha512-trNJ/vtMZYMLhfN45uLq4ShQSw0/S7xCTLLVM+WM1rmFpba/VS42jVUgaO3w/NOLiWR/09lnYk0yMaA/atdIsg==",
"dev": true
},
"@types/minimist": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz",
@ -8220,6 +8312,11 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
"deepmerge": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA=="
},
"define-properties": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
@ -8922,6 +9019,20 @@
"resolved": "https://registry.npmjs.org/format-data-size/-/format-data-size-0.1.0.tgz",
"integrity": "sha512-iataqDS6c73/MpJal7+GCtXiTbZEOn8HwfHesLvHOzu9MQwQ6LNOLbK/oli9qZmrap7TPGlaJnYMCxNR9Fh4iA=="
},
"formik": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/formik/-/formik-2.4.3.tgz",
"integrity": "sha512-2Dy79Szw3zlXmZiokUdKsn+n1ow4G8hRrC/n92cOWHNTWXCRpQXlyvz6HcjW7aSQZrldytvDOavYjhfmDnUq8Q==",
"requires": {
"deepmerge": "^2.1.1",
"hoist-non-react-statics": "^3.3.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"react-fast-compare": "^2.0.1",
"tiny-warning": "^1.0.2",
"tslib": "^2.0.0"
}
},
"fs-extra": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz",
@ -9721,8 +9832,12 @@
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
},
"lodash.camelcase": {
"version": "4.3.0",
@ -10280,6 +10395,11 @@
}
}
},
"property-expr": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz",
"integrity": "sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA=="
},
"punycode": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
@ -10323,6 +10443,11 @@
"scheduler": "^0.20.2"
}
},
"react-fast-compare": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
},
"react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@ -10940,6 +11065,11 @@
"readable-stream": "3"
}
},
"tiny-case": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
},
"tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
@ -10959,6 +11089,11 @@
"is-number": "^7.0.0"
}
},
"toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
},
"trim-newlines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz",
@ -10980,8 +11115,7 @@
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
"dev": true
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"tsutils": {
"version": "3.21.0",
@ -11193,6 +11327,24 @@
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true
},
"yup": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/yup/-/yup-1.2.0.tgz",
"integrity": "sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==",
"requires": {
"property-expr": "^2.0.5",
"tiny-case": "^1.0.3",
"toposort": "^2.0.2",
"type-fest": "^2.19.0"
},
"dependencies": {
"type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="
}
}
}
}
}

@ -27,17 +27,21 @@
"@novnc/novnc": "^1.2.0",
"axios": "^0.24.0",
"format-data-size": "^0.1.0",
"formik": "^2.4.3",
"lodash": "^4.17.21",
"netmask": "^2.0.2",
"next": "^12.1.0",
"pretty-bytes": "^5.6.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"swr": "^1.2.2",
"uuid": "^8.3.2"
"uuid": "^8.3.2",
"yup": "^1.2.0"
},
"devDependencies": {
"@commitlint/cli": "^17.7.0",
"@commitlint/config-conventional": "^12.1.4",
"@types/lodash": "^4.14.198",
"@types/netmask": "^1.0.30",
"@types/node": "^15.12.2",
"@types/novnc-core": "^0.1.3",

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

@ -86,3 +86,31 @@ type AnvilListItem = {
type AnvilList = {
anvils: AnvilListItem[];
};
type APIAnvilOverviewArray = Array<{
anvilDescription: string;
anvilName: string;
anvilUUID: string;
hosts: Array<{
hostName: string;
hostType: string;
hostUUID: string;
}>;
}>;
type APIAnvilOverview = {
description: string;
hosts: {
[uuid: string]: {
name: string;
type: string;
uuid: string;
};
};
name: string;
uuid: string;
};
type APIAnvilOverviewList = {
[uuid: string]: APIAnvilOverview;
};

@ -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;

@ -4,17 +4,14 @@ type DivFormEventHandlerParameters = Parameters<DivFormEventHandler>;
type ConfirmDialogOptionalProps = {
actionCancelText?: string;
closeOnProceed?: boolean;
content?: import('react').ReactNode;
contentContainerProps?: import('../components/FlexBox').FlexBoxProps;
dialogProps?: Partial<import('@mui/material').DialogProps>;
disableProceed?: boolean;
formContent?: boolean;
loading?: boolean;
loadingAction?: boolean;
onActionAppend?: ContainedButtonProps['onClick'];
onProceedAppend?: ContainedButtonProps['onClick'];
onCancelAppend?: ContainedButtonProps['onClick'];
onProceedAppend?: ContainedButtonProps['onClick'];
onSubmitAppend?: DivFormEventHandler;
openInitially?: boolean;
preActionArea?: import('react').ReactNode;
proceedButtonProps?: ContainedButtonProps;
proceedColour?: 'blue' | 'red';
@ -22,11 +19,11 @@ type ConfirmDialogOptionalProps = {
scrollBoxProps?: import('@mui/material').BoxProps;
};
type ConfirmDialogProps = ConfirmDialogOptionalProps & {
actionProceedText: string;
content: import('react').ReactNode;
titleText: import('react').ReactNode;
};
type ConfirmDialogProps = Omit<DialogWithHeaderProps, 'header'> &
ConfirmDialogOptionalProps & {
actionProceedText: string;
titleText: import('react').ReactNode;
};
type ConfirmDialogForwardedRefContent = {
setOpen?: (value: boolean) => void;

@ -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…
Cancel
Save