commit
941221fa41
52 changed files with 1835 additions and 430 deletions
@ -0,0 +1,56 @@ |
||||
import { RequestHandler } from 'express'; |
||||
|
||||
import buildGetRequestHandler from '../buildGetRequestHandler'; |
||||
import { buildQueryResultReducer } from '../../buildQueryResultModifier'; |
||||
import { stdout } from '../../shell'; |
||||
|
||||
export const getFence: RequestHandler = buildGetRequestHandler( |
||||
(request, buildQueryOptions) => { |
||||
const query = ` |
||||
SELECT |
||||
fence_uuid, |
||||
fence_name, |
||||
fence_agent, |
||||
fence_arguments |
||||
FROM fences |
||||
ORDER BY fence_name ASC;`;
|
||||
const afterQueryReturn: QueryResultModifierFunction | undefined = |
||||
buildQueryResultReducer<{ [fenceUUID: string]: FenceOverview }>( |
||||
(previous, [fenceUUID, fenceName, fenceAgent, fenceArgumentString]) => { |
||||
const fenceParameters = fenceArgumentString |
||||
.split(/\s+/) |
||||
.reduce<FenceParameters>((previous, parameterPair) => { |
||||
const [parameterId, parameterValue] = parameterPair.split(/=/); |
||||
|
||||
previous[parameterId] = parameterValue.replace(/['"]/g, ''); |
||||
|
||||
return previous; |
||||
}, {}); |
||||
|
||||
stdout( |
||||
`${fenceAgent}: ${fenceName} (${fenceUUID})\n${JSON.stringify( |
||||
fenceParameters, |
||||
null, |
||||
2, |
||||
)}`,
|
||||
); |
||||
|
||||
previous[fenceUUID] = { |
||||
fenceAgent, |
||||
fenceParameters, |
||||
fenceName, |
||||
fenceUUID, |
||||
}; |
||||
|
||||
return previous; |
||||
}, |
||||
{}, |
||||
); |
||||
|
||||
if (buildQueryOptions) { |
||||
buildQueryOptions.afterQueryReturn = afterQueryReturn; |
||||
} |
||||
|
||||
return query; |
||||
}, |
||||
); |
@ -0,0 +1,22 @@ |
||||
import { RequestHandler } from 'express'; |
||||
import { getAnvilData } from '../../accessModule'; |
||||
import { stderr } from '../../shell'; |
||||
|
||||
export const getFenceTemplate: RequestHandler = (request, response) => { |
||||
let rawFenceData; |
||||
|
||||
try { |
||||
({ fence_data: rawFenceData } = getAnvilData( |
||||
{ fence_data: true }, |
||||
{ predata: [['Striker->get_fence_data']] }, |
||||
)); |
||||
} catch (subError) { |
||||
stderr(`Failed to get fence device template; CAUSE: ${subError}`); |
||||
|
||||
response.status(500).send(); |
||||
|
||||
return; |
||||
} |
||||
|
||||
response.status(200).send(rawFenceData); |
||||
}; |
@ -0,0 +1,2 @@ |
||||
export * from './getFence'; |
||||
export * from './getFenceTemplate'; |
@ -0,0 +1,9 @@ |
||||
import express from 'express'; |
||||
|
||||
import { getFence, getFenceTemplate } from '../lib/request_handlers/fence'; |
||||
|
||||
const router = express.Router(); |
||||
|
||||
router.get('/', getFence).get('/template', getFenceTemplate); |
||||
|
||||
export default router; |
@ -0,0 +1,10 @@ |
||||
type FenceParameters = { |
||||
[parameterId: string]: string; |
||||
}; |
||||
|
||||
type FenceOverview = { |
||||
fenceAgent: string; |
||||
fenceParameters: FenceParameters; |
||||
fenceName: string; |
||||
fenceUUID: string; |
||||
}; |
@ -1,47 +1,111 @@ |
||||
import { FC } from 'react'; |
||||
import { |
||||
Done as MUIDoneIcon, |
||||
Edit as MUIEditIcon, |
||||
Visibility as MUIVisibilityIcon, |
||||
VisibilityOff as MUIVisibilityOffIcon, |
||||
} from '@mui/icons-material'; |
||||
import { |
||||
IconButton as MUIIconButton, |
||||
IconButtonProps as MUIIconButtonProps, |
||||
inputClasses as muiInputClasses, |
||||
styled, |
||||
} from '@mui/material'; |
||||
import { createElement, FC, ReactNode, useMemo } from 'react'; |
||||
|
||||
import { |
||||
BLACK, |
||||
BORDER_RADIUS, |
||||
DISABLED, |
||||
GREY, |
||||
TEXT, |
||||
} from '../../lib/consts/DEFAULT_THEME'; |
||||
|
||||
export type IconButtonProps = MUIIconButtonProps; |
||||
type IconButtonProps = IconButtonOptionalProps & MUIIconButtonProps; |
||||
|
||||
const IconButton: FC<IconButtonProps> = ({ |
||||
children, |
||||
sx, |
||||
...iconButtonRestProps |
||||
}) => ( |
||||
<MUIIconButton |
||||
{...{ |
||||
...iconButtonRestProps, |
||||
sx: { |
||||
const ContainedIconButton = styled(MUIIconButton)({ |
||||
borderRadius: BORDER_RADIUS, |
||||
backgroundColor: GREY, |
||||
color: BLACK, |
||||
|
||||
'&:hover': { |
||||
backgroundColor: TEXT, |
||||
backgroundColor: `${GREY}F0`, |
||||
}, |
||||
|
||||
[`&.${muiInputClasses.disabled}`]: { |
||||
backgroundColor: DISABLED, |
||||
}, |
||||
}); |
||||
|
||||
...sx, |
||||
}, |
||||
}} |
||||
> |
||||
{children} |
||||
</MUIIconButton> |
||||
); |
||||
const NormalIconButton = styled(MUIIconButton)({ |
||||
color: GREY, |
||||
}); |
||||
|
||||
const MAP_TO_VISIBILITY_ICON: IconButtonMapToStateIcon = { |
||||
false: MUIVisibilityIcon, |
||||
true: MUIVisibilityOffIcon, |
||||
}; |
||||
|
||||
const MAP_TO_EDIT_ICON: IconButtonMapToStateIcon = { |
||||
false: MUIEditIcon, |
||||
true: MUIDoneIcon, |
||||
}; |
||||
|
||||
const MAP_TO_MAP_PRESET: Record< |
||||
IconButtonPresetMapToStateIcon, |
||||
IconButtonMapToStateIcon |
||||
> = { |
||||
edit: MAP_TO_EDIT_ICON, |
||||
visibility: MAP_TO_VISIBILITY_ICON, |
||||
}; |
||||
|
||||
const MAP_TO_VARIANT: Record<IconButtonVariant, CreatableComponent> = { |
||||
contained: ContainedIconButton, |
||||
normal: NormalIconButton, |
||||
}; |
||||
|
||||
const IconButton: FC<IconButtonProps> = ({ |
||||
children, |
||||
defaultIcon, |
||||
iconProps, |
||||
mapPreset, |
||||
mapToIcon: externalMapToIcon, |
||||
state, |
||||
variant = 'contained', |
||||
...restIconButtonProps |
||||
}) => { |
||||
const mapToIcon = useMemo<IconButtonMapToStateIcon | undefined>( |
||||
() => externalMapToIcon ?? (mapPreset && MAP_TO_MAP_PRESET[mapPreset]), |
||||
[externalMapToIcon, mapPreset], |
||||
); |
||||
|
||||
const iconButtonContent = useMemo(() => { |
||||
let result: ReactNode; |
||||
|
||||
if (mapToIcon) { |
||||
const iconElementType: CreatableComponent | undefined = state |
||||
? mapToIcon[state] ?? defaultIcon |
||||
: defaultIcon; |
||||
|
||||
if (iconElementType) { |
||||
result = createElement(iconElementType, iconProps); |
||||
} |
||||
} else { |
||||
result = children; |
||||
} |
||||
|
||||
return result; |
||||
}, [children, mapToIcon, state, defaultIcon, iconProps]); |
||||
const iconButtonElementType = useMemo( |
||||
() => MAP_TO_VARIANT[variant], |
||||
[variant], |
||||
); |
||||
|
||||
return createElement( |
||||
iconButtonElementType, |
||||
restIconButtonProps, |
||||
iconButtonContent, |
||||
); |
||||
}; |
||||
|
||||
export type { IconButtonProps }; |
||||
|
||||
export default IconButton; |
||||
|
@ -0,0 +1,108 @@ |
||||
import { Box } from '@mui/material'; |
||||
import { FC, useMemo, useState } from 'react'; |
||||
|
||||
import Autocomplete from '../Autocomplete'; |
||||
import CommonFenceInputGroup from './CommonFenceInputGroup'; |
||||
import FlexBox from '../FlexBox'; |
||||
import Spinner from '../Spinner'; |
||||
import { BodyText } from '../Text'; |
||||
|
||||
const AddFenceInputGroup: FC<AddFenceInputGroupProps> = ({ |
||||
fenceTemplate: externalFenceTemplate, |
||||
loading: isExternalLoading, |
||||
}) => { |
||||
const [fenceTypeValue, setInputFenceTypeValue] = |
||||
useState<FenceAutocompleteOption | null>(null); |
||||
|
||||
const fenceTypeOptions = useMemo<FenceAutocompleteOption[]>( |
||||
() => |
||||
externalFenceTemplate |
||||
? Object.entries(externalFenceTemplate) |
||||
.sort(([a], [b]) => (a > b ? 1 : -1)) |
||||
.map(([id, { description: rawDescription }]) => { |
||||
const description = |
||||
typeof rawDescription === 'string' |
||||
? rawDescription |
||||
: 'No description.'; |
||||
|
||||
return { |
||||
fenceDescription: description, |
||||
fenceId: id, |
||||
label: id, |
||||
}; |
||||
}) |
||||
: [], |
||||
[externalFenceTemplate], |
||||
); |
||||
|
||||
const fenceTypeElement = useMemo( |
||||
() => ( |
||||
<Autocomplete |
||||
id="add-fence-select-type" |
||||
isOptionEqualToValue={(option, value) => |
||||
option.fenceId === value.fenceId |
||||
} |
||||
label="Fence device type" |
||||
onChange={(event, newFenceType) => { |
||||
setInputFenceTypeValue(newFenceType); |
||||
}} |
||||
openOnFocus |
||||
options={fenceTypeOptions} |
||||
renderOption={(props, { fenceDescription, fenceId }, { selected }) => ( |
||||
<Box |
||||
component="li" |
||||
sx={{ |
||||
display: 'flex', |
||||
flexDirection: 'column', |
||||
|
||||
'& > *': { |
||||
width: '100%', |
||||
}, |
||||
}} |
||||
{...props} |
||||
> |
||||
<BodyText |
||||
inverted |
||||
sx={{ |
||||
fontSize: '1.2em', |
||||
fontWeight: selected ? 400 : undefined, |
||||
}} |
||||
> |
||||
{fenceId} |
||||
</BodyText> |
||||
<BodyText selected={false}>{fenceDescription}</BodyText> |
||||
</Box> |
||||
)} |
||||
sx={{ marginTop: '.3em' }} |
||||
value={fenceTypeValue} |
||||
/> |
||||
), |
||||
[fenceTypeOptions, fenceTypeValue], |
||||
); |
||||
const fenceParameterElements = useMemo( |
||||
() => ( |
||||
<CommonFenceInputGroup |
||||
fenceId={fenceTypeValue?.fenceId} |
||||
fenceTemplate={externalFenceTemplate} |
||||
/> |
||||
), |
||||
[externalFenceTemplate, fenceTypeValue], |
||||
); |
||||
|
||||
const content = useMemo( |
||||
() => |
||||
isExternalLoading ? ( |
||||
<Spinner /> |
||||
) : ( |
||||
<FlexBox> |
||||
{fenceTypeElement} |
||||
{fenceParameterElements} |
||||
</FlexBox> |
||||
), |
||||
[fenceTypeElement, fenceParameterElements, isExternalLoading], |
||||
); |
||||
|
||||
return <>{content}</>; |
||||
}; |
||||
|
||||
export default AddFenceInputGroup; |
@ -0,0 +1,246 @@ |
||||
import { Box, styled, Tooltip } from '@mui/material'; |
||||
import { FC, ReactElement, ReactNode, useMemo } from 'react'; |
||||
|
||||
import INPUT_TYPES from '../../lib/consts/INPUT_TYPES'; |
||||
|
||||
import FlexBox from '../FlexBox'; |
||||
import InputWithRef from '../InputWithRef'; |
||||
import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; |
||||
import { ExpandablePanel } from '../Panels'; |
||||
import SelectWithLabel from '../SelectWithLabel'; |
||||
import SwitchWithLabel from '../SwitchWithLabel'; |
||||
import { BodyText } from '../Text'; |
||||
|
||||
const CHECKED_STATES: Array<string | undefined> = ['1', 'on']; |
||||
const ID_SEPARATOR = '-'; |
||||
|
||||
const MAP_TO_INPUT_BUILDER: MapToInputBuilder = { |
||||
boolean: (args) => { |
||||
const { id, isChecked = false, label, name = id } = args; |
||||
|
||||
return ( |
||||
<InputWithRef |
||||
key={`${id}-wrapper`} |
||||
input={ |
||||
<SwitchWithLabel |
||||
checked={isChecked} |
||||
flexBoxProps={{ width: '100%' }} |
||||
id={id} |
||||
label={label} |
||||
name={name} |
||||
/> |
||||
} |
||||
valueType="boolean" |
||||
/> |
||||
); |
||||
}, |
||||
select: (args) => { |
||||
const { |
||||
id, |
||||
isRequired, |
||||
label, |
||||
name = id, |
||||
selectOptions = [], |
||||
value = '', |
||||
} = args; |
||||
|
||||
return ( |
||||
<InputWithRef |
||||
key={`${id}-wrapper`} |
||||
input={ |
||||
<SelectWithLabel |
||||
id={id} |
||||
label={label} |
||||
name={name} |
||||
selectItems={selectOptions} |
||||
value={value} |
||||
/> |
||||
} |
||||
required={isRequired} |
||||
/> |
||||
); |
||||
}, |
||||
string: (args) => { |
||||
const { |
||||
id, |
||||
isRequired, |
||||
isSensitive = false, |
||||
label = '', |
||||
name = id, |
||||
value, |
||||
} = args; |
||||
|
||||
return ( |
||||
<InputWithRef |
||||
key={`${id}-wrapper`} |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={id} |
||||
inputProps={{ |
||||
inputProps: { 'data-sensitive': isSensitive }, |
||||
}} |
||||
label={label} |
||||
name={name} |
||||
value={value} |
||||
type={isSensitive ? INPUT_TYPES.password : undefined} |
||||
/> |
||||
} |
||||
required={isRequired} |
||||
/> |
||||
); |
||||
}, |
||||
}; |
||||
|
||||
const combineIds = (...pieces: string[]) => pieces.join(ID_SEPARATOR); |
||||
|
||||
const FenceInputWrapper = styled(FlexBox)({ |
||||
margin: '.4em 0', |
||||
}); |
||||
|
||||
const CommonFenceInputGroup: FC<CommonFenceInputGroupProps> = ({ |
||||
fenceId, |
||||
fenceParameterTooltipProps, |
||||
fenceTemplate, |
||||
previousFenceName, |
||||
previousFenceParameters, |
||||
}) => { |
||||
const fenceParameterElements = useMemo(() => { |
||||
let result: ReactNode; |
||||
|
||||
if (fenceTemplate && fenceId) { |
||||
const { parameters: fenceParameters } = fenceTemplate[fenceId]; |
||||
|
||||
let mapToPreviousFenceParameterValues: FenceParameters = {}; |
||||
|
||||
if (previousFenceParameters) { |
||||
mapToPreviousFenceParameterValues = Object.entries( |
||||
previousFenceParameters, |
||||
).reduce<FenceParameters>((previous, [parameterId, parameterValue]) => { |
||||
const newKey = combineIds(fenceId, parameterId); |
||||
|
||||
previous[newKey] = parameterValue; |
||||
|
||||
return previous; |
||||
}, {}); |
||||
} |
||||
|
||||
const { optional: optionalInputs, required: requiredInputs } = |
||||
Object.entries(fenceParameters) |
||||
.sort(([a], [b]) => (a > b ? 1 : -1)) |
||||
.reduce<{ |
||||
optional: ReactElement[]; |
||||
required: ReactElement[]; |
||||
}>( |
||||
( |
||||
previous, |
||||
[ |
||||
parameterId, |
||||
{ |
||||
content_type: parameterType, |
||||
default: parameterDefault, |
||||
deprecated: rawParameterDeprecated, |
||||
description: parameterDescription, |
||||
options: parameterSelectOptions, |
||||
required: rawParameterRequired, |
||||
}, |
||||
], |
||||
) => { |
||||
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' }, |
||||
}, |
||||
}, |
||||
}} |
||||
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, |
||||
}), |
||||
], |
||||
}, |
||||
); |
||||
|
||||
result = ( |
||||
<FlexBox |
||||
sx={{ |
||||
'& > div:first-child': { marginTop: 0 }, |
||||
'& > div': { marginBottom: 0 }, |
||||
}} |
||||
> |
||||
<ExpandablePanel expandInitially header="Required parameters"> |
||||
<FenceInputWrapper>{requiredInputs}</FenceInputWrapper> |
||||
</ExpandablePanel> |
||||
<ExpandablePanel header="Optional parameters"> |
||||
<FenceInputWrapper>{optionalInputs}</FenceInputWrapper> |
||||
</ExpandablePanel> |
||||
</FlexBox> |
||||
); |
||||
} |
||||
|
||||
return result; |
||||
}, [ |
||||
fenceId, |
||||
fenceParameterTooltipProps, |
||||
fenceTemplate, |
||||
previousFenceName, |
||||
previousFenceParameters, |
||||
]); |
||||
|
||||
return <>{fenceParameterElements}</>; |
||||
}; |
||||
|
||||
export { ID_SEPARATOR }; |
||||
|
||||
export default CommonFenceInputGroup; |
@ -0,0 +1,37 @@ |
||||
import { FC, useMemo } from 'react'; |
||||
|
||||
import CommonFenceInputGroup from './CommonFenceInputGroup'; |
||||
import Spinner from '../Spinner'; |
||||
|
||||
const EditFenceInputGroup: FC<EditFenceInputGroupProps> = ({ |
||||
fenceId, |
||||
fenceTemplate: externalFenceTemplate, |
||||
loading: isExternalLoading, |
||||
previousFenceName, |
||||
previousFenceParameters, |
||||
}) => { |
||||
const content = useMemo( |
||||
() => |
||||
isExternalLoading ? ( |
||||
<Spinner /> |
||||
) : ( |
||||
<CommonFenceInputGroup |
||||
fenceId={fenceId} |
||||
fenceTemplate={externalFenceTemplate} |
||||
previousFenceName={previousFenceName} |
||||
previousFenceParameters={previousFenceParameters} |
||||
/> |
||||
), |
||||
[ |
||||
externalFenceTemplate, |
||||
fenceId, |
||||
isExternalLoading, |
||||
previousFenceName, |
||||
previousFenceParameters, |
||||
], |
||||
); |
||||
|
||||
return <>{content}</>; |
||||
}; |
||||
|
||||
export default EditFenceInputGroup; |
@ -0,0 +1,356 @@ |
||||
import { Box } from '@mui/material'; |
||||
import { |
||||
FC, |
||||
FormEventHandler, |
||||
ReactElement, |
||||
ReactNode, |
||||
useMemo, |
||||
useRef, |
||||
useState, |
||||
} from 'react'; |
||||
|
||||
import API_BASE_URL from '../../lib/consts/API_BASE_URL'; |
||||
|
||||
import AddFenceInputGroup from './AddFenceInputGroup'; |
||||
import api from '../../lib/api'; |
||||
import { ID_SEPARATOR } from './CommonFenceInputGroup'; |
||||
import ConfirmDialog from '../ConfirmDialog'; |
||||
import EditFenceInputGroup from './EditFenceInputGroup'; |
||||
import FlexBox from '../FlexBox'; |
||||
import handleAPIError from '../../lib/handleAPIError'; |
||||
import List from '../List'; |
||||
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 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; |
||||
}; |
||||
}; |
||||
}; |
||||
|
||||
const fenceParameterBooleanToString = (value: boolean) => (value ? '1' : '0'); |
||||
|
||||
const getFormFenceParameters = ( |
||||
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); |
||||
|
||||
if (matched) { |
||||
const [, fenceId, parameterId] = matched; |
||||
|
||||
previous.fenceAgent = fenceId; |
||||
|
||||
const inputElement = formElement as HTMLInputElement; |
||||
const { |
||||
checked, |
||||
dataset: { sensitive: rawSensitive }, |
||||
value, |
||||
} = inputElement; |
||||
|
||||
if (parameterId === 'name') { |
||||
previous.fenceName = value; |
||||
} |
||||
|
||||
const { |
||||
[fenceId]: { |
||||
parameters: { |
||||
[parameterId]: { content_type: parameterType = 'string' } = {}, |
||||
}, |
||||
}, |
||||
} = fenceTemplate; |
||||
|
||||
previous.parameterInputs[inputId] = { |
||||
isParameterSensitive: rawSensitive === 'true', |
||||
parameterId, |
||||
parameterType, |
||||
parameterValue: |
||||
parameterType === 'boolean' |
||||
? fenceParameterBooleanToString(checked) |
||||
: value, |
||||
}; |
||||
} |
||||
|
||||
return previous; |
||||
}, |
||||
{ fenceAgent: '', fenceName: '', parameterInputs: {} }, |
||||
); |
||||
}; |
||||
|
||||
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 [confirmDialogProps, setConfirmDialogProps] = |
||||
useState<ConfirmDialogProps>({ |
||||
actionProceedText: '', |
||||
content: '', |
||||
titleText: '', |
||||
}); |
||||
const [formDialogProps, setFormDialogProps] = useState<ConfirmDialogProps>({ |
||||
actionProceedText: '', |
||||
content: '', |
||||
titleText: '', |
||||
}); |
||||
const [fenceTemplate, setFenceTemplate] = useProtectedState< |
||||
APIFenceTemplate | undefined |
||||
>(undefined); |
||||
const [isEditFences, setIsEditFences] = useState<boolean>(false); |
||||
const [isLoadingFenceTemplate, setIsLoadingFenceTemplate] = |
||||
useProtectedState<boolean>(true); |
||||
|
||||
const { data: fenceOverviews, isLoading: isFenceOverviewsLoading } = |
||||
periodicFetch<APIFenceOverview>(`${API_BASE_URL}/fence`, { |
||||
refreshInterval: 60000, |
||||
}); |
||||
|
||||
const listElement = useMemo( |
||||
() => ( |
||||
<List |
||||
allowEdit |
||||
allowItemButton={isEditFences} |
||||
edit={isEditFences} |
||||
header |
||||
listItems={fenceOverviews} |
||||
onAdd={() => { |
||||
setFormDialogProps({ |
||||
actionProceedText: 'Add', |
||||
content: <AddFenceInputGroup fenceTemplate={fenceTemplate} />, |
||||
onSubmitAppend: (event) => { |
||||
if (!fenceTemplate) { |
||||
return; |
||||
} |
||||
|
||||
const addData = getFormFenceParameters(fenceTemplate, event); |
||||
|
||||
setConfirmDialogProps({ |
||||
actionProceedText: 'Add', |
||||
content: buildConfirmFenceParameters(addData.parameterInputs), |
||||
titleText: ( |
||||
<HeaderText> |
||||
Add a{' '} |
||||
<InlineMonoText fontSize="inherit"> |
||||
{addData.fenceAgent} |
||||
</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); |
||||
}} |
||||
onEdit={() => { |
||||
setIsEditFences((previous) => !previous); |
||||
}} |
||||
onItemClick={({ fenceAgent: fenceId, fenceName, fenceParameters }) => { |
||||
setFormDialogProps({ |
||||
actionProceedText: 'Update', |
||||
content: ( |
||||
<EditFenceInputGroup |
||||
fenceId={fenceId} |
||||
fenceTemplate={fenceTemplate} |
||||
previousFenceName={fenceName} |
||||
previousFenceParameters={fenceParameters} |
||||
/> |
||||
), |
||||
onSubmitAppend: (event) => { |
||||
if (!fenceTemplate) { |
||||
return; |
||||
} |
||||
|
||||
const editData = getFormFenceParameters(fenceTemplate, event); |
||||
|
||||
setConfirmDialogProps({ |
||||
actionProceedText: 'Update', |
||||
content: buildConfirmFenceParameters(editData.parameterInputs), |
||||
titleText: ( |
||||
<HeaderText> |
||||
Update{' '} |
||||
<InlineMonoText fontSize="inherit"> |
||||
{editData.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); |
||||
}} |
||||
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> |
||||
)} |
||||
/> |
||||
), |
||||
[fenceOverviews, fenceTemplate, isEditFences], |
||||
); |
||||
const panelContent = useMemo( |
||||
() => |
||||
isLoadingFenceTemplate || isFenceOverviewsLoading ? ( |
||||
<Spinner /> |
||||
) : ( |
||||
listElement |
||||
), |
||||
[isFenceOverviewsLoading, isLoadingFenceTemplate, listElement], |
||||
); |
||||
|
||||
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> |
||||
<ConfirmDialog |
||||
dialogProps={{ |
||||
PaperProps: { sx: { minWidth: { xs: '90%', md: '50em' } } }, |
||||
}} |
||||
formContent |
||||
scrollBoxProps={{ |
||||
padding: '.3em .5em', |
||||
}} |
||||
scrollContent |
||||
{...formDialogProps} |
||||
ref={formDialogRef} |
||||
/> |
||||
<ConfirmDialog |
||||
scrollBoxProps={{ paddingRight: '1em' }} |
||||
scrollContent |
||||
{...confirmDialogProps} |
||||
ref={confirmDialogRef} |
||||
/> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default ManageFencePanel; |
@ -0,0 +1,3 @@ |
||||
import ManageFencePanel from './ManageFencePanel'; |
||||
|
||||
export default ManageFencePanel; |
@ -0,0 +1,56 @@ |
||||
import { Switch, SxProps, Theme } from '@mui/material'; |
||||
import { FC, useMemo } from 'react'; |
||||
|
||||
import { GREY } from '../lib/consts/DEFAULT_THEME'; |
||||
|
||||
import FlexBox from './FlexBox'; |
||||
import { BodyText } from './Text'; |
||||
|
||||
const SwitchWithLabel: FC<SwitchWithLabelProps> = ({ |
||||
checked: isChecked, |
||||
flexBoxProps: { sx: flexBoxSx, ...restFlexBoxProps } = {}, |
||||
id: switchId, |
||||
label, |
||||
name: switchName, |
||||
onChange, |
||||
switchProps, |
||||
}) => { |
||||
const combinedFlexBoxSx = useMemo<SxProps<Theme>>( |
||||
() => ({ |
||||
'& > :first-child': { |
||||
flexGrow: 1, |
||||
}, |
||||
|
||||
...flexBoxSx, |
||||
}), |
||||
[flexBoxSx], |
||||
); |
||||
|
||||
const labelElement = useMemo( |
||||
() => |
||||
typeof label === 'string' ? ( |
||||
<BodyText inheritColour color={`${GREY}9F`}> |
||||
{label} |
||||
</BodyText> |
||||
) : ( |
||||
label |
||||
), |
||||
[label], |
||||
); |
||||
|
||||
return ( |
||||
<FlexBox row {...restFlexBoxProps} sx={combinedFlexBoxSx}> |
||||
{labelElement} |
||||
<Switch |
||||
checked={isChecked} |
||||
edge="end" |
||||
id={switchId} |
||||
name={switchName} |
||||
onChange={onChange} |
||||
{...switchProps} |
||||
/> |
||||
</FlexBox> |
||||
); |
||||
}; |
||||
|
||||
export default SwitchWithLabel; |
@ -1,21 +1,29 @@ |
||||
import { Box } from '@mui/material'; |
||||
import { ReactElement, useMemo } from 'react'; |
||||
import { ReactElement, ReactNode, useMemo } from 'react'; |
||||
|
||||
const TabContent = <T,>({ |
||||
changingTabId, |
||||
children, |
||||
retain = false, |
||||
tabId, |
||||
}: TabContentProps<T>): ReactElement => { |
||||
const isTabIdMatch = useMemo( |
||||
() => changingTabId === tabId, |
||||
[changingTabId, tabId], |
||||
); |
||||
const displayValue = useMemo( |
||||
() => (isTabIdMatch ? 'initial' : 'none'), |
||||
[isTabIdMatch], |
||||
const result = useMemo<ReactNode>( |
||||
() => |
||||
retain ? ( |
||||
<Box sx={{ display: isTabIdMatch ? 'initial' : 'none' }}> |
||||
{children} |
||||
</Box> |
||||
) : ( |
||||
isTabIdMatch && children |
||||
), |
||||
[children, isTabIdMatch, retain], |
||||
); |
||||
|
||||
return <Box sx={{ display: displayValue }}>{children}</Box>; |
||||
return <>{result}</>; |
||||
}; |
||||
|
||||
export default TabContent; |
||||
|
@ -0,0 +1,113 @@ |
||||
import { Button, styled } from '@mui/material'; |
||||
import { |
||||
createElement, |
||||
FC, |
||||
ReactElement, |
||||
ReactNode, |
||||
useCallback, |
||||
useMemo, |
||||
useState, |
||||
} from 'react'; |
||||
|
||||
import { BORDER_RADIUS, EERIE_BLACK } from '../../lib/consts/DEFAULT_THEME'; |
||||
|
||||
import BodyText from './BodyText'; |
||||
import FlexBox from '../FlexBox'; |
||||
import IconButton from '../IconButton'; |
||||
import MonoText from './MonoText'; |
||||
|
||||
const InlineButton = styled(Button)({ |
||||
backgroundColor: EERIE_BLACK, |
||||
borderRadius: BORDER_RADIUS, |
||||
minWidth: 'initial', |
||||
padding: '0 .6em', |
||||
textTransform: 'none', |
||||
|
||||
':hover': { |
||||
backgroundColor: `${EERIE_BLACK}F0`, |
||||
}, |
||||
}); |
||||
|
||||
const SensitiveText: FC<SensitiveTextProps> = ({ |
||||
children, |
||||
inline: isInline = false, |
||||
monospaced: isMonospaced = false, |
||||
revealInitially: isRevealInitially = false, |
||||
textProps, |
||||
}) => { |
||||
const [isReveal, setIsReveal] = useState<boolean>(isRevealInitially); |
||||
|
||||
const clickEventHandler = useCallback(() => { |
||||
setIsReveal((previous) => !previous); |
||||
}, []); |
||||
|
||||
const textSxLineHeight = useMemo<number | string | undefined>( |
||||
() => (isInline ? undefined : 2.8), |
||||
[isInline], |
||||
); |
||||
|
||||
const textElementType = useMemo( |
||||
() => (isMonospaced ? MonoText : BodyText), |
||||
[isMonospaced], |
||||
); |
||||
const contentElement = useMemo(() => { |
||||
let content: ReactNode; |
||||
|
||||
if (isReveal) { |
||||
content = |
||||
typeof children === 'string' |
||||
? createElement( |
||||
textElementType, |
||||
{ |
||||
sx: { |
||||
lineHeight: textSxLineHeight, |
||||
maxWidth: '20em', |
||||
overflowY: 'scroll', |
||||
whiteSpace: 'nowrap', |
||||
}, |
||||
...textProps, |
||||
}, |
||||
children, |
||||
) |
||||
: children; |
||||
} else { |
||||
content = createElement( |
||||
textElementType, |
||||
{ |
||||
sx: { |
||||
lineHeight: textSxLineHeight, |
||||
}, |
||||
...textProps, |
||||
}, |
||||
'*****', |
||||
); |
||||
} |
||||
|
||||
return content; |
||||
}, [children, isReveal, textElementType, textProps, textSxLineHeight]); |
||||
const rootElement = useMemo<ReactElement>( |
||||
() => |
||||
isInline ? ( |
||||
<InlineButton onClick={clickEventHandler}> |
||||
{contentElement} |
||||
</InlineButton> |
||||
) : ( |
||||
<FlexBox row spacing=".5em"> |
||||
{contentElement} |
||||
<IconButton |
||||
edge="end" |
||||
mapPreset="visibility" |
||||
onClick={clickEventHandler} |
||||
state={String(isReveal)} |
||||
sx={{ marginRight: '-.2em', padding: '.2em' }} |
||||
variant="normal" |
||||
/> |
||||
</FlexBox> |
||||
), |
||||
[clickEventHandler, contentElement, isInline, isReveal], |
||||
); |
||||
|
||||
return rootElement; |
||||
}; |
||||
|
||||
export default SensitiveText; |
@ -0,0 +1,43 @@ |
||||
type FenceParameterType = |
||||
| 'boolean' |
||||
| 'integer' |
||||
| 'second' |
||||
| 'select' |
||||
| 'string'; |
||||
|
||||
type FenceParameters = { |
||||
[parameterId: string]: string; |
||||
}; |
||||
|
||||
type APIFenceOverview = { |
||||
[fenceUUID: string]: { |
||||
fenceAgent: string; |
||||
fenceParameters: FenceParameters; |
||||
fenceName: string; |
||||
fenceUUID: string; |
||||
}; |
||||
}; |
||||
|
||||
type APIFenceTemplate = { |
||||
[fenceId: string]: { |
||||
actions: string[]; |
||||
description: string; |
||||
parameters: { |
||||
[parameterId: string]: { |
||||
content_type: FenceParameterType; |
||||
default?: string; |
||||
deprecated: number; |
||||
description: string; |
||||
obsoletes: number; |
||||
options?: string[]; |
||||
replacement: string; |
||||
required: '0' | '1'; |
||||
switches: string; |
||||
unique: '0' | '1'; |
||||
}; |
||||
}; |
||||
switch: { |
||||
[switchId: string]: { name: string }; |
||||
}; |
||||
}; |
||||
}; |
@ -0,0 +1,12 @@ |
||||
type FenceAutocompleteOption = { |
||||
fenceDescription: string; |
||||
fenceId: string; |
||||
label: string; |
||||
}; |
||||
|
||||
type AddFenceInputGroupOptionalProps = { |
||||
fenceTemplate?: APIFenceTemplate; |
||||
loading?: boolean; |
||||
}; |
||||
|
||||
type AddFenceInputGroupProps = AddFenceInputGroupOptionalProps; |
@ -0,0 +1,28 @@ |
||||
type FenceParameterInputBuilderParameters = { |
||||
id: string; |
||||
isChecked?: boolean; |
||||
isRequired?: boolean; |
||||
isSensitive?: boolean; |
||||
label?: string; |
||||
name?: string; |
||||
selectOptions?: string[]; |
||||
value?: string; |
||||
}; |
||||
|
||||
type FenceParameterInputBuilder = ( |
||||
args: FenceParameterInputBuilderParameters, |
||||
) => ReactElement; |
||||
|
||||
type MapToInputBuilder = Partial< |
||||
Record<Exclude<FenceParameterType, 'string'>, FenceParameterInputBuilder> |
||||
> & { string: FenceParameterInputBuilder }; |
||||
|
||||
type CommonFenceInputGroupOptionalProps = { |
||||
fenceId?: string; |
||||
fenceTemplate?: APIFenceTemplate; |
||||
previousFenceName?: string; |
||||
previousFenceParameters?: FenceParameters; |
||||
fenceParameterTooltipProps?: import('@mui/material').TooltipProps; |
||||
}; |
||||
|
||||
type CommonFenceInputGroupProps = CommonFenceInputGroupOptionalProps; |
@ -0,0 +1,12 @@ |
||||
type EditFenceInputGroupOptionalProps = { |
||||
fenceTemplate?: APIFenceTemplate; |
||||
loading?: boolean; |
||||
}; |
||||
|
||||
type EditFenceInputGroupProps = EditFenceInputGroupOptionalProps & |
||||
Required< |
||||
Pick< |
||||
CommonFenceInputGroupProps, |
||||
'fenceId' | 'previousFenceName' | 'previousFenceParameters' |
||||
> |
||||
>; |
@ -0,0 +1,10 @@ |
||||
type ExpandablePanelOptionalProps = { |
||||
expandInitially?: boolean; |
||||
loading?: boolean; |
||||
panelProps?: InnerPanelProps; |
||||
showHeaderSpinner?: boolean; |
||||
}; |
||||
|
||||
type ExpandablePanelProps = ExpandablePanelOptionalProps & { |
||||
header: import('react').ReactNode; |
||||
}; |
@ -0,0 +1,16 @@ |
||||
type CreatableComponent = Parameters<typeof import('react').createElement>[0]; |
||||
|
||||
type IconButtonPresetMapToStateIcon = 'edit' | 'visibility'; |
||||
|
||||
type IconButtonMapToStateIcon = Record<string, CreatableComponent>; |
||||
|
||||
type IconButtonVariant = 'contained' | 'normal'; |
||||
|
||||
type IconButtonOptionalProps = { |
||||
defaultIcon?: CreatableComponent; |
||||
iconProps?: import('@mui/material').SvgIconProps; |
||||
mapPreset?: IconButtonPresetMapToStateIcon; |
||||
mapToIcon?: IconButtonMapToStateIcon; |
||||
state?: string; |
||||
variant?: IconButtonVariant; |
||||
}; |
@ -0,0 +1 @@ |
||||
type InnerPanelProps = import('@mui/material').BoxProps; |
@ -0,0 +1,5 @@ |
||||
type SelectOptionalProps = { |
||||
onClearIndicatorClick?: import('@mui/material').IconButtonProps['onClick']; |
||||
}; |
||||
|
||||
type SelectProps = import('@mui/material').SelectProps & SelectOptionalProps; |
@ -0,0 +1,8 @@ |
||||
type SensitiveTextOptionalProps = { |
||||
inline?: boolean; |
||||
monospaced?: boolean; |
||||
revealInitially?: boolean; |
||||
textProps?: import('../components/Text').BodyTextProps; |
||||
}; |
||||
|
||||
type SensitiveTextProps = SensitiveTextOptionalProps; |
@ -0,0 +1,12 @@ |
||||
type SwitchWithLabelOptionalProps = { |
||||
flexBoxProps?: import('../components/FlexBox').FlexBoxProps; |
||||
switchProps?: import('@mui/material').SwitchProps; |
||||
}; |
||||
|
||||
type SwitchWithLabelProps = SwitchWithLabelOptionalProps & |
||||
Pick< |
||||
import('@mui/material').SwitchProps, |
||||
'checked' | 'id' | 'name' | 'onChange' |
||||
> & { |
||||
label: import('react').ReactNode; |
||||
}; |
@ -1,4 +1,9 @@ |
||||
type TabContentProps<T> = import('react').PropsWithChildren<{ |
||||
type TabContentOptionalProps = { |
||||
retain?: boolean; |
||||
}; |
||||
|
||||
type TabContentProps<T> = TabContentOptionalProps & |
||||
import('react').PropsWithChildren<{ |
||||
changingTabId: T; |
||||
tabId: T; |
||||
}>; |
||||
}>; |
||||
|
Loading…
Reference in new issue