|
|
|
import { Grid, menuClasses as muiMenuClasses } from '@mui/material';
|
|
|
|
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, value) => {
|
|
|
|
if (value.delete && value.uuids) {
|
|
|
|
/**
|
|
|
|
* 1 or more existing records marked for removal.
|
|
|
|
*/
|
|
|
|
previous.push(
|
|
|
|
...Object.keys(value.uuids).map<AlertOverrideRequest>((uuid) => ({
|
|
|
|
method: 'delete',
|
|
|
|
url: `${urlPrefix}/${uuid}`,
|
|
|
|
})),
|
|
|
|
);
|
|
|
|
|
|
|
|
return previous;
|
|
|
|
}
|
|
|
|
|
|
|
|
const { level, target, uuids } = value;
|
|
|
|
|
|
|
|
if (!target) return previous;
|
|
|
|
|
|
|
|
const hosts: string[] = target.subnodes ?? [target.uuid];
|
|
|
|
|
|
|
|
if (uuids) {
|
|
|
|
/**
|
|
|
|
* Found existing alert override UUIDs; the requests must be updates.
|
|
|
|
*/
|
|
|
|
|
|
|
|
const slots: string[] = Object.keys(uuids);
|
|
|
|
|
|
|
|
const longest = Math.max(slots.length, hosts.length);
|
|
|
|
|
|
|
|
previous.push(
|
|
|
|
...Array.from({ length: longest }).map<AlertOverrideRequest>(
|
|
|
|
(ignore, i) => {
|
|
|
|
const host = hosts[i];
|
|
|
|
const slot = slots[i];
|
|
|
|
|
|
|
|
if (!slot) {
|
|
|
|
return {
|
|
|
|
body: { hostUuid: host, level, mailRecipientUuid },
|
|
|
|
method: 'post',
|
|
|
|
url: urlPrefix,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const url = `${urlPrefix}/${slot}`;
|
|
|
|
|
|
|
|
if (!host) {
|
|
|
|
return {
|
|
|
|
method: 'delete',
|
|
|
|
url,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
body: { hostUuid: host, level, mailRecipientUuid },
|
|
|
|
method: 'put',
|
|
|
|
url,
|
|
|
|
};
|
|
|
|
},
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
|
|
|
return previous;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* No existing alert override UUIDs, meaning these are new records.
|
|
|
|
*/
|
|
|
|
previous.push(
|
|
|
|
...hosts.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;
|
|
|
|
const { confirm } = tools;
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
|
|
|
let alertOverrideRequestList: AlertOverrideRequest[];
|
|
|
|
|
|
|
|
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}`;
|
|
|
|
|
|
|
|
alertOverrideRequestList = getAlertOverrideRequestList(
|
|
|
|
mailRecipient,
|
|
|
|
previousFormikValues[mrUuid],
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
alertOverrideRequestList = getAlertOverrideRequestList(mailRecipient);
|
|
|
|
}
|
|
|
|
|
|
|
|
const { alertOverrides, uuid: ignore, ...mrBody } = mailRecipient;
|
|
|
|
|
|
|
|
confirm.prepare({
|
|
|
|
actionProceedText,
|
|
|
|
content: (
|
|
|
|
<>
|
|
|
|
<FormSummary entries={mrBody} />
|
|
|
|
<FormSummary
|
|
|
|
entries={Object.entries(alertOverrides).reduce<
|
|
|
|
Record<string, { level: number; name: string }>
|
|
|
|
>((previous, [valueId, value]) => {
|
|
|
|
if (!value.target) return previous;
|
|
|
|
|
|
|
|
previous[valueId] = {
|
|
|
|
level: value.level,
|
|
|
|
name: value.target.name,
|
|
|
|
};
|
|
|
|
|
|
|
|
return previous;
|
|
|
|
}, {})}
|
|
|
|
/>
|
|
|
|
</>
|
|
|
|
),
|
|
|
|
onCancelAppend: () => setSubmitting(false),
|
|
|
|
onProceedAppend: () => {
|
|
|
|
confirm.loading(true);
|
|
|
|
|
|
|
|
const promises = [api[method](url, mrBody)];
|
|
|
|
|
|
|
|
alertOverrideRequestList.forEach((request) => {
|
|
|
|
promises.push(api[request.method](request.url, request.body));
|
|
|
|
});
|
|
|
|
|
|
|
|
Promise.all(promises)
|
|
|
|
.then(() => confirm.finish('Success', { children: successMessage }))
|
|
|
|
.catch((error) => {
|
|
|
|
const emsg = handleAPIError(error);
|
|
|
|
|
|
|
|
emsg.children = (
|
|
|
|
<>
|
|
|
|
{errorMessage} {emsg.children}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
|
|
|
|
confirm.finish('Error', emsg);
|
|
|
|
})
|
|
|
|
.finally(() => setSubmitting(false));
|
|
|
|
},
|
|
|
|
titleText,
|
|
|
|
});
|
|
|
|
|
|
|
|
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;
|