diff --git a/striker-ui/components/ManageMailRecipient/AddMailRecipientForm.tsx b/striker-ui/components/ManageMailRecipient/AddMailRecipientForm.tsx index 063031e4..f3007c15 100644 --- a/striker-ui/components/ManageMailRecipient/AddMailRecipientForm.tsx +++ b/striker-ui/components/ManageMailRecipient/AddMailRecipientForm.tsx @@ -111,6 +111,98 @@ const MAP_TO_LEVEL_LABEL: Record = { 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( + (previous, value) => { + if (value.delete && value.uuids) { + /** + * 1 or more existing records marked for removal. + */ + previous.push( + ...Object.keys(value.uuids).map((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( + (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((hostUuid) => ({ + body: { hostUuid, level, mailRecipientUuid }, + method: 'post', + url: urlPrefix, + })), + ); + + return previous; + }, + [], + ); +}; + const AddMailRecipientForm: FC = (props) => { const { alertOverrideTargetOptions, @@ -132,7 +224,6 @@ const AddMailRecipientForm: FC = (props) => { language: 'en_CA', level: 2, name: '', - uuid: mrUuid, }, }, onSubmit: (values, { setSubmitting }) => { @@ -146,6 +237,8 @@ const AddMailRecipientForm: FC = (props) => { 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.; @@ -153,18 +246,49 @@ const AddMailRecipientForm: FC = (props) => { 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, ...mrBody } = mailRecipient; + const { alertOverrides, uuid: ignore, ...mrBody } = mailRecipient; confirm.prepare({ actionProceedText, - content: , + content: ( + <> + + + >((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); - 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 })) .catch((error) => { const emsg = handleAPIError(error); diff --git a/striker-ui/components/ManageMailRecipient/AlertOverrideInputGroup.tsx b/striker-ui/components/ManageMailRecipient/AlertOverrideInputGroup.tsx index 5bbcf331..d4c2a99e 100644 --- a/striker-ui/components/ManageMailRecipient/AlertOverrideInputGroup.tsx +++ b/striker-ui/components/ManageMailRecipient/AlertOverrideInputGroup.tsx @@ -10,6 +10,7 @@ import { BodyText, SmallText } from '../Text'; import UncontrolledInput from '../UncontrolledInput'; const LEVEL_OPTIONS: SelectItem[] = [ + { displayValue: 'Ignore', value: 0 }, { displayValue: 'Critical', value: 1 }, { displayValue: 'Warning', value: 2 }, { displayValue: 'Notice', value: 3 }, @@ -19,14 +20,18 @@ const LEVEL_OPTIONS: SelectItem[] = [ const AlertOverrideInputGroup: FC = (props) => { const { alertOverrideTargetOptions, - alertOverrideUuid, + alertOverrideValueId, mailRecipientUuid: mrUuid, formikUtils, } = props; - const aoUuid = useMemo( - () => alertOverrideUuid ?? uuidv4(), - [alertOverrideUuid], + /** + * This is the alert override formik value identifier; not to be confused + * with the alert override UUID. + */ + const aoVid = useMemo( + () => alertOverrideValueId ?? uuidv4(), + [alertOverrideValueId], ); const { formik } = formikUtils; @@ -34,12 +39,17 @@ const AlertOverrideInputGroup: FC = (props) => { values: { [mrUuid]: mailRecipient }, } = formik; const { - alertOverrides: { [aoUuid]: alertOverride }, + alertOverrides: { [aoVid]: alertOverride }, } = mailRecipient; const overrideChain = useMemo( - () => `${mrUuid}.alertOverrides.${aoUuid}`, - [aoUuid, mrUuid], + () => `${mrUuid}.alertOverrides.${aoVid}`, + [aoVid, mrUuid], + ); + + const deleteChain = useMemo( + () => `${overrideChain}.delete`, + [overrideChain], ); const targetChain = useMemo( () => `${overrideChain}.target`, @@ -105,7 +115,19 @@ const AlertOverrideInputGroup: FC = (props) => { { - 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" /> diff --git a/striker-ui/components/ManageMailRecipient/ManageAlertOverride.tsx b/striker-ui/components/ManageMailRecipient/ManageAlertOverride.tsx index aea0583f..cfe2ff92 100644 --- a/striker-ui/components/ManageMailRecipient/ManageAlertOverride.tsx +++ b/striker-ui/components/ManageMailRecipient/ManageAlertOverride.tsx @@ -29,28 +29,34 @@ const ManageAlertOverride: FC = (props) => { listEmpty="No alert overrides(s)" listItems={alertOverrides} 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) => { - const current = { ...previous }; + const shallow = { ...previous }; + const { [mrUuid]: mr } = shallow; - current[mrUuid].alertOverrides[aoUuid] = { - level: 2, - target: null, - uuid: aoUuid, + mr.alertOverrides = { + ...mr.alertOverrides, + [valueId]: { level: 2, target: null }, }; - return current; + return shallow; }); }} - renderListItem={(uuid) => ( - - )} + renderListItem={(valueId, value) => + !value.delete && ( + + ) + } /> ); }; diff --git a/striker-ui/components/ManageMailRecipient/ManageMailRecipient.tsx b/striker-ui/components/ManageMailRecipient/ManageMailRecipient.tsx index 01d7a185..45fdebb6 100644 --- a/striker-ui/components/ManageMailRecipient/ManageMailRecipient.tsx +++ b/striker-ui/components/ManageMailRecipient/ManageMailRecipient.tsx @@ -24,26 +24,34 @@ const ManageMailRecipient: FC = () => { Object.values(nodes) .sort((a, b) => a.name.localeCompare(b.name)) .reduce((options, node) => { - options.push({ + const nodeTarget: AlertOverrideTarget = { description: node.description, name: node.name, node: node.uuid, + subnodes: [], type: 'node', uuid: node.uuid, - }); + }; - Object.values(node.hosts) + const subnodeTargets = Object.values(node.hosts) .sort((a, b) => a.name.localeCompare(b.name)) - .forEach((subnode) => { - if (subnode.type === 'dr') return; + .reduce((previous, subnode) => { + if (subnode.type === 'dr') return previous; - options.push({ + 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; }, []), @@ -59,40 +67,87 @@ const ManageMailRecipient: FC = () => { const formikAlertOverrides = useMemo< AlertOverrideFormikValues | undefined >(() => { - if (!alertOverrides) return undefined; - - const groups: Record = {}; - - return Object.values(alertOverrides).reduce( - (previous, value) => { - const { level, node, subnode, uuid } = value; - - groups[node.uuid] = groups[node.uuid] ? groups[node.uuid] + 1 : 1; - - previous[uuid] = { - level, - target: - groups[node.uuid] > 1 - ? { - name: node.name, - node: node.uuid, - type: 'node', - uuid: node.uuid, - } - : { - name: subnode.name, - node: node.uuid, - type: 'subnode', - uuid: subnode.uuid, - }, - uuid, - }; + 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 + >((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( + (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(({ subnode: { uuid } }) => uuid), + type: 'node', + uuid: node.uuid, + }, + uuids: overrides.reduce>( + (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]); + }, [alertOverrides, nodes]); return ( <> diff --git a/striker-ui/components/ManageMailRecipient/schema.ts b/striker-ui/components/ManageMailRecipient/schema.ts index d4d3e3d9..4d833a38 100644 --- a/striker-ui/components/ManageMailRecipient/schema.ts +++ b/striker-ui/components/ManageMailRecipient/schema.ts @@ -2,15 +2,16 @@ import * as yup from 'yup'; 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({ + 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().required(), + uuid: yup.string().uuid().optional(), }); const alertOverrideListSchema = yup.lazy((entries) => @@ -23,7 +24,7 @@ const mailRecipientSchema = yup.object({ language: yup.string().oneOf(['en_CA']).optional(), level: alertLevelSchema.required(), name: yup.string().required(), - uuid: yup.string().uuid().required(), + uuid: yup.string().uuid().optional(), }); const mailRecipientListSchema = yup.lazy((entries) => diff --git a/striker-ui/types/ManageMailRecipient.d.ts b/striker-ui/types/ManageMailRecipient.d.ts index c0196408..382c2306 100644 --- a/striker-ui/types/ManageMailRecipient.d.ts +++ b/striker-ui/types/ManageMailRecipient.d.ts @@ -1,23 +1,36 @@ +type AlertOverrideRequest = { + body?: { + hostUuid: string; + level: number; + mailRecipientUuid: string; + }; + method: 'delete' | 'post' | 'put'; + url: string; +}; + type AlertOverrideTarget = { description?: string; name: string; node: string; + subnodes?: string[]; type: 'node' | 'subnode'; uuid: string; }; type AlertOverrideFormikAlertOverride = { + delete?: boolean; level: number; target: AlertOverrideTarget | null; - uuid: string; + uuids?: Record; }; type AlertOverrideFormikValues = { - [uuid: string]: AlertOverrideFormikAlertOverride; + [valueId: string]: AlertOverrideFormikAlertOverride; }; -type MailRecipientFormikMailRecipient = APIMailRecipientDetail & { +type MailRecipientFormikMailRecipient = Omit & { alertOverrides: AlertOverrideFormikValues; + uuid?: string; }; type MailRecipientFormikValues = { @@ -54,7 +67,7 @@ type ManageAlertOverrideProps = Required< /** AlertOverrideInputGroup */ type AlertOverrideInputGroupOptionalProps = { - alertOverrideUuid?: string; + alertOverrideValueId?: string; }; type AlertOverrideInputGroupProps = AlertOverrideInputGroupOptionalProps &