Local modifications to ClusterLabs/Anvil by Alteeve
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

412 lines
12 KiB

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;