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( (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({}); const addManifestFormDialogRef = useRef({}); const editManifestFormDialogRef = useRef( {}, ); const runManifestFormDialogRef = useRef({}); const messageGroupRef = useRef({}); const [confirmDialogProps, setConfirmDialogProps] = useConfirmDialogProps(); const [hostOverviews, setHostOverviews] = useState< APIHostOverviewList | undefined >(); const [isEditManifests, setIsEditManifests] = useState(false); const [isLoadingHostOverviews, setIsLoadingHostOverviews] = useState(true); const [isLoadingManifestDetail, setIsLoadingManifestDetail] = useState(true); const [isLoadingManifestTemplate, setIsLoadingManifestTemplate] = useState(true); const [manifestOverviews, setManifestOverviews] = useState< APIManifestOverviewList | undefined >(); const [manifestDetail, setManifestDetail] = useState< APIManifestDetail | undefined >(); const [manifestTemplate, setManifestTemplate] = useState< APIManifestTemplate | undefined >(); const { isLoading: isLoadingManifestOverviews } = periodicFetch(`${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>( () => manifestDetail ?? {}, [manifestDetail], ); const { domain: mtemplateDomain, fences: knownFences, prefix: mtemplatePrefix, sequence: mtemplateSequence, upses: knownUpses, } = useMemo>( () => manifestTemplate ?? {}, [manifestTemplate], ); const countHostFences = useCallback( ( body: APIBuildManifestRequestBody, ): { counts: Record; messages: React.ReactNode[] } => { const { hostConfig: { hosts }, } = body; const counts = Object.values(hosts).reduce>( (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( (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 ? ( <> ) : ( No fence device port specified for {hostName}. ); }); return { counts, messages }; }, [], ); const addManifestFormDialogProps = useMemo( () => ({ actionProceedText: 'Add', content: ( ), onSubmitAppend: (...args) => { const body = getFormData(...args); const { messages } = countHostFences(body); setConfirmDialogProps({ actionProceedText: 'Add', content: , onProceedAppend: () => { submitForm({ body, getErrorMsg: (parentMsg) => ( <>Failed to add install manifest. {parentMsg} ), method: 'post', onSuccess: () => getManifestOverviews(), successMsg: 'Successfully added install manifest', url: '/manifest', }); }, preActionArea: {messages}, 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( () => ({ actionProceedText: 'Edit', content: ( ), onSubmitAppend: (...args) => { const body = getFormData(...args); const { messages } = countHostFences(body); setConfirmDialogProps({ actionProceedText: 'Edit', content: , 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: {messages}, 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( () => ({ actionProceedText: 'Run', content: ( ), loading: isLoadingManifestDetail, onSubmitAppend: (...args) => { const body = getRunFormData(mdetailHosts, ...args); setConfirmDialogProps({ actionProceedText: 'Run', content: , 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(`manifest/${manifestUuid}`) .then(({ data }) => { data.uuid = manifestUuid; setManifestDetail(data); }) .catch((error) => { handleAPIError(error); }) .finally(() => { setIsLoadingManifestDetail(false); finallyAppend?.call(null); }); }, [setIsLoadingManifestDetail, setManifestDetail], ); const listElement = useMemo( () => ( { 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 }) => ( {manifestOverviews?.[key].manifestName} ), }), ); 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 }) => ( { setManifestDetail({ name: manifestName, uuid: manifestUUID, } as APIManifestDetail); runManifestFormDialogRef.current.setOpen?.call(null, true); getManifestDetail(manifestUUID); }} variant="normal" /> {manifestName} )} /> ), [ buildDeleteDialogProps, checks, getCheck, getManifestDetail, getManifestOverviews, hasChecks, isEditManifests, manifestOverviews, resetChecks, setCheck, setConfirmDialogProps, setManifestDetail, submitForm, ], ); const panelContent = useMemo( () => isLoadingHostOverviews || isLoadingManifestTemplate || isLoadingManifestOverviews ? ( ) : ( listElement ), [ isLoadingHostOverviews, isLoadingManifestOverviews, isLoadingManifestTemplate, listElement, ], ); const messageArea = useMemo( () => ( ), [], ); if (isFirstRender) { api .get('/manifest/template') .then(({ data }) => { setManifestTemplate(data); }) .catch((error) => { handleAPIError(error); }) .finally(() => { setIsLoadingManifestTemplate(false); }); api .get('/host', { params: { types: 'node' } }) .then(({ data }) => { setHostOverviews(data); }) .catch((error) => { handleAPIError(error); }) .finally(() => { setIsLoadingHostOverviews(false); }); } return ( <> Manage manifests {panelContent} ); }; export default ManageManifestPanel;