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.
 
 
 
 
 
 

671 lines
18 KiB

import { FC, useCallback, useMemo, useRef, useState } from 'react';
import API_BASE_URL from '../../lib/consts/API_BASE_URL';
import AddManifestInputGroup from './AddManifestInputGroup';
import {
INPUT_ID_AI_DOMAIN,
INPUT_ID_AI_PREFIX,
INPUT_ID_AI_SEQUENCE,
} from './AnIdInputGroup';
import {
INPUT_ID_PREFIX_AN_HOST,
MAP_TO_AH_INPUT_HANDLER,
} from './AnHostInputGroup';
import {
INPUT_ID_PREFIX_AN_NETWORK,
MAP_TO_AN_INPUT_HANDLER,
} from './AnNetworkInputGroup';
import {
INPUT_ID_ANC_DNS,
INPUT_ID_ANC_NTP,
} from './AnNetworkConfigInputGroup';
import api from '../../lib/api';
import ConfirmDialog from '../ConfirmDialog';
import EditManifestInputGroup from './EditManifestInputGroup';
import FlexBox from '../FlexBox';
import FormDialog from '../FormDialog';
import FormSummary from '../FormSummary';
import handleAPIError from '../../lib/handleAPIError';
import IconButton from '../IconButton';
import List from '../List';
import MessageBox from '../MessageBox';
import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup';
import { Panel, PanelHeader } from '../Panels';
import periodicFetch from '../../lib/fetchers/periodicFetch';
import RunManifestInputGroup, {
buildInputIdRMHost,
INPUT_ID_RM_AN_CONFIRM_PASSWORD,
INPUT_ID_RM_AN_DESCRIPTION,
INPUT_ID_RM_AN_PASSWORD,
} from './RunManifestInputGroup';
import Spinner from '../Spinner';
import { BodyText, HeaderText } from '../Text';
import useChecklist from '../../hooks/useChecklist';
import useConfirmDialogProps from '../../hooks/useConfirmDialogProps';
import useFormUtils from '../../hooks/useFormUtils';
import useIsFirstRender from '../../hooks/useIsFirstRender';
const REQ_BODY_MAX_DEPTH = 6;
const getFormData = (
...[{ target }]: DivFormEventHandlerParameters
): APIBuildManifestRequestBody => {
const { elements } = target as HTMLFormElement;
const { value: domain } = elements.namedItem(
INPUT_ID_AI_DOMAIN,
) as HTMLInputElement;
const { value: prefix } = elements.namedItem(
INPUT_ID_AI_PREFIX,
) as HTMLInputElement;
const { value: rawSequence } = elements.namedItem(
INPUT_ID_AI_SEQUENCE,
) as HTMLInputElement;
const { value: dnsCsv } = elements.namedItem(
INPUT_ID_ANC_DNS,
) as HTMLInputElement;
const { value: ntpCsv } = elements.namedItem(
INPUT_ID_ANC_NTP,
) as HTMLInputElement;
const sequence = Number.parseInt(rawSequence, 10);
return Object.values(elements).reduce<APIBuildManifestRequestBody>(
(previous, element) => {
const { id: inputId } = element;
if (RegExp(`^${INPUT_ID_PREFIX_AN_HOST}`).test(inputId)) {
const input = element as HTMLInputElement;
const {
dataset: { handler: key = '' },
} = input;
MAP_TO_AH_INPUT_HANDLER[key]?.call(null, previous, input);
} else if (RegExp(`^${INPUT_ID_PREFIX_AN_NETWORK}`).test(inputId)) {
const input = element as HTMLInputElement;
const {
dataset: { handler: key = '' },
} = input;
MAP_TO_AN_INPUT_HANDLER[key]?.call(null, previous, input);
}
return previous;
},
{
domain,
hostConfig: { hosts: {} },
networkConfig: {
dnsCsv,
networks: {},
ntpCsv,
},
prefix,
sequence,
},
);
};
const getRunFormData = (
mdetailHosts: ManifestHostList,
...[{ target }]: DivFormEventHandlerParameters
): APIRunManifestRequestBody => {
const { elements } = target as HTMLFormElement;
const { value: description } = elements.namedItem(
INPUT_ID_RM_AN_DESCRIPTION,
) as HTMLInputElement;
const { value: password } = elements.namedItem(
INPUT_ID_RM_AN_PASSWORD,
) as HTMLInputElement;
const hosts = Object.entries(mdetailHosts).reduce<
APIRunManifestRequestBody['hosts']
>((previous, [hostId, { hostNumber, hostType }]) => {
const inputId = buildInputIdRMHost(hostId);
const { value: hostUuid } = elements.namedItem(inputId) as HTMLInputElement;
previous[hostId] = { hostNumber, hostType, hostUuid };
return previous;
}, {});
return { description, hosts, password };
};
const ManageManifestPanel: FC = () => {
const isFirstRender = useIsFirstRender();
const confirmDialogRef = useRef<ConfirmDialogForwardedRefContent>({});
const addManifestFormDialogRef = useRef<ConfirmDialogForwardedRefContent>({});
const editManifestFormDialogRef = useRef<ConfirmDialogForwardedRefContent>(
{},
);
const runManifestFormDialogRef = useRef<ConfirmDialogForwardedRefContent>({});
const messageGroupRef = useRef<MessageGroupForwardedRefContent>({});
const [confirmDialogProps, setConfirmDialogProps] = useConfirmDialogProps();
const [hostOverviews, setHostOverviews] = useState<
APIHostOverviewList | undefined
>();
const [isEditManifests, setIsEditManifests] = useState<boolean>(false);
const [isLoadingHostOverviews, setIsLoadingHostOverviews] =
useState<boolean>(true);
const [isLoadingManifestDetail, setIsLoadingManifestDetail] =
useState<boolean>(true);
const [isLoadingManifestTemplate, setIsLoadingManifestTemplate] =
useState<boolean>(true);
const [manifestOverviews, setManifestOverviews] = useState<
APIManifestOverviewList | undefined
>();
const [manifestDetail, setManifestDetail] = useState<
APIManifestDetail | undefined
>();
const [manifestTemplate, setManifestTemplate] = useState<
APIManifestTemplate | undefined
>();
const { isLoading: isLoadingManifestOverviews } =
periodicFetch<APIManifestOverviewList>(`${API_BASE_URL}/manifest`, {
onSuccess: (data) => setManifestOverviews(data),
refreshInterval: 60000,
});
const getManifestOverviews = useCallback(() => {
api.get('/manifest').then(({ data }) => {
setManifestOverviews(data);
});
}, [setManifestOverviews]);
const formUtils = useFormUtils(
[
INPUT_ID_AI_DOMAIN,
INPUT_ID_AI_PREFIX,
INPUT_ID_AI_SEQUENCE,
INPUT_ID_ANC_DNS,
INPUT_ID_ANC_NTP,
],
messageGroupRef,
);
const { isFormInvalid, isFormSubmitting, submitForm } = formUtils;
const runFormUtils = useFormUtils(
[
INPUT_ID_RM_AN_CONFIRM_PASSWORD,
INPUT_ID_RM_AN_DESCRIPTION,
INPUT_ID_RM_AN_PASSWORD,
],
messageGroupRef,
);
const {
isFormInvalid: isRunFormInvalid,
isFormSubmitting: isRunFormSubmitting,
submitForm: submitRunForm,
} = runFormUtils;
const {
buildDeleteDialogProps,
checks,
getCheck,
hasChecks,
resetChecks,
setCheck,
} = useChecklist({
list: manifestOverviews,
});
const {
hostConfig: { hosts: mdetailHosts = {} } = {},
name: mdetailName,
uuid: mdetailUuid,
} = useMemo<Partial<APIManifestDetail>>(
() => manifestDetail ?? {},
[manifestDetail],
);
const {
domain: mtemplateDomain,
fences: knownFences,
prefix: mtemplatePrefix,
sequence: mtemplateSequence,
upses: knownUpses,
} = useMemo<Partial<APIManifestTemplate>>(
() => manifestTemplate ?? {},
[manifestTemplate],
);
const countHostFences = useCallback(
(
body: APIBuildManifestRequestBody,
): { counts: Record<string, number>; messages: React.ReactNode[] } => {
const {
hostConfig: { hosts },
} = body;
const counts = Object.values(hosts).reduce<Record<string, number>>(
(previous, host) => {
const { fences, hostType, hostNumber } = host;
const hostName = `${hostType.replace(
/node/,
'subnode',
)}${hostNumber}`;
if (!fences) {
previous[hostName] = 0;
return previous;
}
previous[hostName] = Object.values(fences).reduce<number>(
(count, fence) => {
const { fencePort } = fence;
const diff = fencePort.length ? 1 : 0;
return count + diff;
},
0,
);
return previous;
},
{},
);
const messages = Object.entries(counts).map((entry) => {
const [hostName, fenceCount] = entry;
return fenceCount ? (
<></>
) : (
<MessageBox key={`${hostName}-no-fence-port-message`}>
No fence device port specified for {hostName}.
</MessageBox>
);
});
return { counts, messages };
},
[],
);
const addManifestFormDialogProps = useMemo<ConfirmDialogProps>(
() => ({
actionProceedText: 'Add',
content: (
<AddManifestInputGroup
formUtils={formUtils}
knownFences={knownFences}
knownUpses={knownUpses}
previous={{
domain: mtemplateDomain,
prefix: mtemplatePrefix,
sequence: mtemplateSequence,
}}
/>
),
onSubmitAppend: (...args) => {
const body = getFormData(...args);
const { messages } = countHostFences(body);
setConfirmDialogProps({
actionProceedText: 'Add',
content: <FormSummary entries={body} maxDepth={REQ_BODY_MAX_DEPTH} />,
onProceedAppend: () => {
submitForm({
body,
getErrorMsg: (parentMsg) => (
<>Failed to add install manifest. {parentMsg}</>
),
method: 'post',
onSuccess: () => getManifestOverviews(),
successMsg: 'Successfully added install manifest',
url: '/manifest',
});
},
preActionArea: <FlexBox spacing=".3em">{messages}</FlexBox>,
titleText: `Add install manifest?`,
});
confirmDialogRef.current.setOpen?.call(null, true);
},
titleText: 'Add an install manifest',
}),
[
countHostFences,
formUtils,
getManifestOverviews,
knownFences,
knownUpses,
mtemplateDomain,
mtemplatePrefix,
mtemplateSequence,
setConfirmDialogProps,
submitForm,
],
);
const editManifestFormDialogProps = useMemo<ConfirmDialogProps>(
() => ({
actionProceedText: 'Edit',
content: (
<EditManifestInputGroup
formUtils={formUtils}
knownFences={knownFences}
knownUpses={knownUpses}
previous={manifestDetail}
/>
),
onSubmitAppend: (...args) => {
const body = getFormData(...args);
const { messages } = countHostFences(body);
setConfirmDialogProps({
actionProceedText: 'Edit',
content: <FormSummary entries={body} maxDepth={REQ_BODY_MAX_DEPTH} />,
onProceedAppend: () => {
submitForm({
body,
getErrorMsg: (parentMsg) => (
<>Failed to update install manifest. {parentMsg}</>
),
method: 'put',
onSuccess: () => getManifestOverviews(),
successMsg: `Successfully updated install manifest ${mdetailName}`,
url: `/manifest/${mdetailUuid}`,
});
},
preActionArea: <FlexBox spacing=".3em">{messages}</FlexBox>,
titleText: `Update install manifest ${mdetailName}?`,
});
confirmDialogRef.current.setOpen?.call(null, true);
},
loading: isLoadingManifestDetail,
titleText: `Update install manifest ${mdetailName}`,
}),
[
countHostFences,
formUtils,
getManifestOverviews,
isLoadingManifestDetail,
knownFences,
knownUpses,
manifestDetail,
mdetailName,
mdetailUuid,
setConfirmDialogProps,
submitForm,
],
);
const runManifestFormDialogProps = useMemo<ConfirmDialogProps>(
() => ({
actionProceedText: 'Run',
content: (
<RunManifestInputGroup
formUtils={runFormUtils}
knownFences={knownFences}
knownHosts={hostOverviews}
knownUpses={knownUpses}
previous={manifestDetail}
/>
),
loading: isLoadingManifestDetail,
onSubmitAppend: (...args) => {
const body = getRunFormData(mdetailHosts, ...args);
setConfirmDialogProps({
actionProceedText: 'Run',
content: <FormSummary entries={body} hasPassword />,
onProceedAppend: () => {
submitRunForm({
body,
getErrorMsg: (parentMsg) => (
<>Failed to run install manifest. {parentMsg}</>
),
method: 'put',
successMsg: `Successfully ran install manifest ${mdetailName}`,
url: `/command/run-manifest/${mdetailUuid}`,
});
},
titleText: `Run install manifest ${mdetailName}?`,
});
confirmDialogRef.current.setOpen?.call(null, true);
},
titleText: `Run install manifest ${mdetailName}`,
}),
[
runFormUtils,
knownFences,
hostOverviews,
knownUpses,
manifestDetail,
isLoadingManifestDetail,
mdetailName,
mdetailHosts,
setConfirmDialogProps,
submitRunForm,
mdetailUuid,
],
);
const getManifestDetail = useCallback(
(manifestUuid: string, finallyAppend?: () => void) => {
setIsLoadingManifestDetail(true);
api
.get<APIManifestDetail>(`manifest/${manifestUuid}`)
.then(({ data }) => {
data.uuid = manifestUuid;
setManifestDetail(data);
})
.catch((error) => {
handleAPIError(error);
})
.finally(() => {
setIsLoadingManifestDetail(false);
finallyAppend?.call(null);
});
},
[setIsLoadingManifestDetail, setManifestDetail],
);
const listElement = useMemo(
() => (
<List
allowEdit
allowItemButton={isEditManifests}
disableDelete={!hasChecks}
edit={isEditManifests}
header
listEmpty="No manifest(s) registered."
listItems={manifestOverviews}
onAdd={() => {
addManifestFormDialogRef.current.setOpen?.call(null, true);
}}
onDelete={() => {
setConfirmDialogProps(
buildDeleteDialogProps({
onProceedAppend: () => {
submitForm({
body: { uuids: checks },
getErrorMsg: (parentMsg) => (
<>Delete manifest(s) failed. {parentMsg}</>
),
method: 'delete',
onSuccess: () => {
getManifestOverviews();
resetChecks();
},
url: `/manifest`,
});
},
getConfirmDialogTitle: (count) => `Delete ${count} manifest(s)?`,
renderEntry: ({ key }) => (
<BodyText>{manifestOverviews?.[key].manifestName}</BodyText>
),
}),
);
confirmDialogRef.current.setOpen?.call(null, true);
}}
onEdit={() => {
setIsEditManifests((previous) => !previous);
}}
onItemCheckboxChange={(key, event, checked) => {
setCheck(key, checked);
}}
onItemClick={({ manifestName, manifestUUID }) => {
setManifestDetail({
name: manifestName,
uuid: manifestUUID,
} as APIManifestDetail);
editManifestFormDialogRef.current.setOpen?.call(null, true);
getManifestDetail(manifestUUID);
}}
renderListItemCheckboxState={(key) => getCheck(key)}
renderListItem={(manifestUUID, { manifestName }) => (
<FlexBox fullWidth row>
<IconButton
disabled={isEditManifests}
mapPreset="play"
onClick={() => {
setManifestDetail({
name: manifestName,
uuid: manifestUUID,
} as APIManifestDetail);
runManifestFormDialogRef.current.setOpen?.call(null, true);
getManifestDetail(manifestUUID);
}}
variant="normal"
/>
<BodyText>{manifestName}</BodyText>
</FlexBox>
)}
/>
),
[
buildDeleteDialogProps,
checks,
getCheck,
getManifestDetail,
getManifestOverviews,
hasChecks,
isEditManifests,
manifestOverviews,
resetChecks,
setCheck,
setConfirmDialogProps,
setManifestDetail,
submitForm,
],
);
const panelContent = useMemo(
() =>
isLoadingHostOverviews ||
isLoadingManifestTemplate ||
isLoadingManifestOverviews ? (
<Spinner />
) : (
listElement
),
[
isLoadingHostOverviews,
isLoadingManifestOverviews,
isLoadingManifestTemplate,
listElement,
],
);
const messageArea = useMemo(
() => (
<MessageGroup
count={1}
defaultMessageType="warning"
ref={messageGroupRef}
/>
),
[],
);
if (isFirstRender) {
api
.get<APIManifestTemplate>('/manifest/template')
.then(({ data }) => {
setManifestTemplate(data);
})
.catch((error) => {
handleAPIError(error);
})
.finally(() => {
setIsLoadingManifestTemplate(false);
});
api
.get<APIHostOverviewList>('/host', { params: { types: 'node' } })
.then(({ data }) => {
setHostOverviews(data);
})
.catch((error) => {
handleAPIError(error);
})
.finally(() => {
setIsLoadingHostOverviews(false);
});
}
return (
<>
<Panel>
<PanelHeader>
<HeaderText>Manage manifests</HeaderText>
</PanelHeader>
{panelContent}
</Panel>
<FormDialog
{...addManifestFormDialogProps}
disableProceed={isFormInvalid}
loadingAction={isFormSubmitting}
preActionArea={messageArea}
ref={addManifestFormDialogRef}
scrollContent
showClose
/>
<FormDialog
{...editManifestFormDialogProps}
disableProceed={isFormInvalid}
loadingAction={isFormSubmitting}
preActionArea={messageArea}
ref={editManifestFormDialogRef}
scrollContent
showClose
/>
<FormDialog
{...runManifestFormDialogProps}
disableProceed={isRunFormInvalid}
loadingAction={isRunFormSubmitting}
preActionArea={messageArea}
ref={runManifestFormDialogRef}
scrollContent
showClose
/>
<ConfirmDialog
closeOnProceed
{...confirmDialogProps}
ref={confirmDialogRef}
scrollContent
wide
/>
</>
);
};
export default ManageManifestPanel;