Merge pull request #573 from ylei-tsubame/issues/419-add-mail-config

Web UI: add mail config on UI-side
main
Digimer 11 months ago committed by GitHub
commit 3d38e10a57
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      striker-ui-api/out/index.js
  2. 2
      striker-ui-api/src/lib/execManageAlerts.ts
  3. 94
      striker-ui/components/Autocomplete.tsx
  4. 214
      striker-ui/components/CrudList.tsx
  5. 4
      striker-ui/components/Files/AddFileForm.tsx
  6. 4
      striker-ui/components/Files/EditFileForm.tsx
  7. 8
      striker-ui/components/FormSummary.tsx
  8. 15
      striker-ui/components/GateForm.tsx
  9. 19
      striker-ui/components/List.tsx
  10. 384
      striker-ui/components/ManageMailRecipient/AddMailRecipientForm.tsx
  11. 139
      striker-ui/components/ManageMailRecipient/AlertOverrideInputGroup.tsx
  12. 9
      striker-ui/components/ManageMailRecipient/EditMailRecipientForm.tsx
  13. 60
      striker-ui/components/ManageMailRecipient/ManageAlertOverride.tsx
  14. 216
      striker-ui/components/ManageMailRecipient/ManageMailRecipient.tsx
  15. 4
      striker-ui/components/ManageMailRecipient/index.ts
  16. 34
      striker-ui/components/ManageMailRecipient/schema.ts
  17. 277
      striker-ui/components/ManageMailServer/AddMailServerForm.tsx
  18. 9
      striker-ui/components/ManageMailServer/EditMailServerForm.tsx
  19. 64
      striker-ui/components/ManageMailServer/ManageMailServer.tsx
  20. 4
      striker-ui/components/ManageMailServer/index.ts
  21. 27
      striker-ui/components/ManageMailServer/schema.ts
  22. 10
      striker-ui/components/Select.tsx
  23. 19
      striker-ui/components/SelectWithLabel.tsx
  24. 29
      striker-ui/hooks/useActiveFetch.ts
  25. 2
      striker-ui/hooks/useChecklist.tsx
  26. 28
      striker-ui/hooks/useConfirmDialog.tsx
  27. 22
      striker-ui/hooks/useFetch.tsx
  28. 86
      striker-ui/hooks/useFormikUtils.ts
  29. 10
      striker-ui/lib/consts/ICONS.ts
  30. 22
      striker-ui/lib/consts/REG_EXP_PATTERNS.ts
  31. 26
      striker-ui/lib/convertFormikErrorsToMessages.ts
  32. 18
      striker-ui/lib/debounce.ts
  33. 40
      striker-ui/lib/getFormikErrorMessages.ts
  34. 9
      striker-ui/next.config.js
  35. 1
      striker-ui/out/_next/static/BQQcSBBTtNN9dIUZ9m3Ow/_buildManifest.js
  36. 0
      striker-ui/out/_next/static/BQQcSBBTtNN9dIUZ9m3Ow/_ssgManifest.js
  37. 1
      striker-ui/out/_next/static/Kz-iFpfWQR4uU-BQo8Tpi/_buildManifest.js
  38. 1
      striker-ui/out/_next/static/Kz-iFpfWQR4uU-BQo8Tpi/_middlewareManifest.js
  39. 1
      striker-ui/out/_next/static/chunks/157-0528651bf3cd10a7.js
  40. 1
      striker-ui/out/_next/static/chunks/170-357f4683929223df.js
  41. 1
      striker-ui/out/_next/static/chunks/176-7308c25ba374961e.js
  42. 1
      striker-ui/out/_next/static/chunks/195-d5fd184cc249f755.js
  43. 5
      striker-ui/out/_next/static/chunks/197-c291e38a27168218.js
  44. 1
      striker-ui/out/_next/static/chunks/203-ea1ab9b7c3c7694b.js
  45. 1
      striker-ui/out/_next/static/chunks/209-4e2794319babfeec.js
  46. 1
      striker-ui/out/_next/static/chunks/213-67c4f0768a44e039.js
  47. 1
      striker-ui/out/_next/static/chunks/213-a0488f84cc98f172.js
  48. 1
      striker-ui/out/_next/static/chunks/253.b8103226d6896fd0.js
  49. 1
      striker-ui/out/_next/static/chunks/263-5784adae0d1d8513.js
  50. 4
      striker-ui/out/_next/static/chunks/264-1be1a496ee1255c6.js
  51. 1
      striker-ui/out/_next/static/chunks/27-7790e406eb2ea28d.js
  52. 1
      striker-ui/out/_next/static/chunks/270-56592f453c639f63.js
  53. 1
      striker-ui/out/_next/static/chunks/284-03dc30df5d459e72.js
  54. 1
      striker-ui/out/_next/static/chunks/29107295-fbcfe2172188e46f.js
  55. 1
      striker-ui/out/_next/static/chunks/302-6490e226661e8e00.js
  56. 1
      striker-ui/out/_next/static/chunks/310-4edb13985847ab25.js
  57. 1
      striker-ui/out/_next/static/chunks/336-fc22c38ce3bd59c5.js
  58. 1
      striker-ui/out/_next/static/chunks/380-0eff6addb79bd61f.js
  59. 1
      striker-ui/out/_next/static/chunks/404-b8e9ff2043a0d30c.js
  60. 1
      striker-ui/out/_next/static/chunks/416-b31c470a96d10e58.js
  61. 1
      striker-ui/out/_next/static/chunks/460.4e3effd04e457fb0.js
  62. 1
      striker-ui/out/_next/static/chunks/460.91d31c8392f2cdc4.js
  63. 1
      striker-ui/out/_next/static/chunks/461-c4e18a515455805e.js
  64. 1
      striker-ui/out/_next/static/chunks/48-d4400834d0a31c6e.js
  65. 1
      striker-ui/out/_next/static/chunks/486-1480d7483e28c6f3.js
  66. 174
      striker-ui/out/_next/static/chunks/494-413067ecdf8ec8f0.js
  67. 154
      striker-ui/out/_next/static/chunks/498-e1933a5461cd8607.js
  68. 1
      striker-ui/out/_next/static/chunks/50-af452066db73e3df.js
  69. 1
      striker-ui/out/_next/static/chunks/519-4b7761e884c88eb9.js
  70. 1
      striker-ui/out/_next/static/chunks/556-dbf62d8622405edc.js
  71. 1
      striker-ui/out/_next/static/chunks/560-a9c9ecda0eca25a9.js
  72. 1
      striker-ui/out/_next/static/chunks/569-fa9b9ac8a7639d2d.js
  73. 1
      striker-ui/out/_next/static/chunks/570-6bad4610969fc14b.js
  74. 20
      striker-ui/out/_next/static/chunks/6-dbef3ba2a090cb05.js
  75. 1
      striker-ui/out/_next/static/chunks/633-900b9341a6a3bc53.js
  76. 1
      striker-ui/out/_next/static/chunks/638-13a283c3a7da370b.js
  77. 1
      striker-ui/out/_next/static/chunks/644-4eec2b397fdacb0c.js
  78. 1
      striker-ui/out/_next/static/chunks/665.ae67dcf3c1b6f7f6.js
  79. 1
      striker-ui/out/_next/static/chunks/668-b264bf73f0c1b5eb.js
  80. 1
      striker-ui/out/_next/static/chunks/675-235890fb4812bd16.js
  81. 1
      striker-ui/out/_next/static/chunks/707-ee38ab2abcd0aa3f.js
  82. 1
      striker-ui/out/_next/static/chunks/733-a945bbb3c5f55f74.js
  83. 1
      striker-ui/out/_next/static/chunks/750-b9b6c5fdabc264a0.js
  84. 1
      striker-ui/out/_next/static/chunks/775-3f1c58f77437bd5d.js
  85. 1
      striker-ui/out/_next/static/chunks/804-a6d43595270ed0d2.js
  86. 1
      striker-ui/out/_next/static/chunks/839-dabd319a60c8df83.js
  87. 1
      striker-ui/out/_next/static/chunks/86-9d0634bddd7b8dc2.js
  88. 1
      striker-ui/out/_next/static/chunks/894-e57948de523bcf96.js
  89. 1
      striker-ui/out/_next/static/chunks/910-2a0e86a170f6eb77.js
  90. 1
      striker-ui/out/_next/static/chunks/924-2a2fdb45d3e02493.js
  91. 1
      striker-ui/out/_next/static/chunks/936-f64829e0e2013921.js
  92. 33
      striker-ui/out/_next/static/chunks/framework-467b11a89995b152.js
  93. 1
      striker-ui/out/_next/static/chunks/framework-5f4595e5518b5600.js
  94. 1
      striker-ui/out/_next/static/chunks/main-a054bbf31fb90f6a.js
  95. 1
      striker-ui/out/_next/static/chunks/main-e4de6784702770d6.js
  96. 1
      striker-ui/out/_next/static/chunks/pages/_app-adb26b7c14183a1f.js
  97. 1
      striker-ui/out/_next/static/chunks/pages/_app-e9a8bcd19cdef599.js
  98. 1
      striker-ui/out/_next/static/chunks/pages/_error-2280fa386d040b66.js
  99. 1
      striker-ui/out/_next/static/chunks/pages/_error-a9572f84d60f21da.js
  100. 1
      striker-ui/out/_next/static/chunks/pages/anvil-38307a04a51f8094.js
  101. Some files were not shown because too many files have changed in this diff Show More

File diff suppressed because one or more lines are too long

@ -75,7 +75,7 @@ export const execManageAlerts = (
result = spawnSync( result = spawnSync(
SERVER_PATHS.usr.sbin['anvil-manage-alerts'].self, SERVER_PATHS.usr.sbin['anvil-manage-alerts'].self,
commandArgs, commandArgs,
{ encoding: 'utf-8', timeout: 10000 }, { encoding: 'utf-8', timeout: 30000 },
); );
const { error, signal, status, stderr, stdout } = result; const { error, signal, status, stderr, stdout } = result;

@ -5,11 +5,14 @@ import {
AutocompleteRenderInputParams as MUIAutocompleteRenderInputParams, AutocompleteRenderInputParams as MUIAutocompleteRenderInputParams,
Box, Box,
Grow as MUIGrow, Grow as MUIGrow,
ListSubheader,
outlinedInputClasses as muiOutlinedInputClasses, outlinedInputClasses as muiOutlinedInputClasses,
Paper as MUIPaper, Paper as MUIPaper,
PaperProps as MUIPaperProps, PaperProps as MUIPaperProps,
svgIconClasses as muiSvgIconClasses, svgIconClasses as muiSvgIconClasses,
styled,
} from '@mui/material'; } from '@mui/material';
import { useMemo } from 'react';
import { GREY, TEXT } from '../lib/consts/DEFAULT_THEME'; import { GREY, TEXT } from '../lib/consts/DEFAULT_THEME';
@ -24,6 +27,7 @@ type AutocompleteOptionalProps = {
inputWithLabelProps: OutlinedInputWithLabelProps, inputWithLabelProps: OutlinedInputWithLabelProps,
renderInputParams?: MUIAutocompleteRenderInputParams, renderInputParams?: MUIAutocompleteRenderInputParams,
) => void; ) => void;
getGroupLabel?: (group: string) => React.ReactNode;
messageBoxProps?: Partial<MessageBoxProps>; messageBoxProps?: Partial<MessageBoxProps>;
}; };
@ -49,6 +53,12 @@ const GrowPaper = (paperProps: MUIPaperProps): JSX.Element => (
</MUIGrow> </MUIGrow>
); );
const GroupChildren = styled('ul')({
padding: 0,
});
const GroupHeader = ListSubheader;
const Autocomplete = < const Autocomplete = <
T, T,
Multiple extends boolean | undefined = undefined, Multiple extends boolean | undefined = undefined,
@ -60,31 +70,72 @@ const Autocomplete = <
const { const {
componentsProps, componentsProps,
extendRenderInput, extendRenderInput,
getGroupLabel,
label, label,
messageBoxProps, messageBoxProps,
renderGroup,
renderInput, renderInput,
sx, sx,
...autocompleteRestProps ...autocompleteRestProps
} = autocompleteProps; } = autocompleteProps;
const combinedComponentsProps: AutocompleteProps<
const combinedComponentsProps = useMemo<
AutocompleteProps<
T, T,
Multiple, Multiple,
DisableClearable, DisableClearable,
FreeSolo FreeSolo
>['componentsProps'] = { >['componentsProps']
>(
() => ({
paper: { paper: {
sx: { sx: {
backgroundColor: TEXT, backgroundColor: TEXT,
[`& .${muiAutocompleteClasses.groupLabel}`]: {
backgroundColor: TEXT,
},
}, },
}, },
...componentsProps, ...componentsProps,
}; }),
const combinedRenderInput = [componentsProps],
);
const combinedRenderGroup = useMemo<
AutocompleteProps<T, Multiple, DisableClearable, FreeSolo>['renderGroup']
>(() => {
if (renderGroup) return renderGroup;
return (
getGroupLabel &&
((params) => (
<li key={params.key}>
<GroupHeader
component="div"
className={muiAutocompleteClasses.groupLabel}
>
{getGroupLabel(params.group)}
</GroupHeader>
<GroupChildren className={muiAutocompleteClasses.groupUl}>
{params.children}
</GroupChildren>
</li>
))
);
}, [getGroupLabel, renderGroup]);
const combinedRenderInput = useMemo<
Exclude<
AutocompleteProps<T, Multiple, DisableClearable, FreeSolo>['renderInput'],
undefined
>
>(
() =>
renderInput ?? renderInput ??
((renderInputParams) => { ((params) => {
const { fullWidth, InputProps, InputLabelProps, inputProps } = const { fullWidth, InputProps, InputLabelProps, inputProps } = params;
renderInputParams;
const inputWithLabelProps: OutlinedInputWithLabelProps = { const inputWithLabelProps: OutlinedInputWithLabelProps = {
formControlProps: { formControlProps: {
fullWidth, fullWidth,
@ -100,11 +151,17 @@ const Autocomplete = <
label, label,
}; };
extendRenderInput?.call(null, inputWithLabelProps, renderInputParams); extendRenderInput?.call(null, inputWithLabelProps, params);
return <OutlinedInputWithLabel {...inputWithLabelProps} />; return <OutlinedInputWithLabel {...inputWithLabelProps} />;
}); }),
const combinedSx = { [extendRenderInput, label, renderInput],
);
const combinedSx = useMemo<
AutocompleteProps<T, Multiple, DisableClearable, FreeSolo>['sx']
>(
() => ({
[`& .${muiOutlinedInputClasses.root} .${muiAutocompleteClasses.endAdornment}`]: [`& .${muiOutlinedInputClasses.root} .${muiAutocompleteClasses.endAdornment}`]:
{ {
right: `7px`, right: `7px`,
@ -115,18 +172,19 @@ const Autocomplete = <
}, },
...sx, ...sx,
}; }),
[sx],
);
return ( return (
<Box sx={{ display: 'flex', flexDirection: 'column' }}> <Box sx={{ display: 'flex', flexDirection: 'column' }}>
<MUIAutocomplete <MUIAutocomplete
{...{ PaperComponent={GrowPaper}
PaperComponent: GrowPaper, {...autocompleteRestProps}
...autocompleteRestProps, componentsProps={combinedComponentsProps}
componentsProps: combinedComponentsProps, renderGroup={combinedRenderGroup}
renderInput: combinedRenderInput, renderInput={combinedRenderInput}
sx: combinedSx, sx={combinedSx}
}}
/> />
<InputMessageBox {...messageBoxProps} /> <InputMessageBox {...messageBoxProps} />
</Box> </Box>

@ -0,0 +1,214 @@
import { FC, useMemo, useRef, useState } from 'react';
import api from '../lib/api';
import { DialogWithHeader } from './Dialog';
import handleAPIError from '../lib/handleAPIError';
import List from './List';
import useActiveFetch from '../hooks/useActiveFetch';
import useChecklist from '../hooks/useChecklist';
import useConfirmDialog from '../hooks/useConfirmDialog';
import useFetch from '../hooks/useFetch';
const reduceHeader = <A extends unknown[], R extends React.ReactNode>(
header: R | ((...args: A) => R),
...args: A
): R => (typeof header === 'function' ? header(...args) : header);
const CrudList = <
Overview,
Detail,
OverviewList extends Record<string, Overview> = Record<string, Overview>,
>(
...[props]: Parameters<FC<CrudListProps<Overview, Detail, OverviewList>>>
): ReturnType<FC<CrudListProps<Overview, Detail, OverviewList>>> => {
const {
addHeader: rAddHeader,
editHeader: rEditHeader,
entriesUrl,
getAddLoading,
getDeleteErrorMessage,
getDeleteHeader,
getDeletePromiseChain = (base, ...args) => base(...args),
getDeleteSuccessMessage,
getEditLoading = (previous?: boolean) => previous,
listEmpty,
listProps,
onItemClick = (base, ...args) => base(...args),
refreshInterval = 5000,
renderAddForm,
renderDeleteItem,
renderEditForm,
renderListItem,
} = props;
const addDialogRef = useRef<DialogForwardedRefContent>(null);
const editDialogRef = useRef<DialogForwardedRefContent>(null);
const {
confirmDialog,
finishConfirm,
setConfirmDialogLoading,
setConfirmDialogOpen,
setConfirmDialogProps,
} = useConfirmDialog({ initial: { scrollContent: true } });
const [edit, setEdit] = useState<boolean>(false);
const [entry, setEntry] = useState<Detail | undefined>();
const [entries, setEntries] = useState<OverviewList | undefined>();
const { loading: loadingEntriesPeriodic } = useFetch<OverviewList>(
entriesUrl,
{
onSuccess: (data) => setEntries(data),
refreshInterval,
},
);
const { fetch: getEntries, loading: loadingEntriesActive } =
useActiveFetch<OverviewList>({
onData: (data) => setEntries(data),
url: entriesUrl,
});
const { fetch: getEntry, loading: loadingEntry } = useActiveFetch<Detail>({
onData: (data) => setEntry(data),
url: entriesUrl,
});
const addHeader = useMemo<React.ReactNode>(
() => reduceHeader(rAddHeader),
[rAddHeader],
);
const editHeader = useMemo<React.ReactNode>(
() => reduceHeader(rEditHeader, entry),
[entry, rEditHeader],
);
const formTools = useMemo<CrudListFormTools>(
() => ({
add: {
open: (v = true) => addDialogRef?.current?.setOpen(v),
},
confirm: {
finish: finishConfirm,
loading: setConfirmDialogLoading,
open: (v = true) => setConfirmDialogOpen(v),
prepare: setConfirmDialogProps,
},
edit: {
open: (v = true) => editDialogRef?.current?.setOpen(v),
},
}),
[
finishConfirm,
setConfirmDialogLoading,
setConfirmDialogOpen,
setConfirmDialogProps,
],
);
const loadingEntries = useMemo<boolean>(
() => loadingEntriesPeriodic || loadingEntriesActive,
[loadingEntriesActive, loadingEntriesPeriodic],
);
const {
buildDeleteDialogProps,
checks,
getCheck,
hasAllChecks,
hasChecks,
multipleItems,
resetChecks,
setAllChecks,
setCheck,
} = useChecklist({ list: entries });
return (
<>
<List<Overview>
allowCheckAll={multipleItems}
allowEdit
allowItemButton={edit}
disableDelete={!hasChecks}
edit={edit}
getListCheckboxProps={() => ({
checked: hasAllChecks,
onChange: (event, checked) => setAllChecks(checked),
})}
getListItemCheckboxProps={(key) => ({
checked: getCheck(key),
onChange: (event, checked) => setCheck(key, checked),
})}
header
listEmpty={listEmpty}
listItems={entries}
loading={loadingEntries}
onAdd={() => addDialogRef?.current?.setOpen(true)}
onDelete={() => {
setConfirmDialogProps(
buildDeleteDialogProps({
onProceedAppend: () => {
setConfirmDialogLoading(true);
Promise.all(
getDeletePromiseChain(
(cl, up) => cl.map((key) => api.delete(`${up}/${key}`)),
checks,
entriesUrl,
),
)
.then(() => {
finishConfirm('Success', getDeleteSuccessMessage());
getEntries();
})
.catch((error) => {
const emsg = handleAPIError(error);
finishConfirm('Error', getDeleteErrorMessage(emsg));
});
resetChecks();
},
getConfirmDialogTitle: getDeleteHeader,
renderEntry: (...args) => renderDeleteItem(entries, ...args),
}),
);
setConfirmDialogOpen(true);
}}
onEdit={() => setEdit((previous) => !previous)}
onItemClick={(...args) =>
onItemClick((value, key) => {
editDialogRef?.current?.setOpen(true);
getEntry(`/${key}`);
}, ...args)
}
renderListItem={renderListItem}
{...listProps}
/>
<DialogWithHeader
header={addHeader}
loading={getAddLoading?.call(null)}
ref={addDialogRef}
showClose
>
{renderAddForm(formTools)}
</DialogWithHeader>
<DialogWithHeader
header={editHeader}
loading={getEditLoading(loadingEntry)}
ref={editDialogRef}
showClose
>
{renderEditForm(formTools, entry)}
</DialogWithHeader>
{confirmDialog}
</>
);
};
export default CrudList;

@ -13,9 +13,9 @@ import { v4 as uuidv4 } from 'uuid';
import ActionGroup from '../ActionGroup'; import ActionGroup from '../ActionGroup';
import api from '../../lib/api'; import api from '../../lib/api';
import ContainedButton from '../ContainedButton'; import ContainedButton from '../ContainedButton';
import convertFormikErrorsToMessages from '../../lib/convertFormikErrorsToMessages';
import FileInputGroup from './FileInputGroup'; import FileInputGroup from './FileInputGroup';
import FlexBox from '../FlexBox'; import FlexBox from '../FlexBox';
import getFormikErrorMessages from '../../lib/getFormikErrorMessages';
import handleAPIError from '../../lib/handleAPIError'; import handleAPIError from '../../lib/handleAPIError';
import MessageBox from '../MessageBox'; import MessageBox from '../MessageBox';
import MessageGroup from '../MessageGroup'; import MessageGroup from '../MessageGroup';
@ -111,7 +111,7 @@ const AddFileForm: FC<AddFileFormProps> = (props) => {
}); });
const formikErrors = useMemo<Messages>( const formikErrors = useMemo<Messages>(
() => convertFormikErrorsToMessages(formik.errors), () => getFormikErrorMessages(formik.errors),
[formik.errors], [formik.errors],
); );

@ -3,9 +3,9 @@ import { FC, useCallback, useMemo, useRef } from 'react';
import ActionGroup from '../ActionGroup'; import ActionGroup from '../ActionGroup';
import api from '../../lib/api'; import api from '../../lib/api';
import convertFormikErrorsToMessages from '../../lib/convertFormikErrorsToMessages';
import FileInputGroup from './FileInputGroup'; import FileInputGroup from './FileInputGroup';
import FlexBox from '../FlexBox'; import FlexBox from '../FlexBox';
import getFormikErrorMessages from '../../lib/getFormikErrorMessages';
import handleAPIError from '../../lib/handleAPIError'; import handleAPIError from '../../lib/handleAPIError';
import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup'; import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup';
import fileListSchema from './schema'; import fileListSchema from './schema';
@ -131,7 +131,7 @@ const EditFileForm: FC<EditFileFormProps> = (props) => {
}); });
const formikErrors = useMemo<Messages>( const formikErrors = useMemo<Messages>(
() => convertFormikErrorsToMessages(formik.errors), () => getFormikErrorMessages(formik.errors),
[formik.errors], [formik.errors],
); );

@ -1,9 +1,5 @@
import { import { Box, List as MUIList, ListItem as MUIListItem } from '@mui/material';
Box, import { capitalize } from 'lodash';
List as MUIList,
ListItem as MUIListItem,
capitalize,
} from '@mui/material';
import { FC, ReactElement } from 'react'; import { FC, ReactElement } from 'react';
import FlexBox from './FlexBox'; import FlexBox from './FlexBox';

@ -75,7 +75,7 @@ const GateForm = forwardRef<GateFormForwardedRefContent, GateFormProps>(
const [isSubmitting, setIsSubmitting] = useState<boolean>(false); const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const formUtils = useFormUtils( const formUtils = useFormUtils(
[INPUT_ID_GATE_ID, INPUT_ID_GATE_PASSPHRASE], [identifierId, passphraseId],
messageGroupRef, messageGroupRef,
); );
const { const {
@ -101,10 +101,10 @@ const GateForm = forwardRef<GateFormForwardedRefContent, GateFormProps>(
const { elements } = target as HTMLFormElement; const { elements } = target as HTMLFormElement;
const { value: identifierValue } = elements.namedItem( const { value: identifierValue } = elements.namedItem(
INPUT_ID_GATE_ID, identifierId,
) as HTMLInputElement; ) as HTMLInputElement;
const { value: passphraseValue } = elements.namedItem( const { value: passphraseValue } = elements.namedItem(
INPUT_ID_GATE_PASSPHRASE, passphraseId,
) as HTMLInputElement; ) as HTMLInputElement;
onSubmitAppend?.call( onSubmitAppend?.call(
@ -118,7 +118,14 @@ const GateForm = forwardRef<GateFormForwardedRefContent, GateFormProps>(
...args, ...args,
); );
}), }),
[onSubmit, onSubmitAppend, setIsSubmitting, setMessage], [
identifierId,
onSubmit,
onSubmitAppend,
passphraseId,
setIsSubmitting,
setMessage,
],
); );
const submitElement = useMemo( const submitElement = useMemo(

@ -14,7 +14,6 @@ import {
Theme, Theme,
} from '@mui/material'; } from '@mui/material';
import { FC, forwardRef, useCallback, useMemo } from 'react'; import { FC, forwardRef, useCallback, useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { BLUE, BORDER_RADIUS, GREY, RED } from '../lib/consts/DEFAULT_THEME'; import { BLUE, BORDER_RADIUS, GREY, RED } from '../lib/consts/DEFAULT_THEME';
@ -40,7 +39,7 @@ const List = forwardRef(
insertHeader: isInsertHeader = true, insertHeader: isInsertHeader = true,
listEmpty, listEmpty,
listItemIconMinWidth = '56px', listItemIconMinWidth = '56px',
listItemKeyPrefix = uuidv4(), listItemKeyPrefix = 'list',
listItemProps: { sx: listItemSx, ...restListItemProps } = {}, listItemProps: { sx: listItemSx, ...restListItemProps } = {},
listItems, listItems,
listProps: { sx: listSx, ...restListProps } = {}, listProps: { sx: listSx, ...restListProps } = {},
@ -226,18 +225,18 @@ const List = forwardRef(
); );
}); });
}, [ }, [
loading, getListItemCheckboxProps,
listItems, isAllowItemButton,
listEmptyElement, listEmptyElement,
renderListItem, listItemCheckbox,
restListItemProps,
listItemKeyPrefix, listItemKeyPrefix,
listItemSx, listItemSx,
listItemCheckbox, listItems,
renderListItemCheckboxState, loading,
getListItemCheckboxProps,
isAllowItemButton,
onItemClick, onItemClick,
renderListItem,
renderListItemCheckboxState,
restListItemProps,
]); ]);
const listScrollSx: SxProps<Theme> | undefined = useMemo( const listScrollSx: SxProps<Theme> | undefined = useMemo(
() => (isScroll ? { maxHeight: '100%', overflowY: 'scroll' } : undefined), () => (isScroll ? { maxHeight: '100%', overflowY: 'scroll' } : undefined),

@ -0,0 +1,384 @@
import { Grid, menuClasses as muiMenuClasses } from '@mui/material';
import { AxiosError } from 'axios';
import { FC, useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';
import ActionGroup from '../ActionGroup';
import api from '../../lib/api';
import FlexBox from '../FlexBox';
import FormSummary from '../FormSummary';
import handleAPIError from '../../lib/handleAPIError';
import mailRecipientListSchema from './schema';
import ManageAlertOverride from './ManageAlertOverride';
import MessageGroup from '../MessageGroup';
import OutlinedInputWithLabel from '../OutlinedInputWithLabel';
import SelectWithLabel from '../SelectWithLabel';
import { BodyText, SmallText } from '../Text';
import UncontrolledInput from '../UncontrolledInput';
import useFormikUtils from '../../hooks/useFormikUtils';
/**
* TODO: add descriptions to each item:
*
=head4 1 / critical
Alerts at this level will go to all recipients, except for those ignoring the source system entirely.
This is reserved for alerts that could lead to imminent service interruption or unexpected loss of redundancy.
Alerts at this level should trigger alarm systems for all administrators as well as management who may be impacted by service interruptions.
=head4 2 / warning
This is used for alerts that require attention from administrators. Examples include intentional loss of redundancy caused by load shedding, hardware in pre-failure, loss of input power, temperature anomalies, etc.
Alerts at this level should trigger alarm systems for administrative staff.
=head4 3 / notice
This is used for alerts that are generally safe to ignore, but might provide early warnings of developing issues or insight into system behaviour.
Alerts at this level should not trigger alarm systems. Periodic review is sufficient.
=head4 4 / info
This is used for alerts that are almost always safe to ignore, but may be useful in testing and debugging.
*
*/
const LEVEL_OPTIONS: SelectItem<number>[] = [
{
displayValue: (
<FlexBox spacing={0}>
<BodyText inheritColour fontWeight="inherit">
Critical
</BodyText>
<SmallText inheritColour whiteSpace="normal">
Alerts that could lead to imminent service interruption or unexpected
loss of redundancy.
</SmallText>
</FlexBox>
),
value: 1,
},
{
displayValue: (
<FlexBox spacing={0}>
<BodyText inheritColour fontWeight="inherit">
Warning
</BodyText>
<SmallText inheritColour whiteSpace="normal">
Alerts that require attention from administrators, such as redundancy
loss due to load shedding, hardware in pre-failure, input power loss,
temperature anomalies, etc.
</SmallText>
</FlexBox>
),
value: 2,
},
{
displayValue: (
<FlexBox spacing={0}>
<BodyText inheritColour fontWeight="inherit">
Notice
</BodyText>
<SmallText inheritColour whiteSpace="normal">
Alerts that are generally safe to ignore, but might provide early
warnings of developing issues or insight into system behaviour.
</SmallText>
</FlexBox>
),
value: 3,
},
{
displayValue: (
<FlexBox spacing={0}>
<BodyText inheritColour fontWeight="inherit">
Info
</BodyText>
<SmallText inheritColour whiteSpace="normal">
Alerts that are almost always safe to ignore, but may be useful in
testing and debugging.
</SmallText>
</FlexBox>
),
value: 4,
},
];
const MAP_TO_LEVEL_LABEL: Record<number, string> = {
1: 'Critical',
2: 'Warning',
3: 'Notice',
4: 'Info',
};
const getAlertOverrideRequestList = (
current: MailRecipientFormikMailRecipient,
initial?: MailRecipientFormikMailRecipient,
urlPrefix = '/alert-override',
): AlertOverrideRequest[] => {
const { uuid: mailRecipientUuid } = current;
if (!mailRecipientUuid) return [];
return Object.values(current.alertOverrides).reduce<AlertOverrideRequest[]>(
(previous, { remove, level, target, uuids: existingOverrides }) => {
/**
* There's no update, just delete every record and create the new records.
*
* This is not optimal, but keep it until there's a better solution.
*/
if (existingOverrides) {
previous.push(
...Object.keys(existingOverrides).map<AlertOverrideRequest>(
(overrideUuid) => ({
method: 'delete',
url: `${urlPrefix}/${overrideUuid}`,
}),
),
);
}
if (target && !remove) {
const newHosts: string[] = target.subnodes ?? [target.uuid];
previous.push(
...newHosts.map<AlertOverrideRequest>((hostUuid) => ({
body: { hostUuid, level, mailRecipientUuid },
method: 'post',
url: urlPrefix,
})),
);
}
return previous;
},
[],
);
};
const AddMailRecipientForm: FC<AddMailRecipientFormProps> = (props) => {
const {
alertOverrideTargetOptions,
mailRecipientUuid,
previousFormikValues,
tools,
} = props;
const mrUuid = useMemo<string>(
() => mailRecipientUuid ?? uuidv4(),
[mailRecipientUuid],
);
const formikUtils = useFormikUtils<MailRecipientFormikValues>({
initialValues: previousFormikValues ?? {
[mrUuid]: {
alertOverrides: {},
email: '',
language: 'en_CA',
level: 2,
name: '',
},
},
onSubmit: (values, { setSubmitting }) => {
const { [mrUuid]: mailRecipient } = values;
let actionProceedText: string = 'Add';
let errorMessage: React.ReactNode = <>Failed to add mail recipient.</>;
let method: 'post' | 'put' = 'post';
let successMessage: React.ReactNode = <>Mail recipient added.</>;
let titleText: string = `Add mail recipient with the following?`;
let url: string = '/mail-recipient';
if (previousFormikValues) {
actionProceedText = 'Update';
errorMessage = <>Failed to update mail server.</>;
method = 'put';
successMessage = <>Mail recipient updated.</>;
titleText = `Update ${mailRecipient.name} with the following?`;
url += `/${mrUuid}`;
}
const { alertOverrides, uuid: ignore, ...mrBody } = mailRecipient;
tools.confirm.prepare({
actionProceedText,
content: (
<>
<FormSummary entries={mrBody} />
<FormSummary
entries={{
alertOverrides: Object.entries(alertOverrides).reduce<
Record<string, { level: number; name: string }>
>((previous, [valueId, value]) => {
if (value.remove || !value.target) return previous;
previous[valueId] = {
level: value.level,
name: value.target.name,
};
return previous;
}, {}),
}}
/>
</>
),
onCancelAppend: () => setSubmitting(false),
onProceedAppend: async () => {
tools.confirm.loading(true);
const handleError = (error: AxiosError) => {
const emsg = handleAPIError(error);
emsg.children = (
<>
{errorMessage} {emsg.children}
</>
);
tools.confirm.finish('Error', emsg);
setSubmitting(false);
};
// Handle the mail recipient first, wait until it's done to process
// the related alert override records.
api[method](url, mrBody)
.then((response) => {
const { data } = response;
const shallow = { ...mailRecipient };
if (data) {
shallow.uuid = data.uuid;
}
const initial =
previousFormikValues && previousFormikValues[mrUuid];
const promises = getAlertOverrideRequestList(
shallow,
initial,
).map((request) =>
api[request.method](request.url, request.body),
);
Promise.all(promises)
.then(() => {
tools.confirm.finish('Success', { children: successMessage });
tools[method === 'post' ? 'add' : 'edit'].open(false);
})
.catch(handleError);
})
.catch(handleError);
},
titleText,
});
tools.confirm.open(true);
},
validationSchema: mailRecipientListSchema,
});
const { disabledSubmit, formik, formikErrors, handleChange } = formikUtils;
const emailChain = useMemo<string>(() => `${mrUuid}.email`, [mrUuid]);
const levelChain = useMemo<string>(() => `${mrUuid}.level`, [mrUuid]);
const nameChain = useMemo<string>(() => `${mrUuid}.name`, [mrUuid]);
return (
<Grid
columns={{ xs: 1, sm: 2 }}
component="form"
container
onSubmit={(event) => {
event.preventDefault();
formik.submitForm();
}}
spacing="1em"
>
<Grid item xs={1}>
<UncontrolledInput
input={
<OutlinedInputWithLabel
id={nameChain}
label="Recipient name"
name={nameChain}
onChange={handleChange}
required
value={formik.values[mrUuid].name}
/>
}
/>
</Grid>
<Grid item xs={1}>
<UncontrolledInput
input={
<OutlinedInputWithLabel
id={emailChain}
label="Recipient email"
name={emailChain}
onChange={handleChange}
required
value={formik.values[mrUuid].email}
/>
}
/>
</Grid>
<Grid item xs={1}>
<UncontrolledInput
input={
<SelectWithLabel
id={levelChain}
label="Alert level"
name={levelChain}
onChange={formik.handleChange}
required
selectItems={LEVEL_OPTIONS}
selectProps={{
MenuProps: {
sx: {
[`& .${muiMenuClasses.paper}`]: {
maxWidth: { md: '60%', lg: '40%' },
},
},
},
renderValue: (value) => MAP_TO_LEVEL_LABEL[value],
}}
value={formik.values[mrUuid].level}
/>
}
/>
</Grid>
<Grid item width="100%">
<ManageAlertOverride
alertOverrideTargetOptions={alertOverrideTargetOptions}
formikUtils={formikUtils}
mailRecipientUuid={mrUuid}
/>
</Grid>
<Grid item width="100%">
<MessageGroup count={1} messages={formikErrors} />
</Grid>
<Grid item width="100%">
<ActionGroup
actions={[
{
background: 'blue',
children: previousFormikValues ? 'Update' : 'Add',
disabled: disabledSubmit,
type: 'submit',
},
]}
/>
</Grid>
</Grid>
);
};
export default AddMailRecipientForm;

@ -0,0 +1,139 @@
import { Grid } from '@mui/material';
import { FC, useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';
import Autocomplete from '../Autocomplete';
import FlexBox from '../FlexBox';
import IconButton from '../IconButton';
import SelectWithLabel from '../SelectWithLabel';
import { BodyText, SmallText } from '../Text';
import UncontrolledInput from '../UncontrolledInput';
const LEVEL_OPTIONS: SelectItem<number>[] = [
{ displayValue: 'Ignore', value: 0 },
{ displayValue: 'Critical', value: 1 },
{ displayValue: 'Warning', value: 2 },
{ displayValue: 'Notice', value: 3 },
{ displayValue: 'Info', value: 4 },
];
const AlertOverrideInputGroup: FC<AlertOverrideInputGroupProps> = (props) => {
const {
alertOverrideTargetOptions,
alertOverrideValueId,
mailRecipientUuid: mrUuid,
formikUtils,
} = props;
/**
* This is the alert override formik value identifier; not to be confused
* with the alert override UUID.
*/
const aoVid = useMemo<string>(
() => alertOverrideValueId ?? uuidv4(),
[alertOverrideValueId],
);
const { formik } = formikUtils;
const {
values: { [mrUuid]: mailRecipient },
} = formik;
const {
alertOverrides: { [aoVid]: alertOverride },
} = mailRecipient;
const overrideChain = useMemo<string>(
() => `${mrUuid}.alertOverrides.${aoVid}`,
[aoVid, mrUuid],
);
const removeChain = useMemo<string>(
() => `${overrideChain}.remove`,
[overrideChain],
);
const targetChain = useMemo<string>(
() => `${overrideChain}.target`,
[overrideChain],
);
const levelChain = useMemo<string>(
() => `${overrideChain}.level`,
[overrideChain],
);
return (
<Grid
alignItems="center"
columns={{ xs: 1, sm: 10 }}
container
justifyContent="stretch"
spacing="1em"
>
<Grid item xs={6}>
<Autocomplete
getOptionLabel={(option) => option.name}
id={targetChain}
isOptionEqualToValue={(option, value) => option.uuid === value.uuid}
label="Target"
noOptionsText="No node or subnode found."
onChange={(event, value) =>
formik.setFieldValue(targetChain, value, true)
}
openOnFocus
options={alertOverrideTargetOptions}
renderOption={(optionProps, option) => (
<li {...optionProps} key={`${option.node}-${option.uuid}`}>
{option.type === 'node' ? (
<FlexBox spacing={0}>
<BodyText inheritColour>{option.name}</BodyText>
<SmallText inheritColour>{option.description}</SmallText>
</FlexBox>
) : (
<BodyText inheritColour paddingLeft=".6em">
{option.name}
</BodyText>
)}
</li>
)}
value={alertOverride.target}
/>
</Grid>
<Grid item flexGrow={1}>
<UncontrolledInput
input={
<SelectWithLabel
id={levelChain}
label="Alert level"
name={levelChain}
onChange={formik.handleChange}
selectItems={LEVEL_OPTIONS}
value={alertOverride.level}
/>
}
/>
</Grid>
<Grid item width="min-content">
<IconButton
mapPreset="delete"
onClick={() => {
if (alertOverride.uuids) {
formik.setFieldValue(removeChain, true, true);
} else {
formik.setValues((previous: MailRecipientFormikValues) => {
const shallow = { ...previous };
const { [mrUuid]: mr } = shallow;
const { [aoVid]: remove, ...keep } = mr.alertOverrides;
mr.alertOverrides = { ...keep };
return shallow;
});
}
}}
size="small"
/>
</Grid>
</Grid>
);
};
export default AlertOverrideInputGroup;

@ -0,0 +1,9 @@
import { FC } from 'react';
import AddMailRecipientForm from './AddMailRecipientForm';
const EditMailRecipientForm: FC<EditMailRecipientFormProps> = (props) => (
<AddMailRecipientForm {...props} />
);
export default EditMailRecipientForm;

@ -0,0 +1,60 @@
import { FC } from 'react';
import { v4 as uuidv4 } from 'uuid';
import AlertOverrideInputGroup from './AlertOverrideInputGroup';
import List from '../List';
const ManageAlertOverride: FC<ManageAlertOverrideProps> = (props) => {
const {
alertOverrideTargetOptions,
formikUtils,
mailRecipientUuid: mrUuid,
} = props;
const { formik } = formikUtils;
const {
values: { [mrUuid]: mailRecipient },
} = formik;
const { alertOverrides } = mailRecipient;
return (
<List
allowAddItem
edit
header="Alert override rules"
listEmpty="No alert overrides(s)"
listItems={alertOverrides}
onAdd={() => {
/**
* This is **not** the same as an alert override UUID because 1 alert
* override formik value can reference _n_ alert override rules, where
* _n_ is the number of subnodes per node. */
const valueId = uuidv4();
formik.setValues((previous: MailRecipientFormikValues) => {
const shallow = { ...previous };
const { [mrUuid]: mr } = shallow;
mr.alertOverrides = {
...mr.alertOverrides,
[valueId]: { level: 2, target: null },
};
return shallow;
});
}}
renderListItem={(valueId, value: AlertOverrideFormikAlertOverride) =>
!value.remove && (
<AlertOverrideInputGroup
alertOverrideTargetOptions={alertOverrideTargetOptions}
alertOverrideValueId={valueId}
formikUtils={formikUtils}
mailRecipientUuid={mrUuid}
/>
)
}
/>
);
};
export default ManageAlertOverride;

@ -0,0 +1,216 @@
import { FC, useMemo, useState } from 'react';
import AddMailRecipientForm from './AddMailRecipientForm';
import { toAnvilOverviewList } from '../../lib/api_converters';
import CrudList from '../CrudList';
import EditMailRecipientForm from './EditMailRecipientForm';
import { BodyText } from '../Text';
import useActiveFetch from '../../hooks/useActiveFetch';
import useFetch from '../../hooks/useFetch';
const ManageMailRecipient: FC = () => {
const [alertOverrides, setAlertOverrides] = useState<
APIAlertOverrideOverviewList | undefined
>();
const { altData: nodes, loading: loadingNodes } = useFetch<
APIAnvilOverviewArray,
APIAnvilOverviewList
>('/anvil', { mod: toAnvilOverviewList });
const alertOverrideTargetOptions = useMemo<AlertOverrideTarget[] | undefined>(
() =>
nodes &&
Object.values(nodes)
.sort((a, b) => a.name.localeCompare(b.name))
.reduce<AlertOverrideTarget[]>((options, node) => {
const nodeTarget: AlertOverrideTarget = {
description: node.description,
name: node.name,
node: node.uuid,
subnodes: [],
type: 'node',
uuid: node.uuid,
};
const subnodeTargets = Object.values(node.hosts)
.sort((a, b) => a.name.localeCompare(b.name))
.reduce<AlertOverrideTarget[]>((previous, subnode) => {
if (subnode.type === 'dr') return previous;
previous.push({
name: subnode.name,
node: node.uuid,
type: 'subnode',
uuid: subnode.uuid,
});
nodeTarget.subnodes?.push(subnode.uuid);
return previous;
}, []);
// Append the options in sequence: node followed by its subnode(s).
options.push(nodeTarget, ...subnodeTargets);
return options;
}, []),
[nodes],
);
const { fetch: getAlertOverrides, loading: loadingAlertOverrides } =
useActiveFetch<APIAlertOverrideOverviewList>({
onData: (data) => setAlertOverrides(data),
url: '/alert-override',
});
const formikAlertOverrides = useMemo<
AlertOverrideFormikValues | undefined
>(() => {
if (!nodes || !alertOverrides) return undefined;
/**
* Group alert override rules based on node UUID. The groups will be used
* for comparison to see whether the subnodes are assigned the same alert
* level.
*
* If subnodes have the same level, they will be consolidated into a single
* target for display. Otherwise, every subnode will get its own visual.
*/
const groups = Object.values(alertOverrides).reduce<
Record<string, APIAlertOverrideOverview[]>
>((previous, override) => {
const {
node: { uuid: nodeUuid },
} = override;
if (previous[nodeUuid]) {
previous[nodeUuid].push(override);
} else {
previous[nodeUuid] = [override];
}
return previous;
}, {});
return Object.entries(groups).reduce<AlertOverrideFormikValues>(
(previous, pair) => {
const [nodeUuid, overrides] = pair;
const [firstOverride, ...restOverrides] = overrides;
const sameLevel =
overrides.length > 1 &&
restOverrides.every(({ level }) => level === firstOverride.level);
if (sameLevel) {
const {
0: { level },
} = overrides;
const { [nodeUuid]: node } = nodes;
previous[nodeUuid] = {
level,
target: {
description: node.description,
name: node.name,
node: node.uuid,
subnodes: overrides.map<string>(({ subnode: { uuid } }) => uuid),
type: 'node',
uuid: node.uuid,
},
uuids: overrides.reduce<Record<string, string>>(
(uuids, { subnode, uuid: overrideUuid }) => {
uuids[overrideUuid] = subnode.uuid;
return uuids;
},
{},
),
};
} else {
overrides.forEach(({ level, node, subnode, uuid: overrideUuid }) => {
previous[subnode.uuid] = {
level,
target: {
name: subnode.name,
node: node.uuid,
type: 'subnode',
uuid: subnode.uuid,
},
uuids: { [overrideUuid]: subnode.uuid },
};
});
}
return previous;
},
{},
);
}, [alertOverrides, nodes]);
return (
<>
<CrudList<APIMailRecipientOverview, APIMailRecipientDetail>
addHeader="Add mail recipient"
editHeader={(entry) => `Update ${entry?.name}`}
entriesUrl="/mail-recipient"
getAddLoading={(previous) => previous || loadingNodes}
getDeleteErrorMessage={({ children, ...rest }) => ({
...rest,
children: <>Failed to delete mail recipient(s). {children}</>,
})}
getDeleteHeader={(count) =>
`Delete the following ${count} mail recipient(s)?`
}
getDeleteSuccessMessage={() => ({
children: <>Successfully deleted mail recipient(s).</>,
})}
getEditLoading={(previous) => previous || loadingAlertOverrides}
listEmpty="No mail recipient(s) found."
onItemClick={(base, ...args) => {
const [, mailRecipientUuid] = args;
base(...args);
getAlertOverrides(undefined, {
params: { 'mail-recipient': mailRecipientUuid },
});
}}
renderAddForm={(tools) =>
alertOverrideTargetOptions && (
<AddMailRecipientForm
alertOverrideTargetOptions={alertOverrideTargetOptions}
tools={tools}
/>
)
}
renderDeleteItem={(mailRecipientList, { key }) => {
const mr = mailRecipientList?.[key];
return <BodyText>{mr?.name}</BodyText>;
}}
renderEditForm={(tools, mailRecipient) =>
alertOverrideTargetOptions &&
mailRecipient &&
formikAlertOverrides && (
<EditMailRecipientForm
alertOverrideTargetOptions={alertOverrideTargetOptions}
mailRecipientUuid={mailRecipient.uuid}
previousFormikValues={{
[mailRecipient.uuid]: {
alertOverrides: formikAlertOverrides,
...mailRecipient,
},
}}
tools={tools}
/>
)
}
renderListItem={(uuid, { name }) => <BodyText>{name}</BodyText>}
/>
</>
);
};
export default ManageMailRecipient;

@ -0,0 +1,4 @@
import AddMailRecipientForm from './AddMailRecipientForm';
import ManageMailRecipient from './ManageMailRecipient';
export { AddMailRecipientForm, ManageMailRecipient };

@ -0,0 +1,34 @@
import * as yup from 'yup';
import buildYupDynamicObject from '../../lib/buildYupDynamicObject';
const alertLevelSchema = yup.number().oneOf([0, 1, 2, 3, 4]);
const alertOverrideSchema = yup.object({
delete: yup.boolean().optional(),
level: alertLevelSchema.required(),
target: yup.object({
type: yup.string().oneOf(['node', 'subnode']).required(),
uuid: yup.string().uuid().required(),
}),
uuid: yup.string().uuid().optional(),
});
const alertOverrideListSchema = yup.lazy((entries) =>
yup.object(buildYupDynamicObject(entries, alertOverrideSchema)),
);
const mailRecipientSchema = yup.object({
alertOverrides: alertOverrideListSchema,
email: yup.string().email().required(),
language: yup.string().oneOf(['en_CA']).optional(),
level: alertLevelSchema.required(),
name: yup.string().required(),
uuid: yup.string().uuid().optional(),
});
const mailRecipientListSchema = yup.lazy((entries) =>
yup.object(buildYupDynamicObject(entries, mailRecipientSchema)),
);
export default mailRecipientListSchema;

@ -0,0 +1,277 @@
import { Grid } from '@mui/material';
import { FC, ReactNode, useMemo } from 'react';
import { v4 as uuidv4 } from 'uuid';
import ActionGroup from '../ActionGroup';
import api from '../../lib/api';
import FormSummary from '../FormSummary';
import handleAPIError from '../../lib/handleAPIError';
import MessageGroup from '../MessageGroup';
import OutlinedInputWithLabel from '../OutlinedInputWithLabel';
import mailServerListSchema from './schema';
import SelectWithLabel from '../SelectWithLabel';
import useFormikUtils from '../../hooks/useFormikUtils';
import UncontrolledInput from '../UncontrolledInput';
const AddMailServerForm: FC<AddMailServerFormProps> = (props) => {
const {
localhostDomain = '',
mailServerUuid,
previousFormikValues,
tools,
} = props;
const msUuid = useMemo<string>(
() => mailServerUuid ?? uuidv4(),
[mailServerUuid],
);
const {
disableAutocomplete,
disabledSubmit,
formik,
formikErrors,
handleChange,
} = useFormikUtils<MailServerFormikValues>({
initialValues: previousFormikValues ?? {
[msUuid]: {
address: '',
authentication: 'none',
heloDomain: localhostDomain,
port: 587,
security: 'none',
uuid: msUuid,
},
},
onSubmit: (values, { setSubmitting }) => {
const { [msUuid]: mailServer } = values;
let actionProceedText: string = 'Add';
let errorMessage: ReactNode = <>Failed to add mail server.</>;
let method: 'post' | 'put' = 'post';
let successMessage = <>Mail server added.</>;
let titleText: string = 'Add mail server with the following?';
let url = '/mail-server';
if (previousFormikValues) {
actionProceedText = 'Update';
errorMessage = <>Failed to update mail server.</>;
method = 'put';
successMessage = <>Mail server updated.</>;
titleText = `Update ${mailServer.address}:${mailServer.port} with the following?`;
url += `/${msUuid}`;
}
const { confirmPassword, uuid, ...rest } = mailServer;
tools.confirm.prepare({
actionProceedText,
content: <FormSummary entries={rest} />,
onCancelAppend: () => setSubmitting(false),
onProceedAppend: () => {
tools.confirm.loading(true);
api[method](url, mailServer)
.then(() => {
tools.confirm.finish('Success', { children: successMessage });
tools[method === 'post' ? 'add' : 'edit'].open(false);
})
.catch((error) => {
const emsg = handleAPIError(error);
emsg.children = (
<>
{errorMessage} {emsg.children}
</>
);
tools.confirm.finish('Error', emsg);
setSubmitting(false);
});
},
titleText,
});
tools.confirm.open(true);
},
validationSchema: mailServerListSchema,
});
const addressChain = useMemo<string>(() => `${msUuid}.address`, [msUuid]);
const authenticationChain = useMemo<string>(
() => `${msUuid}.authentication`,
[msUuid],
);
const confirmPasswordChain = useMemo<string>(
() => `${msUuid}.confirmPassword`,
[msUuid],
);
const heloDomainChain = useMemo<string>(
() => `${msUuid}.heloDomain`,
[msUuid],
);
const passwordChain = useMemo<string>(() => `${msUuid}.password`, [msUuid]);
const portChain = useMemo<string>(() => `${msUuid}.port`, [msUuid]);
const securityChain = useMemo<string>(() => `${msUuid}.security`, [msUuid]);
const usernameChain = useMemo<string>(() => `${msUuid}.username`, [msUuid]);
return (
<Grid
component="form"
onSubmit={(event) => {
event.preventDefault();
formik.submitForm();
}}
container
columns={{ xs: 1, sm: 2 }}
spacing="1em"
>
<Grid item xs={1}>
<UncontrolledInput
input={
<OutlinedInputWithLabel
id={addressChain}
label="Server address"
name={addressChain}
onBlur={formik.handleBlur}
onChange={handleChange}
required
value={formik.values[msUuid].address}
/>
}
/>
</Grid>
<Grid item xs={1}>
<UncontrolledInput
input={
<OutlinedInputWithLabel
id={portChain}
label="Server port"
name={portChain}
onBlur={formik.handleBlur}
onChange={handleChange}
required
type="number"
value={formik.values[msUuid].port}
/>
}
/>
</Grid>
<Grid item sm={2} xs={1}>
<UncontrolledInput
input={
<SelectWithLabel
id={securityChain}
label="Server security type"
name={securityChain}
onBlur={formik.handleBlur}
onChange={handleChange}
required
selectItems={['none', 'starttls', 'tls-ssl']}
value={formik.values[msUuid].security}
/>
}
/>
</Grid>
<Grid item sm={2} xs={1}>
<UncontrolledInput
input={
<SelectWithLabel
id={authenticationChain}
label="Server authentication method"
name={authenticationChain}
onBlur={formik.handleBlur}
onChange={handleChange}
required
selectItems={['none', 'plain-text', 'encrypted']}
value={formik.values[msUuid].authentication}
/>
}
/>
</Grid>
<Grid item sm={2} xs={1}>
<UncontrolledInput
input={
<OutlinedInputWithLabel
id={heloDomainChain}
label="HELO domain"
name={heloDomainChain}
onBlur={formik.handleBlur}
onChange={handleChange}
required
value={formik.values[msUuid].heloDomain}
/>
}
/>
</Grid>
<Grid item xs={1}>
<UncontrolledInput
input={
<OutlinedInputWithLabel
id={usernameChain}
inputProps={disableAutocomplete()}
label="Server username"
name={usernameChain}
onBlur={formik.handleBlur}
onChange={handleChange}
value={formik.values[msUuid].username}
/>
}
/>
</Grid>
<Grid item xs={1}>
<UncontrolledInput
input={
<OutlinedInputWithLabel
id={passwordChain}
inputProps={disableAutocomplete()}
label="Server password"
name={passwordChain}
onBlur={formik.handleBlur}
onChange={handleChange}
type="password"
value={formik.values[msUuid].password}
/>
}
/>
</Grid>
<Grid item xs={1} />
<Grid item xs={1}>
<UncontrolledInput
input={
<OutlinedInputWithLabel
id={confirmPasswordChain}
inputProps={disableAutocomplete()}
label="Confirm password"
name={confirmPasswordChain}
onBlur={formik.handleBlur}
onChange={handleChange}
type="password"
value={formik.values[msUuid].confirmPassword}
/>
}
/>
</Grid>
<Grid item width="100%">
<MessageGroup count={1} messages={formikErrors} />
</Grid>
<Grid item width="100%">
<ActionGroup
actions={[
{
background: 'blue',
children: previousFormikValues ? 'Update' : 'Add',
disabled: disabledSubmit,
type: 'submit',
},
]}
/>
</Grid>
</Grid>
);
};
export default AddMailServerForm;

@ -0,0 +1,9 @@
import { FC } from 'react';
import AddMailServerForm from './AddMailServerForm';
const EditMailServerForm: FC<EditMailServerFormProps> = (props) => (
<AddMailServerForm {...props} />
);
export default EditMailServerForm;

@ -0,0 +1,64 @@
import { FC } from 'react';
import AddMailServerForm from './AddMailServerForm';
import CrudList from '../CrudList';
import EditMailServerForm from './EditMailServerForm';
import { BodyText } from '../Text';
import useFetch from '../../hooks/useFetch';
const ManageMailServer: FC = () => {
const { data: host, loading: loadingHost } =
useFetch<APIHostDetail>('/host/local');
return (
<>
<CrudList<APIMailServerOverview, APIMailServerDetail>
addHeader="Add mail server"
editHeader={(entry) => `Update ${entry?.address}:${entry?.port}`}
entriesUrl="/mail-server"
getAddLoading={(previous) => previous || loadingHost}
getDeleteErrorMessage={({ children, ...rest }) => ({
...rest,
children: <>Failed to delete mail server(s). {children}</>,
})}
getDeleteHeader={(count) =>
`Delete the following ${count} mail server(s)?`
}
getDeleteSuccessMessage={() => ({
children: <>Successfully deleted mail server(s).</>,
})}
listEmpty="No mail server(s) found"
renderAddForm={(tools) =>
host && (
<AddMailServerForm localhostDomain={host.domain} tools={tools} />
)
}
renderDeleteItem={(mailServers, { key }) => {
const ms = mailServers?.[key];
return (
<BodyText>
{ms?.address}:{ms?.port}
</BodyText>
);
}}
renderEditForm={(tools, mailServer) =>
mailServer && (
<EditMailServerForm
mailServerUuid={mailServer.uuid}
previousFormikValues={{ [mailServer.uuid]: mailServer }}
tools={tools}
/>
)
}
renderListItem={(uuid, { address, port }) => (
<BodyText>
{address}:{port}
</BodyText>
)}
/>
</>
);
};
export default ManageMailServer;

@ -0,0 +1,4 @@
import AddMailServerForm from './AddMailServerForm';
import ManageMailServer from './ManageMailServer';
export { AddMailServerForm, ManageMailServer };

@ -0,0 +1,27 @@
import * as yup from 'yup';
import buildYupDynamicObject from '../../lib/buildYupDynamicObject';
const mailServerSchema = yup.object({
address: yup.string().required(),
authentication: yup.string().oneOf(['none', 'plain-text', 'encrypted']),
confirmPassword: yup
.string()
.when('password', (password, field) =>
String(password).length > 0
? field.required().oneOf([yup.ref('password')])
: field.optional(),
),
heloDomain: yup.string().required(),
password: yup.string().optional(),
port: yup.number().required(),
security: yup.string().oneOf(['none', 'starttls', 'tls-ssl']),
username: yup.string().optional(),
uuid: yup.string().uuid().required(),
});
const mailServerListSchema = yup.lazy((mailServers) =>
yup.object(buildYupDynamicObject(mailServers, mailServerSchema)),
);
export default mailServerListSchema;

@ -12,10 +12,10 @@ import { FC, useMemo } from 'react';
import { GREY } from '../lib/consts/DEFAULT_THEME'; import { GREY } from '../lib/consts/DEFAULT_THEME';
const Select: FC<SelectProps> = ({ const Select = <Value = string,>(
onClearIndicatorClick, ...[props]: Parameters<FC<SelectProps<Value>>>
...muiSelectProps ) => {
}) => { const { onClearIndicatorClick, ...muiSelectProps } = props;
const { sx: selectSx, value, ...restMuiSelectProps } = muiSelectProps; const { sx: selectSx, value, ...restMuiSelectProps } = muiSelectProps;
const combinedSx = useMemo( const combinedSx = useMemo(
@ -58,7 +58,7 @@ const Select: FC<SelectProps> = ({
); );
return ( return (
<MUISelect <MUISelect<Value>
endAdornment={clearIndicatorElement} endAdornment={clearIndicatorElement}
value={value} value={value}
{...restMuiSelectProps} {...restMuiSelectProps}

@ -11,7 +11,13 @@ import OutlinedInput from './OutlinedInput';
import OutlinedInputLabel from './OutlinedInputLabel'; import OutlinedInputLabel from './OutlinedInputLabel';
import Select from './Select'; import Select from './Select';
const SelectWithLabel: FC<SelectWithLabelProps> = ({ const SelectWithLabel = <
Value = string,
Display extends React.ReactNode = React.ReactNode,
>(
...[props]: Parameters<FC<SelectWithLabelProps<Value, Display>>>
): ReturnType<FC<SelectWithLabelProps<Value, Display>>> => {
const {
id, id,
label, label,
selectItems, selectItems,
@ -35,7 +41,8 @@ const SelectWithLabel: FC<SelectWithLabelProps> = ({
value: selectValue, value: selectValue,
// Props with initial value that depend on others. // Props with initial value that depend on others.
isCheckableItems = selectMultiple, isCheckableItems = selectMultiple,
}) => { } = props;
const combinedSx = useMemo( const combinedSx = useMemo(
() => () =>
isReadOnly isReadOnly
@ -96,8 +103,10 @@ const SelectWithLabel: FC<SelectWithLabelProps> = ({
const menuItemElements = useMemo( const menuItemElements = useMemo(
() => () =>
selectItems.map((item) => { selectItems.map((item) => {
const { value, displayValue = value }: SelectItem = const { value, displayValue }: SelectItem<Value, Display> =
typeof item === 'string' ? { value: item } : item; typeof item === 'object'
? item
: { displayValue: item as Display, value: item as Value };
return createMenuItem(value, displayValue); return createMenuItem(value, displayValue);
}), }),
@ -107,7 +116,7 @@ const SelectWithLabel: FC<SelectWithLabelProps> = ({
return ( return (
<MUIFormControl fullWidth {...formControlProps}> <MUIFormControl fullWidth {...formControlProps}>
{labelElement} {labelElement}
<Select <Select<Value>
id={selectId} id={selectId}
input={inputElement} input={inputElement}
multiple={selectMultiple} multiple={selectMultiple}

@ -1,30 +1,35 @@
import { AxiosRequestConfig } from 'axios';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import api from '../lib/api'; import api from '../lib/api';
import handleAPIError from '../lib/handleAPIError'; import handleAPIError from '../lib/handleAPIError';
type ActiveFetchSetter<T> = (data: T) => void; type ActiveFetchSetter<ResData> = (data: ResData) => void;
type ActiveFetcher = (url?: string) => void; type ActiveFetcher<ReqData> = (
url?: string,
config?: AxiosRequestConfig<ReqData>,
) => void;
type ActiveFetchHookResponse = { type ActiveFetchHookResponse<ReqData> = {
fetch: ActiveFetcher; fetch: ActiveFetcher<ReqData>;
loading: boolean; loading: boolean;
}; };
const useActiveFetch = <Data>( const useActiveFetch = <ResData, ReqData = unknown>(
options: { options: {
onData?: ActiveFetchSetter<Data>; config?: AxiosRequestConfig<ReqData>;
onData?: ActiveFetchSetter<ResData>;
onError?: (emsg: Message) => void; onError?: (emsg: Message) => void;
url?: string; url?: string;
} = {}, } = {},
): ActiveFetchHookResponse => { ): ActiveFetchHookResponse<ReqData> => {
const { onError, onData, url: urlPrefix = '' } = options; const { config: baseConfig, onError, onData, url: urlPrefix = '' } = options;
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const fetch = useCallback<ActiveFetcher>( const fetch = useCallback<ActiveFetcher<ReqData>>(
(urlPostfix = '') => { (urlPostfix = '', config) => {
const url = `${urlPrefix}${urlPostfix}`; const url = `${urlPrefix}${urlPostfix}`;
if (!url) return; if (!url) return;
@ -32,7 +37,7 @@ const useActiveFetch = <Data>(
setLoading(true); setLoading(true);
api api
.get<Data>(url) .get<ResData>(url, { ...baseConfig, ...config })
.then(({ data }) => { .then(({ data }) => {
onData?.call(null, data); onData?.call(null, data);
}) })
@ -45,7 +50,7 @@ const useActiveFetch = <Data>(
setLoading(false); setLoading(false);
}); });
}, },
[urlPrefix, setLoading, onError, onData], [urlPrefix, baseConfig, onData, onError],
); );
return { fetch, loading }; return { fetch, loading };

@ -58,7 +58,7 @@ const useChecklist = ({
); );
const getCheck = useCallback<GetCheckFunction>( const getCheck = useCallback<GetCheckFunction>(
(key) => checklist[key], (key) => Boolean(checklist[key]),
[checklist], [checklist],
); );

@ -20,12 +20,18 @@ const useConfirmDialog = (
): { ): {
confirmDialog: ReactElement; confirmDialog: ReactElement;
confirmDialogRef: MutableRefObject<ConfirmDialogForwardedRefContent | null>; confirmDialogRef: MutableRefObject<ConfirmDialogForwardedRefContent | null>;
setConfirmDialogLoading: (value: boolean) => void;
setConfirmDialogOpen: (value: boolean) => void; setConfirmDialogOpen: (value: boolean) => void;
setConfirmDialogProps: Dispatch<SetStateAction<ConfirmDialogProps>>; setConfirmDialogProps: Dispatch<SetStateAction<ConfirmDialogProps>>;
finishConfirm: (title: ReactNode, message: Message) => void; finishConfirm: (title: ReactNode, message: Message) => void;
} => { } => {
const { const {
initial: { actionProceedText = '', content = '', titleText = '' } = {}, initial: {
actionProceedText = '',
content = '',
titleText = '',
...restInitialProps
} = {},
} = args; } = args;
const confirmDialogRef = useRef<ConfirmDialogForwardedRefContent | null>( const confirmDialogRef = useRef<ConfirmDialogForwardedRefContent | null>(
@ -39,6 +45,15 @@ const useConfirmDialog = (
titleText, titleText,
}); });
const setConfirmDialogLoading = useCallback(
(value: boolean) =>
setConfirmDialogProps(({ loading, ...rest }) => ({
...rest,
loading: value,
})),
[],
);
const setConfirmDialogOpen = useCallback( const setConfirmDialogOpen = useCallback(
(value: boolean) => confirmDialogRef?.current?.setOpen?.call(null, value), (value: boolean) => confirmDialogRef?.current?.setOpen?.call(null, value),
[], [],
@ -57,13 +72,20 @@ const useConfirmDialog = (
); );
const confirmDialog = useMemo<ReactElement>( const confirmDialog = useMemo<ReactElement>(
() => <ConfirmDialog {...confirmDialogProps} ref={confirmDialogRef} />, () => (
[confirmDialogProps], <ConfirmDialog
{...restInitialProps}
{...confirmDialogProps}
ref={confirmDialogRef}
/>
),
[confirmDialogProps, restInitialProps],
); );
return { return {
confirmDialog, confirmDialog,
confirmDialogRef, confirmDialogRef,
setConfirmDialogLoading,
setConfirmDialogOpen, setConfirmDialogOpen,
setConfirmDialogProps, setConfirmDialogProps,
finishConfirm, finishConfirm,

@ -1,3 +1,4 @@
import { useMemo } from 'react';
import useSWR, { BareFetcher, SWRConfiguration } from 'swr'; import useSWR, { BareFetcher, SWRConfiguration } from 'swr';
import API_BASE_URL from '../lib/consts/API_BASE_URL'; import API_BASE_URL from '../lib/consts/API_BASE_URL';
@ -10,20 +11,31 @@ type FetchHookResponse<D, E extends Error = Error> = {
loading: boolean; loading: boolean;
}; };
const useFetch = <Data,>( const useFetch = <Data, Alt = Data>(
url: string, url: string,
options: SWRConfiguration<Data> & { options: SWRConfiguration<Data> & {
fetcher?: BareFetcher<Data>;
baseUrl?: string; baseUrl?: string;
fetcher?: BareFetcher<Data>;
mod?: (data: Data) => Alt;
} = {}, } = {},
): FetchHookResponse<Data> => { ): FetchHookResponse<Data> & { altData?: Alt } => {
const { fetcher = fetchJSON, baseUrl = API_BASE_URL, ...config } = options; const {
baseUrl = API_BASE_URL,
fetcher = fetchJSON,
mod,
...config
} = options;
const { data, error } = useSWR<Data>(`${baseUrl}${url}`, fetcher, config); const { data, error } = useSWR<Data>(`${baseUrl}${url}`, fetcher, config);
const altData = useMemo<Alt | undefined>(
() => mod && data && mod(data),
[data, mod],
);
const loading = !error && !data; const loading = !error && !data;
return { data, error, loading }; return { altData, data, error, loading };
}; };
export default useFetch; export default useFetch;

@ -0,0 +1,86 @@
import { OutlinedInputProps } from '@mui/material';
import { FormikConfig, FormikValues, useFormik } from 'formik';
import { isEqual, isObject } from 'lodash';
import { useCallback, useMemo } from 'react';
import debounce from '../lib/debounce';
import getFormikErrorMessages from '../lib/getFormikErrorMessages';
const isChainEqual = (
chain: string[],
current: Tree<unknown>,
initial: Tree<unknown>,
): boolean => {
const [part, ...remain] = chain;
if (!(part in current)) {
return false;
}
const a = current[part];
const b = initial[part];
if (isObject(a) && isObject(b) && remain.length) {
return isChainEqual(remain, a as Tree<unknown>, b as Tree<unknown>);
}
return !isEqual(a, b);
};
const useFormikUtils = <Values extends FormikValues = FormikValues>(
formikConfig: FormikConfig<Values>,
): FormikUtils<Values> => {
const formik = useFormik<Values>({ ...formikConfig });
const getFieldChanged = useCallback(
(field: string) => {
const parts = field.split('.');
return isChainEqual(parts, formik.values, formik.initialValues);
},
[formik.initialValues, formik.values],
);
const disableAutocomplete = useCallback(
(overwrite?: Partial<OutlinedInputProps>): OutlinedInputProps => ({
readOnly: true,
onFocus: (event) => {
event.target.readOnly = false;
},
...overwrite,
}),
[],
);
const debounceHandleChange = useMemo(
() => debounce(formik.handleChange),
[formik.handleChange],
);
const disabledSubmit = useMemo(
() =>
!formik.dirty ||
!formik.isValid ||
formik.isValidating ||
formik.isSubmitting,
[formik.dirty, formik.isSubmitting, formik.isValid, formik.isValidating],
);
const formikErrors = useMemo<Messages>(
() =>
getFormikErrorMessages(formik.errors, {
skip: (field) => !getFieldChanged(field),
}),
[formik.errors, getFieldChanged],
);
return {
disableAutocomplete,
disabledSubmit,
formik,
formikErrors,
handleChange: debounceHandleChange,
};
};
export default useFormikUtils;

@ -14,6 +14,11 @@ export const ICONS = [
image: '/pngs/configure_icon_on.png', image: '/pngs/configure_icon_on.png',
uri: '/config', uri: '/config',
}, },
{
text: 'Mail',
image: '/pngs/email_on.png',
uri: '/mail-config',
},
// { // {
// text: 'Tasks', // text: 'Tasks',
// image: '/pngs/tasks_no-jobs_icon.png', // image: '/pngs/tasks_no-jobs_icon.png',
@ -25,11 +30,6 @@ export const ICONS = [
// uri: '/config', // uri: '/config',
// }, // },
// { // {
// text: 'Email',
// image: '/pngs/email_on.png',
// uri: '/striker?email=true',
// },
// {
// text: 'Logout', // text: 'Logout',
// image: '/pngs/users_icon_on.png', // image: '/pngs/users_icon_on.png',
// uri: '/striker?logout=true', // uri: '/striker?logout=true',

@ -1,22 +1,20 @@
const alphanumeric = '[a-z0-9]'; export const P_ALPHANUM = '[a-z0-9]';
const alphanumericDash = '[a-z0-9-]'; export const P_ALPHANUM_DASH = '[a-z0-9-]';
const hex = '[0-9a-f]'; export const P_HEX = '[0-9a-f]';
const octet = '(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9]|)[0-9])'; export const P_OCTET = '(?:25[0-5]|(?:2[0-4]|1[0-9]|[1-9]|)[0-9])';
const ipv4 = `(?:${octet}[.]){3}${octet}`; export const P_IPV4 = `(?:${P_OCTET}[.]){3}${P_OCTET}`;
export const P_UUID = `${P_HEX}{8}-(?:${P_HEX}{4}-){3}${P_HEX}{12}`;
export const REP_DOMAIN = new RegExp( export const REP_DOMAIN = new RegExp(
`^(?:${alphanumeric}(?:${alphanumericDash}{0,61}${alphanumeric})?[.])+${alphanumeric}${alphanumericDash}{0,61}${alphanumeric}$`, `^(?:${P_ALPHANUM}(?:${P_ALPHANUM_DASH}{0,61}${P_ALPHANUM})?[.])+${P_ALPHANUM}${P_ALPHANUM_DASH}{0,61}${P_ALPHANUM}$`,
); );
export const REP_IPV4 = new RegExp(`^${ipv4}$`); export const REP_IPV4 = new RegExp(`^${P_IPV4}$`);
export const REP_IPV4_CSV = new RegExp(`^(?:${ipv4}\\s*,\\s*)*${ipv4}$`); export const REP_IPV4_CSV = new RegExp(`^(?:${P_IPV4}\\s*,\\s*)*${P_IPV4}$`);
// Peaceful string is temporarily defined as a string without single-quote, double-quote, slash (/), backslash (\\), angle brackets (< >), and curly brackets ({ }). // Peaceful string is temporarily defined as a string without single-quote, double-quote, slash (/), backslash (\\), angle brackets (< >), and curly brackets ({ }).
export const REP_PEACEFUL_STRING = /^[^'"/\\><}{]*$/; export const REP_PEACEFUL_STRING = /^[^'"/\\><}{]*$/;
export const REP_UUID = new RegExp( export const REP_UUID = new RegExp(`^${P_UUID}$`, 'i');
`^${hex}{8}-(?:${hex}{4}-){3}${hex}{12}$`,
'i',
);

@ -1,26 +0,0 @@
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,18 @@
import { debounce as baseDebounce } from 'lodash';
/* eslint-disable @typescript-eslint/no-explicit-any */
type BaseDebounce<Fn extends (...args: any) => any> = typeof baseDebounce<Fn>;
const debounce = <Fn extends (...args: any) => any>(
fn: Fn,
options: {
wait?: Parameters<BaseDebounce<Fn>>[1];
} & Parameters<BaseDebounce<Fn>>[2] = {},
): ReturnType<BaseDebounce<Fn>> => {
const { wait = 500, ...rest } = options;
return baseDebounce<Fn>(fn, wait, rest);
};
export default debounce;

@ -0,0 +1,40 @@
import { capitalize } from 'lodash';
const getFormikErrorMessages = (
errors: object,
{
build = (field, error) => {
let children = error;
if (typeof children === 'string') {
children = capitalize(children.replace(/^[^\s]+\.([^.]+)/, '$1'));
}
return { children, type: 'warning' };
},
chain = '',
skip,
}: {
build?: (field: string, error: unknown) => Message;
chain?: string;
skip?: (field: string) => boolean;
} = {},
): Messages =>
Object.entries(errors).reduce<Messages>((previous, [key, value]) => {
const field = [chain, key].filter((part) => Boolean(part)).join('.');
if (value !== null && typeof value === 'object') {
return {
...previous,
...getFormikErrorMessages(value, { build, chain: field, skip }),
};
}
if (!skip?.call(null, field)) {
previous[field] = build(field, value);
}
return previous;
}, {});
export default getFormikErrorMessages;

@ -1,5 +1,12 @@
module.exports = { /**
* @type {import('next').NextConfig}
*/
const config = {
distDir: 'out',
output: 'export',
pageExtensions: ['ts', 'tsx'], pageExtensions: ['ts', 'tsx'],
poweredByHeader: false, poweredByHeader: false,
reactStrictMode: true, reactStrictMode: true,
}; };
module.exports = config;

@ -0,0 +1 @@
self.__BUILD_MANIFEST=function(s,c,a,t,e,i,n,f,b,u,k,h,j,d,g,r,l,_,o,m){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":[s,a,t,i,n,f,u,"static/chunks/6-dbef3ba2a090cb05.js",c,e,b,k,g,r,"static/chunks/pages/index-6febd0ab3b8c828c.js"],"/_error":["static/chunks/pages/_error-a9572f84d60f21da.js"],"/anvil":[s,a,t,i,n,f,u,"static/chunks/924-2a2fdb45d3e02493.js",c,e,b,k,g,"static/chunks/pages/anvil-38307a04a51f8094.js"],"/config":[s,a,t,n,h,d,c,e,j,"static/chunks/pages/config-1c39d13147dfe819.js"],"/file-manager":[s,a,t,i,f,h,l,"static/chunks/486-1480d7483e28c6f3.js",c,e,b,_,"static/chunks/pages/file-manager-c8a2ce2c02dc39fc.js"],"/init":[s,a,i,n,f,u,d,o,c,e,b,j,m,"static/chunks/pages/init-210f96453904f447.js"],"/login":[s,a,t,n,c,e,j,"static/chunks/pages/login-5fd1d7a2717b59af.js"],"/mail-config":[s,a,t,i,n,f,u,h,l,c,e,b,k,_,"static/chunks/pages/mail-config-14cdc2dd46514057.js"],"/manage-element":[s,a,t,i,n,f,u,h,d,o,"static/chunks/569-fa9b9ac8a7639d2d.js",c,e,b,k,j,m,"static/chunks/pages/manage-element-766bd9ef38ccbfa4.js"],"/server":[s,t,i,c,r,"static/chunks/pages/server-d81577dd0b817ba2.js"],sortedPages:["/","/_app","/_error","/anvil","/config","/file-manager","/init","/login","/mail-config","/manage-element","/server"]}}("static/chunks/494-413067ecdf8ec8f0.js","static/chunks/775-3f1c58f77437bd5d.js","static/chunks/804-a6d43595270ed0d2.js","static/chunks/416-b31c470a96d10e58.js","static/chunks/675-235890fb4812bd16.js","static/chunks/50-af452066db73e3df.js","static/chunks/263-5784adae0d1d8513.js","static/chunks/213-67c4f0768a44e039.js","static/chunks/633-900b9341a6a3bc53.js","static/chunks/310-4edb13985847ab25.js","static/chunks/733-a945bbb3c5f55f74.js","static/chunks/461-c4e18a515455805e.js","static/chunks/556-dbf62d8622405edc.js","static/chunks/203-ea1ab9b7c3c7694b.js","static/chunks/750-b9b6c5fdabc264a0.js","static/chunks/302-6490e226661e8e00.js","static/chunks/264-1be1a496ee1255c6.js","static/chunks/380-0eff6addb79bd61f.js","static/chunks/197-c291e38a27168218.js","static/chunks/270-56592f453c639f63.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

@ -1 +0,0 @@
self.__BUILD_MANIFEST=function(s,c,a,e,t,n,i,f,u,k,h,j,b,d,r,g,l,_,o,p){return{__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":[s,a,e,i,u,h,r,"static/chunks/936-f64829e0e2013921.js",c,t,n,f,g,l,"static/chunks/pages/index-8766524a2b0384fc.js"],"/_error":["static/chunks/pages/_error-2280fa386d040b66.js"],"/anvil":[s,a,e,i,u,h,"static/chunks/638-13a283c3a7da370b.js",c,t,n,f,g,"static/chunks/pages/anvil-7fb5cba6fcb66e8c.js"],"/config":[k,s,a,e,b,"static/chunks/519-4b7761e884c88eb9.js",c,t,n,f,j,d,_,"static/chunks/pages/config-0c3fc9e77c3ed0ed.js"],"/file-manager":[k,s,a,e,i,u,"static/chunks/176-7308c25ba374961e.js",c,t,n,j,"static/chunks/pages/file-manager-ef725a93a3e227aa.js"],"/init":[k,s,a,i,u,h,b,o,c,t,n,f,p,"static/chunks/pages/init-a4caa81141ec112f.js"],"/login":[k,s,a,e,c,t,f,j,d,"static/chunks/pages/login-452bcef79590e137.js"],"/manage-element":[k,s,a,e,i,u,h,b,o,"static/chunks/195-d5fd184cc249f755.js",c,t,n,f,j,d,p,_,"static/chunks/pages/manage-element-0c2dc758c633b42d.js"],"/server":[s,e,i,r,c,n,l,"static/chunks/pages/server-97d4cafd19cb2e9d.js"],sortedPages:["/","/_app","/_error","/anvil","/config","/file-manager","/init","/login","/manage-element","/server"]}}("static/chunks/498-e1933a5461cd8607.js","static/chunks/668-b264bf73f0c1b5eb.js","static/chunks/910-2a0e86a170f6eb77.js","static/chunks/894-e57948de523bcf96.js","static/chunks/284-03dc30df5d459e72.js","static/chunks/157-0528651bf3cd10a7.js","static/chunks/839-dabd319a60c8df83.js","static/chunks/27-7790e406eb2ea28d.js","static/chunks/213-a0488f84cc98f172.js","static/chunks/29107295-fbcfe2172188e46f.js","static/chunks/209-4e2794319babfeec.js","static/chunks/48-d4400834d0a31c6e.js","static/chunks/644-4eec2b397fdacb0c.js","static/chunks/336-fc22c38ce3bd59c5.js","static/chunks/570-6bad4610969fc14b.js","static/chunks/707-ee38ab2abcd0aa3f.js","static/chunks/170-357f4683929223df.js","static/chunks/560-a9c9ecda0eca25a9.js","static/chunks/404-b8e9ff2043a0d30c.js","static/chunks/86-9d0634bddd7b8dc2.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

@ -1 +0,0 @@
self.__MIDDLEWARE_MANIFEST=[];self.__MIDDLEWARE_MANIFEST_CB&&self.__MIDDLEWARE_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

@ -0,0 +1 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[253],{24253:function(e,n,r){r.r(n);var t=r(85893),c=r(94460),u=r(67294);let rfbConnect=e=>{let{background:n="",clipViewport:r=!1,compressionLevel:t=2,dragViewport:u=!1,focusOnClick:l=!1,onConnect:i,onDisconnect:s,qualityLevel:o=6,resizeSession:a=!0,rfb:f,rfbScreen:d,scaleViewport:v=!0,showDotCursor:p=!1,url:E,viewOnly:b=!1}=e;(null==d?void 0:d.current)&&(null==f||!f.current)&&(d.current.innerHTML="",f.current=new c.Z(d.current,E),f.current.background=n,f.current.clipViewport=r,f.current.compressionLevel=t,f.current.dragViewport=u,f.current.focusOnClick=l,f.current.qualityLevel=o,f.current.resizeSession=a,f.current.scaleViewport=v,f.current.showDotCursor=p,f.current.viewOnly=b,i&&f.current.addEventListener("connect",i),s&&f.current.addEventListener("disconnect",s))},rfbDisconnect=e=>{(null==e?void 0:e.current)&&(e.current.disconnect(),e.current=null)},VncDisplay=e=>{let{onConnect:n,onDisconnect:r,rfb:c,rfbConnectArgs:l,rfbScreen:i,url:s}=e;return(0,u.useEffect)(()=>{if(l){let{url:e=s}=l;if(!e)return;let t={onConnect:n,onDisconnect:r,rfb:c,rfbScreen:i,url:e,...l};rfbConnect(t)}else rfbDisconnect(c)},[s,n,r,c,l,i]),(0,u.useEffect)(()=>()=>{rfbDisconnect(c)},[c]),(0,t.jsx)("div",{style:{width:"100%",height:"75vh"},ref:i,onMouseEnter:()=>{document.activeElement&&document.activeElement instanceof HTMLElement&&document.activeElement.blur(),(null==c?void 0:c.current)&&c.current.focus()}})};VncDisplay.displayName="VncDisplay",n.default=VncDisplay}}]);

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 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[461],{62705:function(t,n,r){var e=r(55639).Symbol;t.exports=e},29932:function(t){t.exports=function(t,n){for(var r=-1,e=null==t?0:t.length,o=Array(e);++r<e;)o[r]=n(t[r],r,t);return o}},44286:function(t){t.exports=function(t){return t.split("")}},44239:function(t,n,r){var e=r(62705),o=r(89607),u=r(2333),i=e?e.toStringTag:void 0;t.exports=function(t){return null==t?void 0===t?"[object Undefined]":"[object Null]":i&&i in Object(t)?o(t):u(t)}},14259:function(t){t.exports=function(t,n,r){var e=-1,o=t.length;n<0&&(n=-n>o?0:o+n),(r=r>o?o:r)<0&&(r+=o),o=n>r?0:r-n>>>0,n>>>=0;for(var u=Array(o);++e<o;)u[e]=t[e+n];return u}},80531:function(t,n,r){var e=r(62705),o=r(29932),u=r(1469),i=r(33448),f=1/0,c=e?e.prototype:void 0,a=c?c.toString:void 0;t.exports=function baseToString(t){if("string"==typeof t)return t;if(u(t))return o(t,baseToString)+"";if(i(t))return a?a.call(t):"";var n=t+"";return"0"==n&&1/t==-f?"-0":n}},27561:function(t,n,r){var e=r(67990),o=/^\s+/;t.exports=function(t){return t?t.slice(0,e(t)+1).replace(o,""):t}},40180:function(t,n,r){var e=r(14259);t.exports=function(t,n,r){var o=t.length;return r=void 0===r?o:r,!n&&r>=o?t:e(t,n,r)}},98805:function(t,n,r){var e=r(40180),o=r(62689),u=r(83140),i=r(79833);t.exports=function(t){return function(n){var r=o(n=i(n))?u(n):void 0,f=r?r[0]:n.charAt(0),c=r?e(r,1).join(""):n.slice(1);return f[t]()+c}}},31957:function(t,n,r){var e="object"==typeof r.g&&r.g&&r.g.Object===Object&&r.g;t.exports=e},89607:function(t,n,r){var e=r(62705),o=Object.prototype,u=o.hasOwnProperty,i=o.toString,f=e?e.toStringTag:void 0;t.exports=function(t){var n=u.call(t,f),r=t[f];try{t[f]=void 0;var e=!0}catch(t){}var o=i.call(t);return e&&(n?t[f]=r:delete t[f]),o}},62689:function(t){var n=RegExp("[\\u200d\ud800-\udfff\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\ufe0e\\ufe0f]");t.exports=function(t){return n.test(t)}},2333:function(t){var n=Object.prototype.toString;t.exports=function(t){return n.call(t)}},55639:function(t,n,r){var e=r(31957),o="object"==typeof self&&self&&self.Object===Object&&self,u=e||o||Function("return this")();t.exports=u},83140:function(t,n,r){var e=r(44286),o=r(62689),u=r(676);t.exports=function(t){return o(t)?u(t):e(t)}},67990:function(t){var n=/\s/;t.exports=function(t){for(var r=t.length;r--&&n.test(t.charAt(r)););return r}},676:function(t){var n="\ud800-\udfff",r="[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff]",e="\ud83c[\udffb-\udfff]",o="[^"+n+"]",u="(?:\ud83c[\udde6-\uddff]){2}",i="[\ud800-\udbff][\udc00-\udfff]",f="(?:"+r+"|"+e+")?",c="[\\ufe0e\\ufe0f]?",a="(?:\\u200d(?:"+[o,u,i].join("|")+")"+c+f+")*",v=RegExp(e+"(?="+e+")|(?:"+[o+r+"?",r,u,i,"["+n+"]"].join("|")+")"+(c+f+a),"g");t.exports=function(t){return t.match(v)||[]}},48403:function(t,n,r){var e=r(79833),o=r(11700);t.exports=function(t){return o(e(t).toLowerCase())}},23279:function(t,n,r){var e=r(13218),o=r(7771),u=r(14841),i=Math.max,f=Math.min;t.exports=function(t,n,r){var c,a,v,s,p,d,l=0,x=!1,g=!1,b=!0;if("function"!=typeof t)throw TypeError("Expected a function");function invokeFunc(n){var r=c,e=a;return c=a=void 0,l=n,s=t.apply(e,r)}function shouldInvoke(t){var r=t-d,e=t-l;return void 0===d||r>=n||r<0||g&&e>=v}function timerExpired(){var t,r,e,u=o();if(shouldInvoke(u))return trailingEdge(u);p=setTimeout(timerExpired,(t=u-d,r=u-l,e=n-t,g?f(e,v-r):e))}function trailingEdge(t){return(p=void 0,b&&c)?invokeFunc(t):(c=a=void 0,s)}function debounced(){var t,r=o(),e=shouldInvoke(r);if(c=arguments,a=this,d=r,e){if(void 0===p)return l=t=d,p=setTimeout(timerExpired,n),x?invokeFunc(t):s;if(g)return clearTimeout(p),p=setTimeout(timerExpired,n),invokeFunc(d)}return void 0===p&&(p=setTimeout(timerExpired,n)),s}return n=u(n)||0,e(r)&&(x=!!r.leading,v=(g="maxWait"in r)?i(u(r.maxWait)||0,n):v,b="trailing"in r?!!r.trailing:b),debounced.cancel=function(){void 0!==p&&clearTimeout(p),l=0,c=d=a=p=void 0},debounced.flush=function(){return void 0===p?s:trailingEdge(o())},debounced}},1469:function(t){var n=Array.isArray;t.exports=n},13218:function(t){t.exports=function(t){var n=typeof t;return null!=t&&("object"==n||"function"==n)}},37005:function(t){t.exports=function(t){return null!=t&&"object"==typeof t}},33448:function(t,n,r){var e=r(44239),o=r(37005);t.exports=function(t){return"symbol"==typeof t||o(t)&&"[object Symbol]"==e(t)}},7771:function(t,n,r){var e=r(55639);t.exports=function(){return e.Date.now()}},14841:function(t,n,r){var e=r(27561),o=r(13218),u=r(33448),i=0/0,f=/^[-+]0x[0-9a-f]+$/i,c=/^0b[01]+$/i,a=/^0o[0-7]+$/i,v=parseInt;t.exports=function(t){if("number"==typeof t)return t;if(u(t))return i;if(o(t)){var n="function"==typeof t.valueOf?t.valueOf():t;t=o(n)?n+"":n}if("string"!=typeof t)return 0===t?t:+t;t=e(t);var r=c.test(t);return r||a.test(t)?v(t.slice(2),r?2:8):f.test(t)?i:+t}},79833:function(t,n,r){var e=r(80531);t.exports=function(t){return null==t?"":e(t)}},11700:function(t,n,r){var e=r(98805)("toUpperCase");t.exports=e}}]);

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

@ -1 +0,0 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[665],{4665:function(e,n,r){r.r(n);var t=r(5893),o=r(4460),c=r(7294);function i(e,n,r){return n in e?Object.defineProperty(e,n,{value:r,enumerable:!0,configurable:!0,writable:!0}):e[n]=r,e}var u=function(e){(null===e||void 0===e?void 0:e.current)&&(e.current.disconnect(),e.current=null)},l=function(e){var n=e.onConnect,r=e.onDisconnect,l=e.rfb,s=e.rfbConnectArgs,f=e.rfbScreen,v=e.url;(0,c.useEffect)((function(){if(s){var e=s.url,t=void 0===e?v:e;if(!t)return;var c=function(e){for(var n=1;n<arguments.length;n++){var r=null!=arguments[n]?arguments[n]:{},t=Object.keys(r);"function"===typeof Object.getOwnPropertySymbols&&(t=t.concat(Object.getOwnPropertySymbols(r).filter((function(e){return Object.getOwnPropertyDescriptor(r,e).enumerable})))),t.forEach((function(n){i(e,n,r[n])}))}return e}({onConnect:n,onDisconnect:r,rfb:l,rfbScreen:f,url:t},s);!function(e){var n=e.background,r=void 0===n?"":n,t=e.clipViewport,c=void 0!==t&&t,i=e.compressionLevel,u=void 0===i?2:i,l=e.dragViewport,s=void 0!==l&&l,f=e.focusOnClick,v=void 0!==f&&f,a=e.onConnect,d=e.onDisconnect,b=e.qualityLevel,p=void 0===b?6:b,y=e.resizeSession,w=void 0===y||y,m=e.rfb,h=e.rfbScreen,E=e.scaleViewport,O=void 0===E||E,g=e.showDotCursor,C=void 0!==g&&g,S=e.url,k=e.viewOnly,L=void 0!==k&&k;(null===h||void 0===h?void 0:h.current)&&!(null===m||void 0===m?void 0:m.current)&&(h.current.innerHTML="",m.current=new o.Z(h.current,S),m.current.background=r,m.current.clipViewport=c,m.current.compressionLevel=u,m.current.dragViewport=s,m.current.focusOnClick=v,m.current.qualityLevel=p,m.current.resizeSession=w,m.current.scaleViewport=O,m.current.showDotCursor=C,m.current.viewOnly=L,a&&m.current.addEventListener("connect",a),d&&m.current.addEventListener("disconnect",d))}(c)}else u(l)}),[v,n,r,l,s,f]),(0,c.useEffect)((function(){return function(){u(l)}}),[l]);return(0,t.jsx)("div",{style:{width:"100%",height:"75vh"},ref:f,onMouseEnter:function(){var e,n;document.activeElement&&(e=document.activeElement,null!=(n=HTMLElement)&&"undefined"!==typeof Symbol&&n[Symbol.hasInstance]?n[Symbol.hasInstance](e):e instanceof n)&&document.activeElement.blur(),(null===l||void 0===l?void 0:l.current)&&l.current.focus()}})};l.displayName="VncDisplay",n.default=l}}]);

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

@ -1 +0,0 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[820],{4977:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_error",function(){return u(9185)}])}},function(n){n.O(0,[774,888,179],(function(){return _=4977,n(n.s=_);var _}));var _=n.O();_N_E=_}]);

@ -0,0 +1 @@
(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[820],{81981:function(n,_,u){(window.__NEXT_P=window.__NEXT_P||[]).push(["/_error",function(){return u(55480)}])}},function(n){n.O(0,[774,888,179],function(){return n(n.s=81981)}),_N_E=n.O()}]);

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save