|
|
|
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<FormEventHandler<HTMLDivElement>>
|
|
|
|
) => {
|
|
|
|
const { elements } = target as HTMLFormElement;
|
|
|
|
|
|
|
|
return Object.values(elements).reduce<FenceFormData>(
|
|
|
|
(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<ConfirmDialogForwardedRefContent>({});
|
|
|
|
const formDialogRef = useRef<ConfirmDialogForwardedRefContent>({});
|
|
|
|
const messageGroupRef = useRef<MessageGroupForwardedRefContent>({});
|
|
|
|
|
|
|
|
const [confirmDialogProps, setConfirmDialogProps] = useConfirmDialogProps();
|
|
|
|
const [formDialogProps, setFormDialogProps] = useConfirmDialogProps();
|
|
|
|
|
|
|
|
const [fenceOverviews, setFenceOverviews] = useProtectedState<
|
|
|
|
APIFenceOverview | undefined
|
|
|
|
>(undefined);
|
|
|
|
const [fenceTemplate, setFenceTemplate] = useProtectedState<
|
|
|
|
APIFenceTemplate | undefined
|
|
|
|
>(undefined);
|
|
|
|
const [isEditFences, setIsEditFences] = useState<boolean>(false);
|
|
|
|
const [isLoadingFenceTemplate, setIsLoadingFenceTemplate] =
|
|
|
|
useProtectedState<boolean>(true);
|
|
|
|
|
|
|
|
const { isLoading: isFenceOverviewsLoading } =
|
|
|
|
periodicFetch<APIFenceOverview>(`${API_BASE_URL}/fence`, {
|
|
|
|
onSuccess: (data) => setFenceOverviews(data),
|
|
|
|
refreshInterval: 60000,
|
|
|
|
});
|
|
|
|
|
|
|
|
const getFenceOverviews = useCallback(() => {
|
|
|
|
api.get('/fence').then(({ data }) => {
|
|
|
|
setFenceOverviews(data);
|
|
|
|
});
|
|
|
|
}, [setFenceOverviews]);
|
|
|
|
|
|
|
|
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<GetFormEntryLabelFunction>(
|
|
|
|
({ cap, depth, key }) => (depth === 0 ? cap(key) : key),
|
|
|
|
[],
|
|
|
|
);
|
|
|
|
|
|
|
|
const listElement = useMemo(
|
|
|
|
() => (
|
|
|
|
<List
|
|
|
|
allowEdit
|
|
|
|
allowItemButton={isEditFences}
|
|
|
|
disableDelete={!hasChecks}
|
|
|
|
edit={isEditFences}
|
|
|
|
header
|
|
|
|
listItems={fenceOverviews}
|
|
|
|
onAdd={() => {
|
|
|
|
setFormDialogProps({
|
|
|
|
actionProceedText: 'Add',
|
|
|
|
content: (
|
|
|
|
<AddFenceInputGroup
|
|
|
|
fenceTemplate={fenceTemplate}
|
|
|
|
formUtils={formUtils}
|
|
|
|
/>
|
|
|
|
),
|
|
|
|
onSubmitAppend: (event) => {
|
|
|
|
if (!fenceTemplate) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const addData = getFormData(fenceTemplate, event);
|
|
|
|
const { agent, name } = addData;
|
|
|
|
|
|
|
|
setConfirmDialogProps({
|
|
|
|
actionProceedText: 'Add',
|
|
|
|
content: (
|
|
|
|
<FormSummary
|
|
|
|
entries={addData}
|
|
|
|
hasPassword
|
|
|
|
getEntryLabel={getFormSummaryEntryLabel}
|
|
|
|
/>
|
|
|
|
),
|
|
|
|
onProceedAppend: () => {
|
|
|
|
submitForm({
|
|
|
|
body: addData,
|
|
|
|
getErrorMsg: (parentMsg) => (
|
|
|
|
<>Failed to add fence device. {parentMsg}</>
|
|
|
|
),
|
|
|
|
method: 'post',
|
|
|
|
onSuccess: () => getFenceOverviews(),
|
|
|
|
successMsg: `Added fence device ${name}`,
|
|
|
|
url: '/fence',
|
|
|
|
});
|
|
|
|
},
|
|
|
|
titleText: (
|
|
|
|
<HeaderText>
|
|
|
|
Add a{' '}
|
|
|
|
<InlineMonoText fontSize="inherit">{agent}</InlineMonoText>{' '}
|
|
|
|
fence device with the following parameters?
|
|
|
|
</HeaderText>
|
|
|
|
),
|
|
|
|
});
|
|
|
|
|
|
|
|
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',
|
|
|
|
onSuccess: () => getFenceOverviews(),
|
|
|
|
url: '/fence',
|
|
|
|
});
|
|
|
|
},
|
|
|
|
renderEntry: ({ key }) => (
|
|
|
|
<BodyText>{fenceOverviews?.[key].fenceName}</BodyText>
|
|
|
|
),
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
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: (
|
|
|
|
<EditFenceInputGroup
|
|
|
|
fenceId={fenceId}
|
|
|
|
fenceTemplate={fenceTemplate}
|
|
|
|
formUtils={formUtils}
|
|
|
|
previousFenceName={fenceName}
|
|
|
|
previousFenceParameters={fenceParameters}
|
|
|
|
/>
|
|
|
|
),
|
|
|
|
onSubmitAppend: (event) => {
|
|
|
|
if (!fenceTemplate) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const editData = getFormData(fenceTemplate, event);
|
|
|
|
|
|
|
|
setConfirmDialogProps({
|
|
|
|
actionProceedText: 'Update',
|
|
|
|
content: (
|
|
|
|
<FormSummary
|
|
|
|
entries={editData}
|
|
|
|
hasPassword
|
|
|
|
getEntryLabel={getFormSummaryEntryLabel}
|
|
|
|
/>
|
|
|
|
),
|
|
|
|
onProceedAppend: () => {
|
|
|
|
submitForm({
|
|
|
|
body: editData,
|
|
|
|
getErrorMsg: (parentMsg) => (
|
|
|
|
<>Failed to update fence device. {parentMsg}</>
|
|
|
|
),
|
|
|
|
method: 'put',
|
|
|
|
onSuccess: () => getFenceOverviews(),
|
|
|
|
successMsg: `Updated fence device ${fenceName}`,
|
|
|
|
url: `/fence/${fenceUUID}`,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
titleText: (
|
|
|
|
<HeaderText>
|
|
|
|
Update{' '}
|
|
|
|
<InlineMonoText fontSize="inherit">
|
|
|
|
{fenceName}
|
|
|
|
</InlineMonoText>{' '}
|
|
|
|
fence device with the following parameters?
|
|
|
|
</HeaderText>
|
|
|
|
),
|
|
|
|
});
|
|
|
|
|
|
|
|
confirmDialogRef.current.setOpen?.call(null, true);
|
|
|
|
},
|
|
|
|
titleText: (
|
|
|
|
<HeaderText>
|
|
|
|
Update fence device{' '}
|
|
|
|
<InlineMonoText fontSize="inherit">{fenceName}</InlineMonoText>{' '}
|
|
|
|
parameters
|
|
|
|
</HeaderText>
|
|
|
|
),
|
|
|
|
});
|
|
|
|
|
|
|
|
formDialogRef.current.setOpen?.call(null, true);
|
|
|
|
}}
|
|
|
|
renderListItemCheckboxState={(key) => getCheck(key)}
|
|
|
|
renderListItem={(
|
|
|
|
fenceUUID,
|
|
|
|
{ fenceAgent, fenceName, fenceParameters },
|
|
|
|
) => (
|
|
|
|
<FlexBox row>
|
|
|
|
<BodyText>{fenceName}</BodyText>
|
|
|
|
<BodyText>
|
|
|
|
{Object.entries(fenceParameters).reduce<ReactNode>(
|
|
|
|
(previous, [parameterId, parameterValue]) => {
|
|
|
|
let current: ReactNode = <>{parameterId}="</>;
|
|
|
|
|
|
|
|
current = /passw/i.test(parameterId) ? (
|
|
|
|
<>
|
|
|
|
{current}
|
|
|
|
<SensitiveText inline>{parameterValue}</SensitiveText>
|
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
{current}
|
|
|
|
{parameterValue}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{previous} {current}"
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
fenceAgent,
|
|
|
|
)}
|
|
|
|
</BodyText>
|
|
|
|
</FlexBox>
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
),
|
|
|
|
[
|
|
|
|
buildDeleteDialogProps,
|
|
|
|
checks,
|
|
|
|
fenceOverviews,
|
|
|
|
fenceTemplate,
|
|
|
|
formUtils,
|
|
|
|
getCheck,
|
|
|
|
getFenceOverviews,
|
|
|
|
getFormSummaryEntryLabel,
|
|
|
|
hasChecks,
|
|
|
|
isEditFences,
|
|
|
|
setCheck,
|
|
|
|
setConfirmDialogProps,
|
|
|
|
setFormDialogProps,
|
|
|
|
submitForm,
|
|
|
|
],
|
|
|
|
);
|
|
|
|
const panelContent = useMemo(
|
|
|
|
() =>
|
|
|
|
isLoadingFenceTemplate || isFenceOverviewsLoading ? (
|
|
|
|
<Spinner />
|
|
|
|
) : (
|
|
|
|
listElement
|
|
|
|
),
|
|
|
|
[isFenceOverviewsLoading, isLoadingFenceTemplate, listElement],
|
|
|
|
);
|
|
|
|
|
|
|
|
const messageArea = useMemo(
|
|
|
|
() => (
|
|
|
|
<MessageGroup
|
|
|
|
count={1}
|
|
|
|
defaultMessageType="warning"
|
|
|
|
ref={messageGroupRef}
|
|
|
|
/>
|
|
|
|
),
|
|
|
|
[],
|
|
|
|
);
|
|
|
|
|
|
|
|
if (isFirstRender) {
|
|
|
|
api
|
|
|
|
.get<APIFenceTemplate>(`/fence/template`)
|
|
|
|
.then(({ data }) => {
|
|
|
|
setFenceTemplate(data);
|
|
|
|
})
|
|
|
|
.catch((error) => {
|
|
|
|
handleAPIError(error);
|
|
|
|
})
|
|
|
|
.finally(() => {
|
|
|
|
setIsLoadingFenceTemplate(false);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<Panel>
|
|
|
|
<PanelHeader>
|
|
|
|
<HeaderText>Manage fence devices</HeaderText>
|
|
|
|
</PanelHeader>
|
|
|
|
{panelContent}
|
|
|
|
</Panel>
|
|
|
|
<FormDialog
|
|
|
|
dialogProps={{
|
|
|
|
PaperProps: { sx: { minWidth: { xs: '90%', md: '50em' } } },
|
|
|
|
}}
|
|
|
|
scrollBoxProps={{
|
|
|
|
padding: '.3em .5em',
|
|
|
|
}}
|
|
|
|
{...formDialogProps}
|
|
|
|
disableProceed={isFormInvalid}
|
|
|
|
loadingAction={isFormSubmitting}
|
|
|
|
preActionArea={messageArea}
|
|
|
|
ref={formDialogRef}
|
|
|
|
scrollContent
|
|
|
|
showClose
|
|
|
|
/>
|
|
|
|
<ConfirmDialog
|
|
|
|
closeOnProceed
|
|
|
|
scrollBoxProps={{ paddingRight: '1em' }}
|
|
|
|
{...confirmDialogProps}
|
|
|
|
ref={confirmDialogRef}
|
|
|
|
scrollContent
|
|
|
|
/>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default ManageFencePanel;
|