fix(striker-ui): patch alert override CRUD operations; still has bugs

main
Tsu-ba-me 11 months ago
parent 93416b0913
commit 9a6a4f8b7c
  1. 132
      striker-ui/components/ManageMailRecipient/AddMailRecipientForm.tsx
  2. 38
      striker-ui/components/ManageMailRecipient/AlertOverrideInputGroup.tsx
  3. 26
      striker-ui/components/ManageMailRecipient/ManageAlertOverride.tsx
  4. 97
      striker-ui/components/ManageMailRecipient/ManageMailRecipient.tsx
  5. 7
      striker-ui/components/ManageMailRecipient/schema.ts
  6. 21
      striker-ui/types/ManageMailRecipient.d.ts

@ -111,6 +111,98 @@ const MAP_TO_LEVEL_LABEL: Record<number, string> = {
4: 'Info', 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 AddMailRecipientForm: FC<AddMailRecipientFormProps> = (props) => {
const { const {
alertOverrideTargetOptions, alertOverrideTargetOptions,
@ -132,7 +224,6 @@ const AddMailRecipientForm: FC<AddMailRecipientFormProps> = (props) => {
language: 'en_CA', language: 'en_CA',
level: 2, level: 2,
name: '', name: '',
uuid: mrUuid,
}, },
}, },
onSubmit: (values, { setSubmitting }) => { onSubmit: (values, { setSubmitting }) => {
@ -146,6 +237,8 @@ const AddMailRecipientForm: FC<AddMailRecipientFormProps> = (props) => {
let titleText: string = `Add mail recipient with the following?`; let titleText: string = `Add mail recipient with the following?`;
let url: string = '/mail-recipient'; let url: string = '/mail-recipient';
let alertOverrideRequestList: AlertOverrideRequest[];
if (previousFormikValues) { if (previousFormikValues) {
actionProceedText = 'Update'; actionProceedText = 'Update';
errorMessage = <>Failed to update mail server.</>; errorMessage = <>Failed to update mail server.</>;
@ -153,18 +246,49 @@ const AddMailRecipientForm: FC<AddMailRecipientFormProps> = (props) => {
successMessage = <>Mail recipient updated.</>; successMessage = <>Mail recipient updated.</>;
titleText = `Update ${mailRecipient.name} with the following?`; titleText = `Update ${mailRecipient.name} with the following?`;
url += `/${mrUuid}`; url += `/${mrUuid}`;
alertOverrideRequestList = getAlertOverrideRequestList(
mailRecipient,
previousFormikValues[mrUuid],
);
} else {
alertOverrideRequestList = getAlertOverrideRequestList(mailRecipient);
} }
const { alertOverrides, uuid, ...mrBody } = mailRecipient; const { alertOverrides, uuid: ignore, ...mrBody } = mailRecipient;
confirm.prepare({ confirm.prepare({
actionProceedText, actionProceedText,
content: <FormSummary entries={mrBody} />, 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), onCancelAppend: () => setSubmitting(false),
onProceedAppend: () => { onProceedAppend: () => {
confirm.loading(true); confirm.loading(true);
api[method](url, mrBody) 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 })) .then(() => confirm.finish('Success', { children: successMessage }))
.catch((error) => { .catch((error) => {
const emsg = handleAPIError(error); const emsg = handleAPIError(error);

@ -10,6 +10,7 @@ import { BodyText, SmallText } from '../Text';
import UncontrolledInput from '../UncontrolledInput'; import UncontrolledInput from '../UncontrolledInput';
const LEVEL_OPTIONS: SelectItem<number>[] = [ const LEVEL_OPTIONS: SelectItem<number>[] = [
{ displayValue: 'Ignore', value: 0 },
{ displayValue: 'Critical', value: 1 }, { displayValue: 'Critical', value: 1 },
{ displayValue: 'Warning', value: 2 }, { displayValue: 'Warning', value: 2 },
{ displayValue: 'Notice', value: 3 }, { displayValue: 'Notice', value: 3 },
@ -19,14 +20,18 @@ const LEVEL_OPTIONS: SelectItem<number>[] = [
const AlertOverrideInputGroup: FC<AlertOverrideInputGroupProps> = (props) => { const AlertOverrideInputGroup: FC<AlertOverrideInputGroupProps> = (props) => {
const { const {
alertOverrideTargetOptions, alertOverrideTargetOptions,
alertOverrideUuid, alertOverrideValueId,
mailRecipientUuid: mrUuid, mailRecipientUuid: mrUuid,
formikUtils, formikUtils,
} = props; } = props;
const aoUuid = useMemo<string>( /**
() => alertOverrideUuid ?? uuidv4(), * This is the alert override formik value identifier; not to be confused
[alertOverrideUuid], * with the alert override UUID.
*/
const aoVid = useMemo<string>(
() => alertOverrideValueId ?? uuidv4(),
[alertOverrideValueId],
); );
const { formik } = formikUtils; const { formik } = formikUtils;
@ -34,12 +39,17 @@ const AlertOverrideInputGroup: FC<AlertOverrideInputGroupProps> = (props) => {
values: { [mrUuid]: mailRecipient }, values: { [mrUuid]: mailRecipient },
} = formik; } = formik;
const { const {
alertOverrides: { [aoUuid]: alertOverride }, alertOverrides: { [aoVid]: alertOverride },
} = mailRecipient; } = mailRecipient;
const overrideChain = useMemo<string>( const overrideChain = useMemo<string>(
() => `${mrUuid}.alertOverrides.${aoUuid}`, () => `${mrUuid}.alertOverrides.${aoVid}`,
[aoUuid, mrUuid], [aoVid, mrUuid],
);
const deleteChain = useMemo<string>(
() => `${overrideChain}.delete`,
[overrideChain],
); );
const targetChain = useMemo<string>( const targetChain = useMemo<string>(
() => `${overrideChain}.target`, () => `${overrideChain}.target`,
@ -105,7 +115,19 @@ const AlertOverrideInputGroup: FC<AlertOverrideInputGroupProps> = (props) => {
<IconButton <IconButton
mapPreset="delete" mapPreset="delete"
onClick={() => { onClick={() => {
formik.setFieldValue(overrideChain, undefined, true); if (alertOverride.uuids) {
formik.setFieldValue(deleteChain, true, true);
} else {
formik.setValues((previous) => {
const shallow = { ...previous };
const { [mrUuid]: mr } = shallow;
const { [aoVid]: remove, ...keep } = mr.alertOverrides;
mr.alertOverrides = { ...keep };
return shallow;
});
}
}} }}
size="small" size="small"
/> />

@ -29,28 +29,34 @@ const ManageAlertOverride: FC<ManageAlertOverrideProps> = (props) => {
listEmpty="No alert overrides(s)" listEmpty="No alert overrides(s)"
listItems={alertOverrides} listItems={alertOverrides}
onAdd={() => { onAdd={() => {
const aoUuid = uuidv4(); /**
* 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) => { formik.setValues((previous) => {
const current = { ...previous }; const shallow = { ...previous };
const { [mrUuid]: mr } = shallow;
current[mrUuid].alertOverrides[aoUuid] = { mr.alertOverrides = {
level: 2, ...mr.alertOverrides,
target: null, [valueId]: { level: 2, target: null },
uuid: aoUuid,
}; };
return current; return shallow;
}); });
}} }}
renderListItem={(uuid) => ( renderListItem={(valueId, value) =>
!value.delete && (
<AlertOverrideInputGroup <AlertOverrideInputGroup
alertOverrideTargetOptions={alertOverrideTargetOptions} alertOverrideTargetOptions={alertOverrideTargetOptions}
alertOverrideUuid={uuid} alertOverrideValueId={valueId}
formikUtils={formikUtils} formikUtils={formikUtils}
mailRecipientUuid={mrUuid} mailRecipientUuid={mrUuid}
/> />
)} )
}
/> />
); );
}; };

@ -24,26 +24,34 @@ const ManageMailRecipient: FC = () => {
Object.values(nodes) Object.values(nodes)
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
.reduce<AlertOverrideTarget[]>((options, node) => { .reduce<AlertOverrideTarget[]>((options, node) => {
options.push({ const nodeTarget: AlertOverrideTarget = {
description: node.description, description: node.description,
name: node.name, name: node.name,
node: node.uuid, node: node.uuid,
subnodes: [],
type: 'node', type: 'node',
uuid: node.uuid, uuid: node.uuid,
}); };
Object.values(node.hosts) const subnodeTargets = Object.values(node.hosts)
.sort((a, b) => a.name.localeCompare(b.name)) .sort((a, b) => a.name.localeCompare(b.name))
.forEach((subnode) => { .reduce<AlertOverrideTarget[]>((previous, subnode) => {
if (subnode.type === 'dr') return; if (subnode.type === 'dr') return previous;
options.push({ previous.push({
name: subnode.name, name: subnode.name,
node: node.uuid, node: node.uuid,
type: 'subnode', type: 'subnode',
uuid: subnode.uuid, 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; return options;
}, []), }, []),
@ -59,40 +67,87 @@ const ManageMailRecipient: FC = () => {
const formikAlertOverrides = useMemo< const formikAlertOverrides = useMemo<
AlertOverrideFormikValues | undefined AlertOverrideFormikValues | undefined
>(() => { >(() => {
if (!alertOverrides) return 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 groups: Record<string, number> = {}; const sameLevel =
overrides.length > 1 &&
restOverrides.every(({ level }) => level === firstOverride.level);
return Object.values(alertOverrides).reduce<AlertOverrideFormikValues>( if (sameLevel) {
(previous, value) => { const {
const { level, node, subnode, uuid } = value; 0: { level },
} = overrides;
groups[node.uuid] = groups[node.uuid] ? groups[node.uuid] + 1 : 1; const { [nodeUuid]: node } = nodes;
previous[uuid] = { previous[nodeUuid] = {
level, level,
target: target: {
groups[node.uuid] > 1 description: node.description,
? {
name: node.name, name: node.name,
node: node.uuid, node: node.uuid,
subnodes: overrides.map<string>(({ subnode: { uuid } }) => uuid),
type: 'node', type: 'node',
uuid: node.uuid, 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, name: subnode.name,
node: node.uuid, node: node.uuid,
type: 'subnode', type: 'subnode',
uuid: subnode.uuid, uuid: subnode.uuid,
}, },
uuid, uuids: { [overrideUuid]: subnode.uuid },
}; };
});
}
return previous; return previous;
}, },
{}, {},
); );
}, [alertOverrides]); }, [alertOverrides, nodes]);
return ( return (
<> <>

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

@ -1,23 +1,36 @@
type AlertOverrideRequest = {
body?: {
hostUuid: string;
level: number;
mailRecipientUuid: string;
};
method: 'delete' | 'post' | 'put';
url: string;
};
type AlertOverrideTarget = { type AlertOverrideTarget = {
description?: string; description?: string;
name: string; name: string;
node: string; node: string;
subnodes?: string[];
type: 'node' | 'subnode'; type: 'node' | 'subnode';
uuid: string; uuid: string;
}; };
type AlertOverrideFormikAlertOverride = { type AlertOverrideFormikAlertOverride = {
delete?: boolean;
level: number; level: number;
target: AlertOverrideTarget | null; target: AlertOverrideTarget | null;
uuid: string; uuids?: Record<string, string>;
}; };
type AlertOverrideFormikValues = { type AlertOverrideFormikValues = {
[uuid: string]: AlertOverrideFormikAlertOverride; [valueId: string]: AlertOverrideFormikAlertOverride;
}; };
type MailRecipientFormikMailRecipient = APIMailRecipientDetail & { type MailRecipientFormikMailRecipient = Omit<APIMailRecipientDetail, 'uuid'> & {
alertOverrides: AlertOverrideFormikValues; alertOverrides: AlertOverrideFormikValues;
uuid?: string;
}; };
type MailRecipientFormikValues = { type MailRecipientFormikValues = {
@ -54,7 +67,7 @@ type ManageAlertOverrideProps = Required<
/** AlertOverrideInputGroup */ /** AlertOverrideInputGroup */
type AlertOverrideInputGroupOptionalProps = { type AlertOverrideInputGroupOptionalProps = {
alertOverrideUuid?: string; alertOverrideValueId?: string;
}; };
type AlertOverrideInputGroupProps = AlertOverrideInputGroupOptionalProps & type AlertOverrideInputGroupProps = AlertOverrideInputGroupOptionalProps &

Loading…
Cancel
Save