import { FC, FormEventHandler, ReactNode, useCallback, useMemo, useRef, useState, } from 'react'; import API_BASE_URL from '../../lib/consts/API_BASE_URL'; import AddFenceInputGroup, { INPUT_ID_FENCE_AGENT } from './AddFenceInputGroup'; import api from '../../lib/api'; import { INPUT_ID_SEPARATOR } from './CommonFenceInputGroup'; import ConfirmDialog from '../ConfirmDialog'; import EditFenceInputGroup from './EditFenceInputGroup'; import FlexBox from '../FlexBox'; import FormDialog from '../FormDialog'; import FormSummary from '../FormSummary'; import handleAPIError from '../../lib/handleAPIError'; import List from '../List'; import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup'; import { Panel, PanelHeader } from '../Panels'; import periodicFetch from '../../lib/fetchers/periodicFetch'; import Spinner from '../Spinner'; import { BodyText, HeaderText, InlineMonoText, SensitiveText } from '../Text'; import useChecklist from '../../hooks/useChecklist'; import useConfirmDialogProps from '../../hooks/useConfirmDialogProps'; import useFormUtils from '../../hooks/useFormUtils'; import useIsFirstRender from '../../hooks/useIsFirstRender'; import useProtectedState from '../../hooks/useProtectedState'; type FenceFormData = { agent: string; name: string; parameters: { [parameterId: string]: string }; }; const assertFormInputId = (element: Element) => { const { id } = element; const re = new RegExp(`^(fence[^-]+)${INPUT_ID_SEPARATOR}([^\\s]+)$`); const matched = id.match(re); if (!matched) throw Error('Not target input element'); return matched; }; const assertFormInputName = ( paramId: string, parent: FenceFormData, value: string, ) => { if (paramId === 'name') { parent.name = value; throw Error('Not child parameter'); } }; const assertFormParamSpec = ( spec: APIFenceTemplate[string]['parameters'][string], ) => { if (!spec) throw Error('Not parameter specification'); }; const assertFormParamValue = (value: string, paramDefault?: string) => { if ([paramDefault, '', null, undefined].some((bad) => value === bad)) throw Error('Skippable parameter value'); }; const getFormData = ( fenceTemplate: APIFenceTemplate, ...[{ target }]: Parameters> ) => { const { elements } = target as HTMLFormElement; return Object.values(elements).reduce( (previous, element) => { try { const matched = assertFormInputId(element); const [, fenceId, paramId] = matched; previous.agent = fenceId; const inputElement = element as HTMLInputElement; const { checked, value } = inputElement; assertFormInputName(paramId, previous, value); const { [fenceId]: { parameters: { [paramId]: paramSpec }, }, } = fenceTemplate; assertFormParamSpec(paramSpec); const { content_type: paramType, default: paramDefault } = paramSpec; let paramValue = value; if (paramType === 'boolean') { paramValue = checked ? '1' : ''; } assertFormParamValue(paramValue, paramDefault); previous.parameters[paramId] = paramValue; } catch (error) { return previous; } return previous; }, { agent: '', name: '', parameters: {} }, ); }; const ManageFencePanel: FC = () => { const isFirstRender = useIsFirstRender(); const confirmDialogRef = useRef({}); const formDialogRef = useRef({}); const messageGroupRef = useRef({}); const [confirmDialogProps, setConfirmDialogProps] = useConfirmDialogProps(); const [formDialogProps, setFormDialogProps] = useConfirmDialogProps(); const [fenceTemplate, setFenceTemplate] = useProtectedState< APIFenceTemplate | undefined >(undefined); const [isEditFences, setIsEditFences] = useState(false); const [isLoadingFenceTemplate, setIsLoadingFenceTemplate] = useProtectedState(true); const { data: fenceOverviews, isLoading: isFenceOverviewsLoading } = periodicFetch(`${API_BASE_URL}/fence`, { refreshInterval: 60000, }); const formUtils = useFormUtils([INPUT_ID_FENCE_AGENT], messageGroupRef); const { isFormInvalid, isFormSubmitting, submitForm } = formUtils; const { buildDeleteDialogProps, checks, getCheck, hasChecks, setCheck } = useChecklist({ list: fenceOverviews }); const getFormSummaryEntryLabel = useCallback( ({ cap, depth, key }) => (depth === 0 ? cap(key) : key), [], ); const listElement = useMemo( () => ( { setFormDialogProps({ actionProceedText: 'Add', content: ( ), onSubmitAppend: (event) => { if (!fenceTemplate) { return; } const addData = getFormData(fenceTemplate, event); const { agent, name } = addData; setConfirmDialogProps({ actionProceedText: 'Add', content: ( ), onProceedAppend: () => { submitForm({ body: addData, getErrorMsg: (parentMsg) => ( <>Failed to add fence device. {parentMsg} ), method: 'post', successMsg: `Added fence device ${name}`, url: '/fence', }); }, titleText: ( Add a{' '} {agent}{' '} fence device with the following parameters? ), }); confirmDialogRef.current.setOpen?.call(null, true); }, titleText: 'Add a fence device', }); formDialogRef.current.setOpen?.call(null, true); }} onDelete={() => { setConfirmDialogProps( buildDeleteDialogProps({ getConfirmDialogTitle: (count) => `Delete ${count} fence device(s)?`, onProceedAppend: () => { submitForm({ body: { uuids: checks }, getErrorMsg: (parentMsg) => ( <>Failed to delete fence device(s). {parentMsg} ), method: 'delete', url: '/fence', }); }, renderEntry: ({ key }) => ( {fenceOverviews?.[key].fenceName} ), }), ); confirmDialogRef.current.setOpen?.call(null, true); }} onEdit={() => { setIsEditFences((previous) => !previous); }} onItemCheckboxChange={(key, event, checked) => { setCheck(key, checked); }} onItemClick={({ fenceAgent: fenceId, fenceName, fenceParameters, fenceUUID, }) => { setFormDialogProps({ actionProceedText: 'Update', content: ( ), onSubmitAppend: (event) => { if (!fenceTemplate) { return; } const editData = getFormData(fenceTemplate, event); setConfirmDialogProps({ actionProceedText: 'Update', content: ( ), onProceedAppend: () => { submitForm({ body: editData, getErrorMsg: (parentMsg) => ( <>Failed to update fence device. {parentMsg} ), method: 'put', successMsg: `Updated fence device ${fenceName}`, url: `/fence/${fenceUUID}`, }); }, titleText: ( Update{' '} {fenceName} {' '} fence device with the following parameters? ), }); confirmDialogRef.current.setOpen?.call(null, true); }, titleText: ( Update fence device{' '} {fenceName}{' '} parameters ), }); formDialogRef.current.setOpen?.call(null, true); }} renderListItemCheckboxState={(key) => getCheck(key)} renderListItem={( fenceUUID, { fenceAgent, fenceName, fenceParameters }, ) => ( {fenceName} {Object.entries(fenceParameters).reduce( (previous, [parameterId, parameterValue]) => { let current: ReactNode = <>{parameterId}="; current = /passw/i.test(parameterId) ? ( <> {current} {parameterValue} ) : ( <> {current} {parameterValue} ); return ( <> {previous} {current}" ); }, fenceAgent, )} )} /> ), [ buildDeleteDialogProps, checks, fenceOverviews, fenceTemplate, formUtils, getCheck, getFormSummaryEntryLabel, hasChecks, isEditFences, setCheck, setConfirmDialogProps, setFormDialogProps, submitForm, ], ); const panelContent = useMemo( () => isLoadingFenceTemplate || isFenceOverviewsLoading ? ( ) : ( listElement ), [isFenceOverviewsLoading, isLoadingFenceTemplate, listElement], ); const messageArea = useMemo( () => ( ), [], ); if (isFirstRender) { api .get(`/fence/template`) .then(({ data }) => { setFenceTemplate(data); }) .catch((error) => { handleAPIError(error); }) .finally(() => { setIsLoadingFenceTemplate(false); }); } return ( <> Manage fence devices {panelContent} ); }; export default ManageFencePanel;