import { FC, ReactNode, 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_MTU, 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 handleAPIError from '../../lib/handleAPIError'; import IconButton from '../IconButton'; import List from '../List'; 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 useConfirmDialogProps from '../../hooks/useConfirmDialogProps'; import useFormUtils from '../../hooks/useFormUtils'; import useIsFirstRender from '../../hooks/useIsFirstRender'; import useProtectedState from '../../hooks/useProtectedState'; const MSG_ID_MANIFEST_API = 'api'; 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: rawMtu } = elements.namedItem( INPUT_ID_ANC_MTU, ) as HTMLInputElement; const { value: ntpCsv } = elements.namedItem( INPUT_ID_ANC_NTP, ) as HTMLInputElement; const mtu = Number.parseInt(rawMtu, 10); 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, mtu, 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] = useProtectedState< APIHostOverviewList | undefined >(undefined); const [isEditManifests, setIsEditManifests] = useState(false); const [isLoadingHostOverviews, setIsLoadingHostOverviews] = useProtectedState(true); const [isLoadingManifestDetail, setIsLoadingManifestDetail] = useProtectedState(true); const [isLoadingManifestTemplate, setIsLoadingManifestTemplate] = useProtectedState(true); const [isSubmittingForm, setIsSubmittingForm] = useProtectedState(false); const [manifestDetail, setManifestDetail] = useProtectedState< APIManifestDetail | undefined >(undefined); const [manifestTemplate, setManifestTemplate] = useProtectedState< APIManifestTemplate | undefined >(undefined); const { data: manifestOverviews, isLoading: isLoadingManifestOverviews } = periodicFetch(`${API_BASE_URL}/manifest`, { refreshInterval: 60000, }); const formUtils = useFormUtils( [ INPUT_ID_AI_DOMAIN, INPUT_ID_AI_PREFIX, INPUT_ID_AI_SEQUENCE, INPUT_ID_ANC_DNS, INPUT_ID_ANC_MTU, INPUT_ID_ANC_NTP, ], messageGroupRef, ); const { isFormInvalid, setMessage } = formUtils; const runFormUtils = useFormUtils( [ INPUT_ID_RM_AN_CONFIRM_PASSWORD, INPUT_ID_RM_AN_DESCRIPTION, INPUT_ID_RM_AN_PASSWORD, ], messageGroupRef, ); const { isFormInvalid: isRunFormInvalid } = runFormUtils; 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 submitForm = useCallback( ({ body, getErrorMsg, method, successMsg, url, }: { body: Record; getErrorMsg: (parentMsg: ReactNode) => ReactNode; method: 'post' | 'put'; successMsg: ReactNode; url: string; }) => { setIsSubmittingForm(true); api[method](url, body) .then(() => { setMessage(MSG_ID_MANIFEST_API, { children: successMsg, }); }) .catch((apiError) => { const emsg = handleAPIError(apiError); emsg.children = getErrorMsg(emsg.children); setMessage(MSG_ID_MANIFEST_API, emsg); }) .finally(() => { setIsSubmittingForm(false); }); }, [setIsSubmittingForm, setMessage], ); const addManifestFormDialogProps = useMemo( () => ({ actionProceedText: 'Add', content: ( ), onSubmitAppend: (...args) => { const body = getFormData(...args); setConfirmDialogProps({ actionProceedText: 'Add', content: <>, onProceedAppend: () => { submitForm({ body, getErrorMsg: (parentMsg) => ( <>Failed to add install manifest. {parentMsg} ), method: 'post', successMsg: 'Successfully added install manifest', url: '/manifest', }); }, titleText: `Add install manifest?`, }); confirmDialogRef.current.setOpen?.call(null, true); }, titleText: 'Add an install manifest', }), [ formUtils, knownFences, knownUpses, mtemplateDomain, mtemplatePrefix, mtemplateSequence, setConfirmDialogProps, submitForm, ], ); const editManifestFormDialogProps = useMemo( () => ({ actionProceedText: 'Edit', content: ( ), onSubmitAppend: (...args) => { const body = getFormData(...args); setConfirmDialogProps({ actionProceedText: 'Edit', content: <>, onProceedAppend: () => { submitForm({ body, getErrorMsg: (parentMsg) => ( <>Failed to update install manifest. {parentMsg} ), method: 'put', successMsg: `Successfully updated install manifest ${mdetailName}`, url: `/manifest/${mdetailUuid}`, }); }, titleText: `Update install manifest ${mdetailName}?`, }); confirmDialogRef.current.setOpen?.call(null, true); }, loading: isLoadingManifestDetail, titleText: `Update install manifest ${mdetailName}`, }), [ formUtils, knownFences, knownUpses, manifestDetail, isLoadingManifestDetail, mdetailName, setConfirmDialogProps, submitForm, mdetailUuid, ], ); const runManifestFormDialogProps = useMemo( () => ({ actionProceedText: 'Run', content: ( ), loading: isLoadingManifestDetail, onSubmitAppend: (...args) => { const body = getRunFormData(mdetailHosts, ...args); setConfirmDialogProps({ actionProceedText: 'Run', content: <>, onProceedAppend: () => { submitForm({ 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, submitForm, 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); }} onEdit={() => { setIsEditManifests((previous) => !previous); }} onItemClick={({ manifestName, manifestUUID }) => { setManifestDetail({ name: manifestName, uuid: manifestUUID, } as APIManifestDetail); editManifestFormDialogRef.current.setOpen?.call(null, true); getManifestDetail(manifestUUID); }} renderListItem={(manifestUUID, { manifestName }) => ( { setManifestDetail({ name: manifestName, uuid: manifestUUID, } as APIManifestDetail); runManifestFormDialogRef.current.setOpen?.call(null, true); getManifestDetail(manifestUUID); }} variant="normal" /> {manifestName} )} /> ), [getManifestDetail, isEditManifests, manifestOverviews, setManifestDetail], ); 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((apiError) => { handleAPIError(apiError); }) .finally(() => { setIsLoadingHostOverviews(false); }); } return ( <> Manage manifests {panelContent} ); }; export default ManageManifestPanel;