fix(striker-ui): correct fence management; see details

* add input validation to fix fields, i.e., select agent, and dynamic
  fields, i.e., fence parameters (according to param type)
* connect add, update, delete dialogs to respective back-end endpoints
main
Tsu-ba-me 2 years ago
parent 203c852518
commit 488ed99370
  1. 37
      striker-ui/components/ManageFence/AddFenceInputGroup.tsx
  2. 190
      striker-ui/components/ManageFence/CommonFenceInputGroup.tsx
  3. 9
      striker-ui/components/ManageFence/EditFenceInputGroup.tsx
  4. 328
      striker-ui/components/ManageFence/ManageFencePanel.tsx
  5. 5
      striker-ui/types/AddFenceInputGroup.d.ts
  6. 18
      striker-ui/types/CommonFenceInputGroup.d.ts
  7. 7
      striker-ui/types/EditFenceInputGroup.d.ts

@ -1,17 +1,25 @@
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { FC, useMemo, useState } from 'react'; import { ReactElement, useEffect, useMemo, useState } from 'react';
import Autocomplete from '../Autocomplete'; import Autocomplete from '../Autocomplete';
import CommonFenceInputGroup from './CommonFenceInputGroup'; import CommonFenceInputGroup from './CommonFenceInputGroup';
import FlexBox from '../FlexBox'; import FlexBox from '../FlexBox';
import Spinner from '../Spinner'; import Spinner from '../Spinner';
import { BodyText } from '../Text'; import { BodyText } from '../Text';
import useIsFirstRender from '../../hooks/useIsFirstRender';
const AddFenceInputGroup: FC<AddFenceInputGroupProps> = ({ const INPUT_ID_FENCE_AGENT = 'add-fence-input-agent';
const AddFenceInputGroup = <M extends Record<string, string>>({
fenceTemplate: externalFenceTemplate, fenceTemplate: externalFenceTemplate,
formUtils,
loading: isExternalLoading, loading: isExternalLoading,
}) => { }: AddFenceInputGroupProps<M>): ReactElement => {
const [fenceTypeValue, setInputFenceTypeValue] = const { setValidity } = formUtils;
const isFirstRender = useIsFirstRender();
const [inputFenceTypeValue, setInputFenceTypeValue] =
useState<FenceAutocompleteOption | null>(null); useState<FenceAutocompleteOption | null>(null);
const fenceTypeOptions = useMemo<FenceAutocompleteOption[]>( const fenceTypeOptions = useMemo<FenceAutocompleteOption[]>(
@ -38,12 +46,13 @@ const AddFenceInputGroup: FC<AddFenceInputGroupProps> = ({
const fenceTypeElement = useMemo( const fenceTypeElement = useMemo(
() => ( () => (
<Autocomplete <Autocomplete
id="add-fence-select-type" id={INPUT_ID_FENCE_AGENT}
isOptionEqualToValue={(option, value) => isOptionEqualToValue={(option, value) =>
option.fenceId === value.fenceId option.fenceId === value.fenceId
} }
label="Fence device type" label="Fence device type"
onChange={(event, newFenceType) => { onChange={(event, newFenceType) => {
setValidity(INPUT_ID_FENCE_AGENT, newFenceType !== null);
setInputFenceTypeValue(newFenceType); setInputFenceTypeValue(newFenceType);
}} }}
openOnFocus openOnFocus
@ -74,19 +83,21 @@ const AddFenceInputGroup: FC<AddFenceInputGroupProps> = ({
</Box> </Box>
)} )}
sx={{ marginTop: '.3em' }} sx={{ marginTop: '.3em' }}
value={fenceTypeValue} value={inputFenceTypeValue}
/> />
), ),
[fenceTypeOptions, fenceTypeValue], [fenceTypeOptions, inputFenceTypeValue, setValidity],
); );
const fenceParameterElements = useMemo( const fenceParameterElements = useMemo(
() => ( () => (
<CommonFenceInputGroup <CommonFenceInputGroup
fenceId={fenceTypeValue?.fenceId} fenceId={inputFenceTypeValue?.fenceId}
fenceTemplate={externalFenceTemplate} fenceTemplate={externalFenceTemplate}
formUtils={formUtils}
/> />
), ),
[externalFenceTemplate, fenceTypeValue], [externalFenceTemplate, inputFenceTypeValue?.fenceId, formUtils],
); );
const content = useMemo( const content = useMemo(
@ -102,7 +113,15 @@ const AddFenceInputGroup: FC<AddFenceInputGroupProps> = ({
[fenceTypeElement, fenceParameterElements, isExternalLoading], [fenceTypeElement, fenceParameterElements, isExternalLoading],
); );
useEffect(() => {
if (isFirstRender) {
setValidity(INPUT_ID_FENCE_AGENT, inputFenceTypeValue !== null);
}
}, [inputFenceTypeValue, isFirstRender, setValidity]);
return <>{content}</>; return <>{content}</>;
}; };
export { INPUT_ID_FENCE_AGENT };
export default AddFenceInputGroup; export default AddFenceInputGroup;

@ -1,5 +1,5 @@
import { Box, styled, Tooltip } from '@mui/material'; import { Box, styled, Tooltip } from '@mui/material';
import { FC, ReactElement, ReactNode, useMemo } from 'react'; import { ReactElement, ReactNode, useMemo } from 'react';
import INPUT_TYPES from '../../lib/consts/INPUT_TYPES'; import INPUT_TYPES from '../../lib/consts/INPUT_TYPES';
@ -9,12 +9,92 @@ import OutlinedInputWithLabel from '../OutlinedInputWithLabel';
import { ExpandablePanel } from '../Panels'; import { ExpandablePanel } from '../Panels';
import SelectWithLabel from '../SelectWithLabel'; import SelectWithLabel from '../SelectWithLabel';
import SwitchWithLabel from '../SwitchWithLabel'; import SwitchWithLabel from '../SwitchWithLabel';
import {
buildIPAddressTestBatch,
buildNumberTestBatch,
buildPeacefulStringTestBatch,
testNotBlank,
} from '../../lib/test_input';
import { BodyText } from '../Text'; import { BodyText } from '../Text';
const CHECKED_STATES: Array<string | undefined> = ['1', 'on']; const CHECKED_STATES: Array<string | undefined> = ['1', 'on'];
const ID_SEPARATOR = '-';
const MAP_TO_INPUT_BUILDER: MapToInputBuilder = { const INPUT_ID_SEPARATOR = '-';
const getStringParamInputTestBatch = <M extends MapToInputTestID>({
formUtils: { buildFinishInputTestBatchFunction, setMessage },
id,
label,
}: {
formUtils: FormUtils<M>;
id: string;
label: string;
}) => {
const onFinishBatch = buildFinishInputTestBatchFunction(id);
const onSuccess = () => {
setMessage(id);
};
return label.toLowerCase() === 'ip'
? buildIPAddressTestBatch(
label,
onSuccess,
{ onFinishBatch },
(message) => {
setMessage(id, { children: message });
},
)
: {
defaults: {
onSuccess,
},
onFinishBatch,
tests: [{ test: testNotBlank }],
};
};
const buildNumberParamInput = <M extends MapToInputTestID>(
args: FenceParameterInputBuilderParameters<M>,
): ReactElement => {
const { formUtils, id, isRequired, label = '', name = id, value } = args;
const {
buildFinishInputTestBatchFunction,
buildInputFirstRenderFunction,
setMessage,
} = formUtils;
return (
<InputWithRef
key={`${id}-wrapper`}
input={
<OutlinedInputWithLabel
id={id}
label={label}
name={name}
type={INPUT_TYPES.number}
value={value}
/>
}
inputTestBatch={buildNumberTestBatch(
label,
() => {
setMessage(id);
},
{ onFinishBatch: buildFinishInputTestBatchFunction(id) },
(message) => {
setMessage(id, { children: message });
},
)}
onFirstRender={buildInputFirstRenderFunction(id)}
required={isRequired}
valueType="number"
/>
);
};
const MAP_TO_INPUT_BUILDER: MapToInputBuilder<Record<string, string>> = {
boolean: (args) => { boolean: (args) => {
const { id, isChecked = false, label, name = id } = args; const { id, isChecked = false, label, name = id } = args;
@ -33,8 +113,11 @@ const MAP_TO_INPUT_BUILDER: MapToInputBuilder = {
/> />
); );
}, },
integer: buildNumberParamInput,
second: buildNumberParamInput,
select: (args) => { select: (args) => {
const { const {
formUtils,
id, id,
isRequired, isRequired,
label, label,
@ -43,6 +126,12 @@ const MAP_TO_INPUT_BUILDER: MapToInputBuilder = {
value = '', value = '',
} = args; } = args;
const {
buildFinishInputTestBatchFunction,
buildInputFirstRenderFunction,
setMessage,
} = formUtils;
return ( return (
<InputWithRef <InputWithRef
key={`${id}-wrapper`} key={`${id}-wrapper`}
@ -55,12 +144,23 @@ const MAP_TO_INPUT_BUILDER: MapToInputBuilder = {
value={value} value={value}
/> />
} }
inputTestBatch={{
defaults: {
onSuccess: () => {
setMessage(id);
},
},
onFinishBatch: buildFinishInputTestBatchFunction(id),
tests: [{ test: testNotBlank }],
}}
onFirstRender={buildInputFirstRenderFunction(id)}
required={isRequired} required={isRequired}
/> />
); );
}, },
string: (args) => { string: (args) => {
const { const {
formUtils,
id, id,
isRequired, isRequired,
isSensitive = false, isSensitive = false,
@ -69,40 +169,54 @@ const MAP_TO_INPUT_BUILDER: MapToInputBuilder = {
value, value,
} = args; } = args;
const { buildInputFirstRenderFunction } = formUtils;
let inputType;
if (isSensitive) {
inputType = INPUT_TYPES.password;
}
return ( return (
<InputWithRef <InputWithRef
key={`${id}-wrapper`} key={`${id}-wrapper`}
input={ input={
<OutlinedInputWithLabel <OutlinedInputWithLabel
id={id} id={id}
inputProps={{
inputProps: { 'data-sensitive': isSensitive },
}}
label={label} label={label}
name={name} name={name}
type={inputType}
value={value} value={value}
type={isSensitive ? INPUT_TYPES.password : undefined}
/> />
} }
inputTestBatch={getStringParamInputTestBatch({ formUtils, id, label })}
onFirstRender={buildInputFirstRenderFunction(id)}
required={isRequired} required={isRequired}
/> />
); );
}, },
}; };
const combineIds = (...pieces: string[]) => pieces.join(ID_SEPARATOR); const combineIds = (...pieces: string[]) => pieces.join(INPUT_ID_SEPARATOR);
const FenceInputWrapper = styled(FlexBox)({ const FenceInputWrapper = styled(FlexBox)({
margin: '.4em 0', margin: '.4em 0',
}); });
const CommonFenceInputGroup: FC<CommonFenceInputGroupProps> = ({ const CommonFenceInputGroup = <M extends Record<string, string>>({
fenceId, fenceId,
fenceParameterTooltipProps, fenceParameterTooltipProps,
fenceTemplate, fenceTemplate,
formUtils,
previousFenceName, previousFenceName,
previousFenceParameters, previousFenceParameters,
}) => { }: CommonFenceInputGroupProps<M>): ReactElement => {
const {
buildFinishInputTestBatchFunction,
buildInputFirstRenderFunction,
setMessage,
} = formUtils;
const fenceParameterElements = useMemo(() => { const fenceParameterElements = useMemo(() => {
let result: ReactNode; let result: ReactNode;
@ -147,7 +261,8 @@ const CommonFenceInputGroup: FC<CommonFenceInputGroupProps> = ({
const isParameterDeprecated = const isParameterDeprecated =
String(rawParameterDeprecated) === '1'; String(rawParameterDeprecated) === '1';
if (!isParameterDeprecated) { if (isParameterDeprecated) return previous;
const { optional, required } = previous; const { optional, required } = previous;
const buildInput = const buildInput =
MAP_TO_INPUT_BUILDER[parameterType] ?? MAP_TO_INPUT_BUILDER[parameterType] ??
@ -157,11 +272,11 @@ const CommonFenceInputGroup: FC<CommonFenceInputGroupProps> = ({
const initialValue = const initialValue =
mapToPreviousFenceParameterValues[fenceJoinParameterId] ?? mapToPreviousFenceParameterValues[fenceJoinParameterId] ??
parameterDefault; parameterDefault;
const isParameterRequired = const isParameterRequired = String(rawParameterRequired) === '1';
String(rawParameterRequired) === '1';
const isParameterSensitive = /passw/i.test(parameterId); const isParameterSensitive = /passw/i.test(parameterId);
const parameterInput = buildInput({ const parameterInput = buildInput({
formUtils,
id: fenceJoinParameterId, id: fenceJoinParameterId,
isChecked: CHECKED_STATES.includes(initialValue), isChecked: CHECKED_STATES.includes(initialValue),
isRequired: isParameterRequired, isRequired: isParameterRequired,
@ -194,23 +309,18 @@ const CommonFenceInputGroup: FC<CommonFenceInputGroupProps> = ({
} else { } else {
optional.push(parameterInputWithTooltip); optional.push(parameterInputWithTooltip);
} }
}
return previous; return previous;
}, },
{ {
optional: [], optional: [],
required: [ required: [],
MAP_TO_INPUT_BUILDER.string({
id: combineIds(fenceId, 'name'),
isRequired: true,
label: 'Fence device name',
value: previousFenceName,
}),
],
}, },
); );
const inputIdFenceName = combineIds(fenceId, 'name');
const inputLabelFenceName = 'Fence device name';
result = ( result = (
<FlexBox <FlexBox
sx={{ sx={{
@ -219,7 +329,35 @@ const CommonFenceInputGroup: FC<CommonFenceInputGroupProps> = ({
}} }}
> >
<ExpandablePanel expandInitially header="Required parameters"> <ExpandablePanel expandInitially header="Required parameters">
<FenceInputWrapper>{requiredInputs}</FenceInputWrapper> <FenceInputWrapper>
<InputWithRef
key={`${inputIdFenceName}-wrapper`}
input={
<OutlinedInputWithLabel
id={inputIdFenceName}
label={inputLabelFenceName}
name={inputIdFenceName}
value={previousFenceName}
/>
}
inputTestBatch={buildPeacefulStringTestBatch(
inputLabelFenceName,
() => {
setMessage(inputIdFenceName);
},
{
onFinishBatch:
buildFinishInputTestBatchFunction(inputIdFenceName),
},
(message) => {
setMessage(inputIdFenceName, { children: message });
},
)}
onFirstRender={buildInputFirstRenderFunction(inputIdFenceName)}
required
/>
{requiredInputs}
</FenceInputWrapper>
</ExpandablePanel> </ExpandablePanel>
<ExpandablePanel header="Optional parameters"> <ExpandablePanel header="Optional parameters">
<FenceInputWrapper>{optionalInputs}</FenceInputWrapper> <FenceInputWrapper>{optionalInputs}</FenceInputWrapper>
@ -230,16 +368,20 @@ const CommonFenceInputGroup: FC<CommonFenceInputGroupProps> = ({
return result; return result;
}, [ }, [
buildFinishInputTestBatchFunction,
buildInputFirstRenderFunction,
fenceId, fenceId,
fenceParameterTooltipProps, fenceParameterTooltipProps,
fenceTemplate, fenceTemplate,
formUtils,
previousFenceName, previousFenceName,
previousFenceParameters, previousFenceParameters,
setMessage,
]); ]);
return <>{fenceParameterElements}</>; return <>{fenceParameterElements}</>;
}; };
export { ID_SEPARATOR }; export { INPUT_ID_SEPARATOR };
export default CommonFenceInputGroup; export default CommonFenceInputGroup;

@ -1,15 +1,16 @@
import { FC, useMemo } from 'react'; import { ReactElement, useMemo } from 'react';
import CommonFenceInputGroup from './CommonFenceInputGroup'; import CommonFenceInputGroup from './CommonFenceInputGroup';
import Spinner from '../Spinner'; import Spinner from '../Spinner';
const EditFenceInputGroup: FC<EditFenceInputGroupProps> = ({ const EditFenceInputGroup = <M extends Record<string, string>>({
fenceId, fenceId,
fenceTemplate: externalFenceTemplate, fenceTemplate: externalFenceTemplate,
formUtils,
loading: isExternalLoading, loading: isExternalLoading,
previousFenceName, previousFenceName,
previousFenceParameters, previousFenceParameters,
}) => { }: EditFenceInputGroupProps<M>): ReactElement => {
const content = useMemo( const content = useMemo(
() => () =>
isExternalLoading ? ( isExternalLoading ? (
@ -18,6 +19,7 @@ const EditFenceInputGroup: FC<EditFenceInputGroupProps> = ({
<CommonFenceInputGroup <CommonFenceInputGroup
fenceId={fenceId} fenceId={fenceId}
fenceTemplate={externalFenceTemplate} fenceTemplate={externalFenceTemplate}
formUtils={formUtils}
previousFenceName={previousFenceName} previousFenceName={previousFenceName}
previousFenceParameters={previousFenceParameters} previousFenceParameters={previousFenceParameters}
/> />
@ -25,6 +27,7 @@ const EditFenceInputGroup: FC<EditFenceInputGroupProps> = ({
[ [
externalFenceTemplate, externalFenceTemplate,
fenceId, fenceId,
formUtils,
isExternalLoading, isExternalLoading,
previousFenceName, previousFenceName,
previousFenceParameters, previousFenceParameters,

@ -1,9 +1,8 @@
import { Box } from '@mui/material';
import { import {
FC, FC,
FormEventHandler, FormEventHandler,
ReactElement,
ReactNode, ReactNode,
useCallback,
useMemo, useMemo,
useRef, useRef,
useState, useState,
@ -11,156 +10,126 @@ import {
import API_BASE_URL from '../../lib/consts/API_BASE_URL'; import API_BASE_URL from '../../lib/consts/API_BASE_URL';
import AddFenceInputGroup from './AddFenceInputGroup'; import AddFenceInputGroup, { INPUT_ID_FENCE_AGENT } from './AddFenceInputGroup';
import api from '../../lib/api'; import api from '../../lib/api';
import { ID_SEPARATOR } from './CommonFenceInputGroup'; import { INPUT_ID_SEPARATOR } from './CommonFenceInputGroup';
import ConfirmDialog from '../ConfirmDialog'; import ConfirmDialog from '../ConfirmDialog';
import EditFenceInputGroup from './EditFenceInputGroup'; import EditFenceInputGroup from './EditFenceInputGroup';
import FlexBox from '../FlexBox'; import FlexBox from '../FlexBox';
import FormDialog from '../FormDialog';
import FormSummary from '../FormSummary';
import handleAPIError from '../../lib/handleAPIError'; import handleAPIError from '../../lib/handleAPIError';
import List from '../List'; import List from '../List';
import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup';
import { Panel, PanelHeader } from '../Panels'; import { Panel, PanelHeader } from '../Panels';
import periodicFetch from '../../lib/fetchers/periodicFetch'; import periodicFetch from '../../lib/fetchers/periodicFetch';
import Spinner from '../Spinner'; import Spinner from '../Spinner';
import { import { BodyText, HeaderText, InlineMonoText, SensitiveText } from '../Text';
BodyText, import useChecklist from '../../hooks/useChecklist';
HeaderText, import useConfirmDialogProps from '../../hooks/useConfirmDialogProps';
InlineMonoText, import useFormUtils from '../../hooks/useFormUtils';
MonoText,
SensitiveText,
SmallText,
} from '../Text';
import useIsFirstRender from '../../hooks/useIsFirstRender'; import useIsFirstRender from '../../hooks/useIsFirstRender';
import useProtectedState from '../../hooks/useProtectedState'; import useProtectedState from '../../hooks/useProtectedState';
type FormFenceParameterData = { type FenceFormData = {
fenceAgent: string; agent: string;
fenceName: string; name: string;
parameterInputs: { parameters: { [parameterId: string]: string };
[parameterInputId: string]: {
isParameterSensitive: boolean;
parameterId: string;
parameterType: string;
parameterValue: string;
};
};
}; };
const fenceParameterBooleanToString = (value: boolean) => (value ? '1' : '0'); 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');
const getFormFenceParameters = ( 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, fenceTemplate: APIFenceTemplate,
...[{ target }]: Parameters<FormEventHandler<HTMLDivElement>> ...[{ target }]: Parameters<FormEventHandler<HTMLDivElement>>
) => { ) => {
const { elements } = target as HTMLFormElement; const { elements } = target as HTMLFormElement;
return Object.values(elements).reduce<FormFenceParameterData>( return Object.values(elements).reduce<FenceFormData>(
(previous, formElement) => { (previous, element) => {
const { id: inputId } = formElement; try {
const reExtract = new RegExp(`^(fence[^-]+)${ID_SEPARATOR}([^\\s]+)$`); const matched = assertFormInputId(element);
const matched = inputId.match(reExtract);
if (matched) { const [, fenceId, paramId] = matched;
const [, fenceId, parameterId] = matched;
previous.fenceAgent = fenceId; previous.agent = fenceId;
const inputElement = formElement as HTMLInputElement; const inputElement = element as HTMLInputElement;
const { const { checked, value } = inputElement;
checked,
dataset: { sensitive: rawSensitive },
value,
} = inputElement;
if (parameterId === 'name') { assertFormInputName(paramId, previous, value);
previous.fenceName = value;
}
const { const {
[fenceId]: { [fenceId]: {
parameters: { parameters: { [paramId]: paramSpec },
[parameterId]: { content_type: parameterType = 'string' } = {},
},
}, },
} = fenceTemplate; } = fenceTemplate;
previous.parameterInputs[inputId] = { assertFormParamSpec(paramSpec);
isParameterSensitive: rawSensitive === 'true',
parameterId, const { content_type: paramType, default: paramDefault } = paramSpec;
parameterType,
parameterValue: let paramValue = value;
parameterType === 'boolean'
? fenceParameterBooleanToString(checked) if (paramType === 'boolean') {
: value, paramValue = checked ? '1' : '';
};
} }
return previous; assertFormParamValue(paramValue, paramDefault);
},
{ fenceAgent: '', fenceName: '', parameterInputs: {} },
);
};
const buildConfirmFenceParameters = ( previous.parameters[paramId] = paramValue;
parameterInputs: FormFenceParameterData['parameterInputs'], } catch (error) {
) => ( return previous;
<List
listItems={parameterInputs}
listItemProps={{ sx: { padding: 0 } }}
renderListItem={(
parameterInputId,
{ isParameterSensitive, parameterId, parameterValue },
) => {
let textElement: ReactElement;
if (parameterValue) {
textElement = isParameterSensitive ? (
<SensitiveText monospaced>{parameterValue}</SensitiveText>
) : (
<Box sx={{ maxWidth: '100%', overflowX: 'scroll' }}>
<MonoText lineHeight={2.8} whiteSpace="nowrap">
{parameterValue}
</MonoText>
</Box>
);
} else {
textElement = <SmallText>none</SmallText>;
} }
return ( return previous;
<FlexBox },
fullWidth { agent: '', name: '', parameters: {} },
growFirst
height="2.8em"
key={`confirm-${parameterInputId}`}
maxWidth="100%"
row
>
<BodyText>{parameterId}</BodyText>
{textElement}
</FlexBox>
); );
}} };
/>
);
const ManageFencePanel: FC = () => { const ManageFencePanel: FC = () => {
const isFirstRender = useIsFirstRender(); const isFirstRender = useIsFirstRender();
const confirmDialogRef = useRef<ConfirmDialogForwardedRefContent>({}); const confirmDialogRef = useRef<ConfirmDialogForwardedRefContent>({});
const formDialogRef = useRef<ConfirmDialogForwardedRefContent>({}); const formDialogRef = useRef<ConfirmDialogForwardedRefContent>({});
const messageGroupRef = useRef<MessageGroupForwardedRefContent>({});
const [confirmDialogProps, setConfirmDialogProps] = useConfirmDialogProps();
const [formDialogProps, setFormDialogProps] = useConfirmDialogProps();
const [confirmDialogProps, setConfirmDialogProps] =
useState<ConfirmDialogProps>({
actionProceedText: '',
content: '',
titleText: '',
});
const [formDialogProps, setFormDialogProps] = useState<ConfirmDialogProps>({
actionProceedText: '',
content: '',
titleText: '',
});
const [fenceTemplate, setFenceTemplate] = useProtectedState< const [fenceTemplate, setFenceTemplate] = useProtectedState<
APIFenceTemplate | undefined APIFenceTemplate | undefined
>(undefined); >(undefined);
@ -173,34 +142,67 @@ const ManageFencePanel: FC = () => {
refreshInterval: 60000, 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<GetFormEntryLabelFunction>(
({ cap, depth, key }) => (depth === 0 ? cap(key) : key),
[],
);
const listElement = useMemo( const listElement = useMemo(
() => ( () => (
<List <List
allowEdit allowEdit
allowItemButton={isEditFences} allowItemButton={isEditFences}
disableDelete={!hasChecks}
edit={isEditFences} edit={isEditFences}
header header
listItems={fenceOverviews} listItems={fenceOverviews}
onAdd={() => { onAdd={() => {
setFormDialogProps({ setFormDialogProps({
actionProceedText: 'Add', actionProceedText: 'Add',
content: <AddFenceInputGroup fenceTemplate={fenceTemplate} />, content: (
<AddFenceInputGroup
fenceTemplate={fenceTemplate}
formUtils={formUtils}
/>
),
onSubmitAppend: (event) => { onSubmitAppend: (event) => {
if (!fenceTemplate) { if (!fenceTemplate) {
return; return;
} }
const addData = getFormFenceParameters(fenceTemplate, event); const addData = getFormData(fenceTemplate, event);
const { agent, name } = addData;
setConfirmDialogProps({ setConfirmDialogProps({
actionProceedText: 'Add', actionProceedText: 'Add',
content: buildConfirmFenceParameters(addData.parameterInputs), content: (
<FormSummary
entries={addData}
hasPassword
getEntryLabel={getFormSummaryEntryLabel}
/>
),
onProceedAppend: () => {
submitForm({
body: addData,
getErrorMsg: (parentMsg) => (
<>Failed to add fence device. {parentMsg}</>
),
method: 'post',
successMsg: `Added fence device ${name}`,
url: '/fence',
});
},
titleText: ( titleText: (
<HeaderText> <HeaderText>
Add a{' '} Add a{' '}
<InlineMonoText fontSize="inherit"> <InlineMonoText fontSize="inherit">{agent}</InlineMonoText>{' '}
{addData.fenceAgent}
</InlineMonoText>{' '}
fence device with the following parameters? fence device with the following parameters?
</HeaderText> </HeaderText>
), ),
@ -213,16 +215,48 @@ const ManageFencePanel: FC = () => {
formDialogRef.current.setOpen?.call(null, true); 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 }) => (
<BodyText>{fenceOverviews?.[key].fenceName}</BodyText>
),
}),
);
confirmDialogRef.current.setOpen?.call(null, true);
}}
onEdit={() => { onEdit={() => {
setIsEditFences((previous) => !previous); setIsEditFences((previous) => !previous);
}} }}
onItemClick={({ fenceAgent: fenceId, fenceName, fenceParameters }) => { onItemCheckboxChange={(key, event, checked) => {
setCheck(key, checked);
}}
onItemClick={({
fenceAgent: fenceId,
fenceName,
fenceParameters,
fenceUUID,
}) => {
setFormDialogProps({ setFormDialogProps({
actionProceedText: 'Update', actionProceedText: 'Update',
content: ( content: (
<EditFenceInputGroup <EditFenceInputGroup
fenceId={fenceId} fenceId={fenceId}
fenceTemplate={fenceTemplate} fenceTemplate={fenceTemplate}
formUtils={formUtils}
previousFenceName={fenceName} previousFenceName={fenceName}
previousFenceParameters={fenceParameters} previousFenceParameters={fenceParameters}
/> />
@ -232,16 +266,33 @@ const ManageFencePanel: FC = () => {
return; return;
} }
const editData = getFormFenceParameters(fenceTemplate, event); const editData = getFormData(fenceTemplate, event);
setConfirmDialogProps({ setConfirmDialogProps({
actionProceedText: 'Update', actionProceedText: 'Update',
content: buildConfirmFenceParameters(editData.parameterInputs), content: (
<FormSummary
entries={editData}
hasPassword
getEntryLabel={getFormSummaryEntryLabel}
/>
),
onProceedAppend: () => {
submitForm({
body: editData,
getErrorMsg: (parentMsg) => (
<>Failed to update fence device. {parentMsg}</>
),
method: 'put',
successMsg: `Updated fence device ${fenceName}`,
url: `/fence/${fenceUUID}`,
});
},
titleText: ( titleText: (
<HeaderText> <HeaderText>
Update{' '} Update{' '}
<InlineMonoText fontSize="inherit"> <InlineMonoText fontSize="inherit">
{editData.fenceName} {fenceName}
</InlineMonoText>{' '} </InlineMonoText>{' '}
fence device with the following parameters? fence device with the following parameters?
</HeaderText> </HeaderText>
@ -261,6 +312,7 @@ const ManageFencePanel: FC = () => {
formDialogRef.current.setOpen?.call(null, true); formDialogRef.current.setOpen?.call(null, true);
}} }}
renderListItemCheckboxState={(key) => getCheck(key)}
renderListItem={( renderListItem={(
fenceUUID, fenceUUID,
{ fenceAgent, fenceName, fenceParameters }, { fenceAgent, fenceName, fenceParameters },
@ -297,7 +349,21 @@ const ManageFencePanel: FC = () => {
)} )}
/> />
), ),
[fenceOverviews, fenceTemplate, isEditFences], [
buildDeleteDialogProps,
checks,
fenceOverviews,
fenceTemplate,
formUtils,
getCheck,
getFormSummaryEntryLabel,
hasChecks,
isEditFences,
setCheck,
setConfirmDialogProps,
setFormDialogProps,
submitForm,
],
); );
const panelContent = useMemo( const panelContent = useMemo(
() => () =>
@ -309,6 +375,17 @@ const ManageFencePanel: FC = () => {
[isFenceOverviewsLoading, isLoadingFenceTemplate, listElement], [isFenceOverviewsLoading, isLoadingFenceTemplate, listElement],
); );
const messageArea = useMemo(
() => (
<MessageGroup
count={1}
defaultMessageType="warning"
ref={messageGroupRef}
/>
),
[],
);
if (isFirstRender) { if (isFirstRender) {
api api
.get<APIFenceTemplate>(`/fence/template`) .get<APIFenceTemplate>(`/fence/template`)
@ -331,23 +408,26 @@ const ManageFencePanel: FC = () => {
</PanelHeader> </PanelHeader>
{panelContent} {panelContent}
</Panel> </Panel>
<ConfirmDialog <FormDialog
dialogProps={{ dialogProps={{
PaperProps: { sx: { minWidth: { xs: '90%', md: '50em' } } }, PaperProps: { sx: { minWidth: { xs: '90%', md: '50em' } } },
}} }}
formContent
scrollBoxProps={{ scrollBoxProps={{
padding: '.3em .5em', padding: '.3em .5em',
}} }}
scrollContent
{...formDialogProps} {...formDialogProps}
disableProceed={isFormInvalid}
loadingAction={isFormSubmitting}
preActionArea={messageArea}
ref={formDialogRef} ref={formDialogRef}
scrollContent
/> />
<ConfirmDialog <ConfirmDialog
closeOnProceed
scrollBoxProps={{ paddingRight: '1em' }} scrollBoxProps={{ paddingRight: '1em' }}
scrollContent
{...confirmDialogProps} {...confirmDialogProps}
ref={confirmDialogRef} ref={confirmDialogRef}
scrollContent
/> />
</> </>
); );

@ -9,4 +9,7 @@ type AddFenceInputGroupOptionalProps = {
loading?: boolean; loading?: boolean;
}; };
type AddFenceInputGroupProps = AddFenceInputGroupOptionalProps; type AddFenceInputGroupProps<M extends MapToInputTestID> =
AddFenceInputGroupOptionalProps & {
formUtils: FormUtils<M>;
};

@ -1,4 +1,5 @@
type FenceParameterInputBuilderParameters = { type FenceParameterInputBuilderParameters<M extends MapToInputTestID> = {
formUtils: FormUtils<M>;
id: string; id: string;
isChecked?: boolean; isChecked?: boolean;
isRequired?: boolean; isRequired?: boolean;
@ -9,13 +10,13 @@ type FenceParameterInputBuilderParameters = {
value?: string; value?: string;
}; };
type FenceParameterInputBuilder = ( type FenceParameterInputBuilder<M extends MapToInputTestID> = (
args: FenceParameterInputBuilderParameters, args: FenceParameterInputBuilderParameters<M>,
) => ReactElement; ) => ReactElement;
type MapToInputBuilder = Partial< type MapToInputBuilder<M extends MapToInputTestID> = Partial<
Record<Exclude<FenceParameterType, 'string'>, FenceParameterInputBuilder> Record<Exclude<FenceParameterType, 'string'>, FenceParameterInputBuilder<M>>
> & { string: FenceParameterInputBuilder }; > & { string: FenceParameterInputBuilder<M> };
type CommonFenceInputGroupOptionalProps = { type CommonFenceInputGroupOptionalProps = {
fenceId?: string; fenceId?: string;
@ -25,4 +26,7 @@ type CommonFenceInputGroupOptionalProps = {
fenceParameterTooltipProps?: import('@mui/material').TooltipProps; fenceParameterTooltipProps?: import('@mui/material').TooltipProps;
}; };
type CommonFenceInputGroupProps = CommonFenceInputGroupOptionalProps; type CommonFenceInputGroupProps<M extends MapToInputTestID> =
CommonFenceInputGroupOptionalProps & {
formUtils: FormUtils<M>;
};

@ -3,10 +3,13 @@ type EditFenceInputGroupOptionalProps = {
loading?: boolean; loading?: boolean;
}; };
type EditFenceInputGroupProps = EditFenceInputGroupOptionalProps & type EditFenceInputGroupProps<M extends MapToInputTestID> =
EditFenceInputGroupOptionalProps &
Required< Required<
Pick< Pick<
CommonFenceInputGroupProps, CommonFenceInputGroupProps,
'fenceId' | 'previousFenceName' | 'previousFenceParameters' 'fenceId' | 'previousFenceName' | 'previousFenceParameters'
> >
>; > & {
formUtils: FormUtils<M>;
};

Loading…
Cancel
Save