Merge pull request #573 from ylei-tsubame/issues/419-add-mail-config
Web UI: add mail config on UI-sidemain
commit
3d38e10a57
143 changed files with 2442 additions and 418 deletions
File diff suppressed because one or more lines are too long
@ -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; |
@ -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; |
@ -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; |
@ -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…
Reference in new issue