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 { |
import { |
||||||
IconButton as MUIIconButton, |
IconButton as MUIIconButton, |
||||||
IconButtonProps as MUIIconButtonProps, |
IconButtonProps as MUIIconButtonProps, |
||||||
inputClasses as muiInputClasses, |
inputClasses as muiInputClasses, |
||||||
|
styled, |
||||||
} from '@mui/material'; |
} from '@mui/material'; |
||||||
|
import { createElement, FC, ReactNode, useMemo } from 'react'; |
||||||
|
|
||||||
import { |
import { |
||||||
BLACK, |
BLACK, |
||||||
BORDER_RADIUS, |
BORDER_RADIUS, |
||||||
DISABLED, |
DISABLED, |
||||||
GREY, |
GREY, |
||||||
TEXT, |
|
||||||
} from '../../lib/consts/DEFAULT_THEME'; |
} from '../../lib/consts/DEFAULT_THEME'; |
||||||
|
|
||||||
export type IconButtonProps = MUIIconButtonProps; |
type IconButtonProps = IconButtonOptionalProps & MUIIconButtonProps; |
||||||
|
|
||||||
|
const ContainedIconButton = styled(MUIIconButton)({ |
||||||
|
borderRadius: BORDER_RADIUS, |
||||||
|
backgroundColor: GREY, |
||||||
|
color: BLACK, |
||||||
|
|
||||||
|
'&:hover': { |
||||||
|
backgroundColor: `${GREY}F0`, |
||||||
|
}, |
||||||
|
|
||||||
|
[`&.${muiInputClasses.disabled}`]: { |
||||||
|
backgroundColor: DISABLED, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
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> = ({ |
const IconButton: FC<IconButtonProps> = ({ |
||||||
children, |
children, |
||||||
sx, |
defaultIcon, |
||||||
...iconButtonRestProps |
iconProps, |
||||||
}) => ( |
mapPreset, |
||||||
<MUIIconButton |
mapToIcon: externalMapToIcon, |
||||||
{...{ |
state, |
||||||
...iconButtonRestProps, |
variant = 'contained', |
||||||
sx: { |
...restIconButtonProps |
||||||
borderRadius: BORDER_RADIUS, |
}) => { |
||||||
backgroundColor: GREY, |
const mapToIcon = useMemo<IconButtonMapToStateIcon | undefined>( |
||||||
color: BLACK, |
() => externalMapToIcon ?? (mapPreset && MAP_TO_MAP_PRESET[mapPreset]), |
||||||
|
[externalMapToIcon, mapPreset], |
||||||
'&:hover': { |
); |
||||||
backgroundColor: TEXT, |
|
||||||
}, |
const iconButtonContent = useMemo(() => { |
||||||
|
let result: ReactNode; |
||||||
[`&.${muiInputClasses.disabled}`]: { |
|
||||||
backgroundColor: DISABLED, |
if (mapToIcon) { |
||||||
}, |
const iconElementType: CreatableComponent | undefined = state |
||||||
|
? mapToIcon[state] ?? defaultIcon |
||||||
...sx, |
: defaultIcon; |
||||||
}, |
|
||||||
}} |
if (iconElementType) { |
||||||
> |
result = createElement(iconElementType, iconProps); |
||||||
{children} |
} |
||||||
</MUIIconButton> |
} 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; |
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; |
@ -1,28 +1,26 @@ |
|||||||
import { FC } from 'react'; |
import { FC, useMemo } from 'react'; |
||||||
import { Box as MUIBox, BoxProps as MUIBoxProps } from '@mui/material'; |
import { Box as MUIBox, SxProps, Theme } from '@mui/material'; |
||||||
|
|
||||||
import { BORDER_RADIUS, DIVIDER } from '../../lib/consts/DEFAULT_THEME'; |
import { BORDER_RADIUS, DIVIDER } from '../../lib/consts/DEFAULT_THEME'; |
||||||
|
|
||||||
type InnerPanelProps = MUIBoxProps; |
const InnerPanel: FC<InnerPanelProps> = ({ sx, ...muiBoxRestProps }) => { |
||||||
|
const combinedSx = useMemo<SxProps<Theme>>( |
||||||
|
() => ({ |
||||||
|
borderWidth: '1px', |
||||||
|
borderRadius: BORDER_RADIUS, |
||||||
|
borderStyle: 'solid', |
||||||
|
borderColor: DIVIDER, |
||||||
|
marginTop: '1.4em', |
||||||
|
marginBottom: '1.4em', |
||||||
|
paddingBottom: 0, |
||||||
|
position: 'relative', |
||||||
|
|
||||||
const InnerPanel: FC<InnerPanelProps> = ({ sx, ...muiBoxRestProps }) => ( |
...sx, |
||||||
<MUIBox |
}), |
||||||
{...{ |
[sx], |
||||||
sx: { |
); |
||||||
borderWidth: '1px', |
|
||||||
borderRadius: BORDER_RADIUS, |
|
||||||
borderStyle: 'solid', |
|
||||||
borderColor: DIVIDER, |
|
||||||
marginTop: '1.4em', |
|
||||||
marginBottom: '1.4em', |
|
||||||
paddingBottom: 0, |
|
||||||
position: 'relative', |
|
||||||
|
|
||||||
...sx, |
return <MUIBox {...muiBoxRestProps} sx={combinedSx} />; |
||||||
}, |
}; |
||||||
...muiBoxRestProps, |
|
||||||
}} |
|
||||||
/> |
|
||||||
); |
|
||||||
|
|
||||||
export default InnerPanel; |
export default InnerPanel; |
||||||
|
@ -1,82 +1,70 @@ |
|||||||
import { FC } from 'react'; |
import { Close as CloseIcon } from '@mui/icons-material'; |
||||||
import { |
import { |
||||||
IconButton as MUIIconButton, |
IconButton as MUIIconButton, |
||||||
IconButtonProps as MUIIconButtonProps, |
|
||||||
iconButtonClasses as muiIconButtonClasses, |
iconButtonClasses as muiIconButtonClasses, |
||||||
inputClasses, |
inputClasses, |
||||||
Select as MUISelect, |
Select as MUISelect, |
||||||
selectClasses as muiSelectClasses, |
selectClasses as muiSelectClasses, |
||||||
SelectProps as MUISelectProps, |
|
||||||
InputAdornment as MUIInputAdornment, |
InputAdornment as MUIInputAdornment, |
||||||
inputAdornmentClasses as muiInputAdornmentClasses, |
inputAdornmentClasses as muiInputAdornmentClasses, |
||||||
} from '@mui/material'; |
} from '@mui/material'; |
||||||
import { Close as CloseIcon } from '@mui/icons-material'; |
import { FC, useMemo } from 'react'; |
||||||
|
|
||||||
import { GREY } from '../lib/consts/DEFAULT_THEME'; |
import { GREY } from '../lib/consts/DEFAULT_THEME'; |
||||||
|
|
||||||
type SelectOptionalProps = { |
const Select: FC<SelectProps> = ({ |
||||||
onClearIndicatorClick?: MUIIconButtonProps['onClick'] | null; |
onClearIndicatorClick, |
||||||
}; |
...muiSelectProps |
||||||
|
}) => { |
||||||
type SelectProps = MUISelectProps & SelectOptionalProps; |
const { sx: selectSx, value, ...restMuiSelectProps } = muiSelectProps; |
||||||
|
|
||||||
const SELECT_DEFAULT_PROPS: Required<SelectOptionalProps> = { |
const combinedSx = useMemo( |
||||||
onClearIndicatorClick: null, |
() => ({ |
||||||
}; |
[`& .${muiSelectClasses.icon}`]: { |
||||||
|
color: GREY, |
||||||
const Select: FC<SelectProps> = (selectProps) => { |
}, |
||||||
const { |
|
||||||
onClearIndicatorClick = SELECT_DEFAULT_PROPS.onClearIndicatorClick, |
|
||||||
...muiSelectProps |
|
||||||
} = selectProps; |
|
||||||
const { children, sx, value } = muiSelectProps; |
|
||||||
const clearIndicator: JSX.Element | undefined = |
|
||||||
String(value).length > 0 && onClearIndicatorClick ? ( |
|
||||||
<MUIInputAdornment position="end"> |
|
||||||
<MUIIconButton onClick={onClearIndicatorClick}> |
|
||||||
<CloseIcon fontSize="small" /> |
|
||||||
</MUIIconButton> |
|
||||||
</MUIInputAdornment> |
|
||||||
) : undefined; |
|
||||||
|
|
||||||
const combinedSx = { |
|
||||||
[`& .${muiSelectClasses.icon}`]: { |
|
||||||
color: GREY, |
|
||||||
}, |
|
||||||
|
|
||||||
[`& .${muiInputAdornmentClasses.root}`]: { |
[`& .${muiInputAdornmentClasses.root}`]: { |
||||||
marginRight: '.8em', |
marginRight: '.8em', |
||||||
}, |
}, |
||||||
|
|
||||||
[`& .${muiIconButtonClasses.root}`]: { |
[`& .${muiIconButtonClasses.root}`]: { |
||||||
color: GREY, |
color: GREY, |
||||||
visibility: 'hidden', |
visibility: 'hidden', |
||||||
}, |
}, |
||||||
|
|
||||||
[`&:hover .${muiInputAdornmentClasses.root} .${muiIconButtonClasses.root},
|
[`&:hover .${muiInputAdornmentClasses.root} .${muiIconButtonClasses.root},
|
||||||
&.${inputClasses.focused} .${muiInputAdornmentClasses.root} .${muiIconButtonClasses.root}`]:
|
&.${inputClasses.focused} .${muiInputAdornmentClasses.root} .${muiIconButtonClasses.root}`]:
|
||||||
{ |
{ |
||||||
visibility: 'visible', |
visibility: 'visible', |
||||||
}, |
}, |
||||||
|
|
||||||
|
...selectSx, |
||||||
|
}), |
||||||
|
[selectSx], |
||||||
|
); |
||||||
|
|
||||||
...sx, |
const clearIndicatorElement = useMemo( |
||||||
}; |
() => |
||||||
|
String(value).length > 0 && |
||||||
|
onClearIndicatorClick && ( |
||||||
|
<MUIInputAdornment position="end"> |
||||||
|
<MUIIconButton onClick={onClearIndicatorClick}> |
||||||
|
<CloseIcon fontSize="small" /> |
||||||
|
</MUIIconButton> |
||||||
|
</MUIInputAdornment> |
||||||
|
), |
||||||
|
[onClearIndicatorClick, value], |
||||||
|
); |
||||||
|
|
||||||
return ( |
return ( |
||||||
<MUISelect |
<MUISelect |
||||||
{...{ |
endAdornment={clearIndicatorElement} |
||||||
endAdornment: clearIndicator, |
value={value} |
||||||
...muiSelectProps, |
{...restMuiSelectProps} |
||||||
sx: combinedSx, |
sx={combinedSx} |
||||||
}} |
/> |
||||||
> |
|
||||||
{children} |
|
||||||
</MUISelect> |
|
||||||
); |
); |
||||||
}; |
}; |
||||||
|
|
||||||
Select.defaultProps = SELECT_DEFAULT_PROPS; |
|
||||||
|
|
||||||
export type { SelectProps }; |
|
||||||
|
|
||||||
export default Select; |
export default Select; |
||||||
|
@ -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 { Box } from '@mui/material'; |
||||||
import { ReactElement, useMemo } from 'react'; |
import { ReactElement, ReactNode, useMemo } from 'react'; |
||||||
|
|
||||||
const TabContent = <T,>({ |
const TabContent = <T,>({ |
||||||
changingTabId, |
changingTabId, |
||||||
children, |
children, |
||||||
|
retain = false, |
||||||
tabId, |
tabId, |
||||||
}: TabContentProps<T>): ReactElement => { |
}: TabContentProps<T>): ReactElement => { |
||||||
const isTabIdMatch = useMemo( |
const isTabIdMatch = useMemo( |
||||||
() => changingTabId === tabId, |
() => changingTabId === tabId, |
||||||
[changingTabId, tabId], |
[changingTabId, tabId], |
||||||
); |
); |
||||||
const displayValue = useMemo( |
const result = useMemo<ReactNode>( |
||||||
() => (isTabIdMatch ? 'initial' : 'none'), |
() => |
||||||
[isTabIdMatch], |
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; |
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 = { |
||||||
changingTabId: T; |
retain?: boolean; |
||||||
tabId: T; |
}; |
||||||
}>; |
|
||||||
|
type TabContentProps<T> = TabContentOptionalProps & |
||||||
|
import('react').PropsWithChildren<{ |
||||||
|
changingTabId: T; |
||||||
|
tabId: T; |
||||||
|
}>; |
||||||
|
Loading…
Reference in new issue