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. 274
      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. 17
      striker-ui/types/EditFenceInputGroup.d.ts

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

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

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

@ -1,9 +1,8 @@
import { Box } from '@mui/material';
import {
FC,
FormEventHandler,
ReactElement,
ReactNode,
useCallback,
useMemo,
useRef,
useState,
@ -11,156 +10,126 @@ import {
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 { ID_SEPARATOR } from './CommonFenceInputGroup';
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,
MonoText,
SensitiveText,
SmallText,
} from '../Text';
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 FormFenceParameterData = {
fenceAgent: string;
fenceName: string;
parameterInputs: {
[parameterInputId: string]: {
isParameterSensitive: boolean;
parameterId: string;
parameterType: string;
parameterValue: string;
};
};
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 fenceParameterBooleanToString = (value: boolean) => (value ? '1' : '0');
const assertFormParamValue = (value: string, paramDefault?: string) => {
if ([paramDefault, '', null, undefined].some((bad) => value === bad))
throw Error('Skippable parameter value');
};
const getFormFenceParameters = (
const getFormData = (
fenceTemplate: APIFenceTemplate,
...[{ target }]: Parameters<FormEventHandler<HTMLDivElement>>
) => {
const { elements } = target as HTMLFormElement;
return Object.values(elements).reduce<FormFenceParameterData>(
(previous, formElement) => {
const { id: inputId } = formElement;
const reExtract = new RegExp(`^(fence[^-]+)${ID_SEPARATOR}([^\\s]+)$`);
const matched = inputId.match(reExtract);
return Object.values(elements).reduce<FenceFormData>(
(previous, element) => {
try {
const matched = assertFormInputId(element);
if (matched) {
const [, fenceId, parameterId] = matched;
const [, fenceId, paramId] = matched;
previous.fenceAgent = fenceId;
previous.agent = fenceId;
const inputElement = formElement as HTMLInputElement;
const {
checked,
dataset: { sensitive: rawSensitive },
value,
} = inputElement;
const inputElement = element as HTMLInputElement;
const { checked, value } = inputElement;
if (parameterId === 'name') {
previous.fenceName = value;
}
assertFormInputName(paramId, previous, value);
const {
[fenceId]: {
parameters: {
[parameterId]: { content_type: parameterType = 'string' } = {},
},
parameters: { [paramId]: paramSpec },
},
} = fenceTemplate;
previous.parameterInputs[inputId] = {
isParameterSensitive: rawSensitive === 'true',
parameterId,
parameterType,
parameterValue:
parameterType === 'boolean'
? fenceParameterBooleanToString(checked)
: value,
};
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;
},
{ fenceAgent: '', fenceName: '', parameterInputs: {} },
{ agent: '', name: '', parameters: {} },
);
};
const buildConfirmFenceParameters = (
parameterInputs: FormFenceParameterData['parameterInputs'],
) => (
<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 (
<FlexBox
fullWidth
growFirst
height="2.8em"
key={`confirm-${parameterInputId}`}
maxWidth="100%"
row
>
<BodyText>{parameterId}</BodyText>
{textElement}
</FlexBox>
);
}}
/>
);
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 [confirmDialogProps, setConfirmDialogProps] =
useState<ConfirmDialogProps>({
actionProceedText: '',
content: '',
titleText: '',
});
const [formDialogProps, setFormDialogProps] = useState<ConfirmDialogProps>({
actionProceedText: '',
content: '',
titleText: '',
});
const [fenceTemplate, setFenceTemplate] = useProtectedState<
APIFenceTemplate | undefined
>(undefined);
@ -173,34 +142,67 @@ const ManageFencePanel: FC = () => {
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(
() => (
<List
allowEdit
allowItemButton={isEditFences}
disableDelete={!hasChecks}
edit={isEditFences}
header
listItems={fenceOverviews}
onAdd={() => {
setFormDialogProps({
actionProceedText: 'Add',
content: <AddFenceInputGroup fenceTemplate={fenceTemplate} />,
content: (
<AddFenceInputGroup
fenceTemplate={fenceTemplate}
formUtils={formUtils}
/>
),
onSubmitAppend: (event) => {
if (!fenceTemplate) {
return;
}
const addData = getFormFenceParameters(fenceTemplate, event);
const addData = getFormData(fenceTemplate, event);
const { agent, name } = addData;
setConfirmDialogProps({
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: (
<HeaderText>
Add a{' '}
<InlineMonoText fontSize="inherit">
{addData.fenceAgent}
</InlineMonoText>{' '}
<InlineMonoText fontSize="inherit">{agent}</InlineMonoText>{' '}
fence device with the following parameters?
</HeaderText>
),
@ -213,16 +215,48 @@ const ManageFencePanel: FC = () => {
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={() => {
setIsEditFences((previous) => !previous);
}}
onItemClick={({ fenceAgent: fenceId, fenceName, fenceParameters }) => {
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}
/>
@ -232,16 +266,33 @@ const ManageFencePanel: FC = () => {
return;
}
const editData = getFormFenceParameters(fenceTemplate, event);
const editData = getFormData(fenceTemplate, event);
setConfirmDialogProps({
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: (
<HeaderText>
Update{' '}
<InlineMonoText fontSize="inherit">
{editData.fenceName}
{fenceName}
</InlineMonoText>{' '}
fence device with the following parameters?
</HeaderText>
@ -261,6 +312,7 @@ const ManageFencePanel: FC = () => {
formDialogRef.current.setOpen?.call(null, true);
}}
renderListItemCheckboxState={(key) => getCheck(key)}
renderListItem={(
fenceUUID,
{ 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(
() =>
@ -309,6 +375,17 @@ const ManageFencePanel: FC = () => {
[isFenceOverviewsLoading, isLoadingFenceTemplate, listElement],
);
const messageArea = useMemo(
() => (
<MessageGroup
count={1}
defaultMessageType="warning"
ref={messageGroupRef}
/>
),
[],
);
if (isFirstRender) {
api
.get<APIFenceTemplate>(`/fence/template`)
@ -331,23 +408,26 @@ const ManageFencePanel: FC = () => {
</PanelHeader>
{panelContent}
</Panel>
<ConfirmDialog
<FormDialog
dialogProps={{
PaperProps: { sx: { minWidth: { xs: '90%', md: '50em' } } },
}}
formContent
scrollBoxProps={{
padding: '.3em .5em',
}}
scrollContent
{...formDialogProps}
disableProceed={isFormInvalid}
loadingAction={isFormSubmitting}
preActionArea={messageArea}
ref={formDialogRef}
scrollContent
/>
<ConfirmDialog
closeOnProceed
scrollBoxProps={{ paddingRight: '1em' }}
scrollContent
{...confirmDialogProps}
ref={confirmDialogRef}
scrollContent
/>
</>
);

@ -9,4 +9,7 @@ type AddFenceInputGroupOptionalProps = {
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;
isChecked?: boolean;
isRequired?: boolean;
@ -9,13 +10,13 @@ type FenceParameterInputBuilderParameters = {
value?: string;
};
type FenceParameterInputBuilder = (
args: FenceParameterInputBuilderParameters,
type FenceParameterInputBuilder<M extends MapToInputTestID> = (
args: FenceParameterInputBuilderParameters<M>,
) => ReactElement;
type MapToInputBuilder = Partial<
Record<Exclude<FenceParameterType, 'string'>, FenceParameterInputBuilder>
> & { string: FenceParameterInputBuilder };
type MapToInputBuilder<M extends MapToInputTestID> = Partial<
Record<Exclude<FenceParameterType, 'string'>, FenceParameterInputBuilder<M>>
> & { string: FenceParameterInputBuilder<M> };
type CommonFenceInputGroupOptionalProps = {
fenceId?: string;
@ -25,4 +26,7 @@ type CommonFenceInputGroupOptionalProps = {
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;
};
type EditFenceInputGroupProps = EditFenceInputGroupOptionalProps &
Required<
Pick<
CommonFenceInputGroupProps,
'fenceId' | 'previousFenceName' | 'previousFenceParameters'
>
>;
type EditFenceInputGroupProps<M extends MapToInputTestID> =
EditFenceInputGroupOptionalProps &
Required<
Pick<
CommonFenceInputGroupProps,
'fenceId' | 'previousFenceName' | 'previousFenceParameters'
>
> & {
formUtils: FormUtils<M>;
};

Loading…
Cancel
Save