Merge pull request #317 from ylei-tsubame/manage-fence-device

Add fence device management tab
main
Digimer 2 years ago committed by GitHub
commit 941221fa41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 56
      striker-ui-api/src/lib/request_handlers/fence/getFence.ts
  2. 22
      striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts
  3. 2
      striker-ui-api/src/lib/request_handlers/fence/index.ts
  4. 9
      striker-ui-api/src/routes/fence.ts
  5. 2
      striker-ui-api/src/routes/index.ts
  6. 10
      striker-ui-api/src/types/APIFence.d.ts
  7. 129
      striker-ui/components/ConfirmDialog.tsx
  8. 16
      striker-ui/components/ContainedButton.tsx
  9. 17
      striker-ui/components/FlexBox.tsx
  10. 122
      striker-ui/components/IconButton/IconButton.tsx
  11. 77
      striker-ui/components/InputWithRef.tsx
  12. 116
      striker-ui/components/List.tsx
  13. 108
      striker-ui/components/ManageFence/AddFenceInputGroup.tsx
  14. 246
      striker-ui/components/ManageFence/CommonFenceInputGroup.tsx
  15. 37
      striker-ui/components/ManageFence/EditFenceInputGroup.tsx
  16. 356
      striker-ui/components/ManageFence/ManageFencePanel.tsx
  17. 3
      striker-ui/components/ManageFence/index.tsx
  18. 19
      striker-ui/components/OutlinedInputWithLabel.tsx
  19. 18
      striker-ui/components/OutlinedLabeledInputWithSelect.tsx
  20. 37
      striker-ui/components/Panels/ExpandablePanel.tsx
  21. 40
      striker-ui/components/Panels/InnerPanel.tsx
  22. 102
      striker-ui/components/Select.tsx
  23. 179
      striker-ui/components/SelectWithLabel.tsx
  24. 5
      striker-ui/components/StrikerConfig/ConfigPeersForm.tsx
  25. 5
      striker-ui/components/StrikerConfig/ManageChangedSSHKeysForm.tsx
  26. 5
      striker-ui/components/StrikerConfig/ManageUsersForm.tsx
  27. 56
      striker-ui/components/SwitchWithLabel.tsx
  28. 18
      striker-ui/components/TabContent.tsx
  29. 54
      striker-ui/components/Text/BodyText.tsx
  30. 9
      striker-ui/components/Text/InlineMonoText.tsx
  31. 6
      striker-ui/components/Text/MonoText.tsx
  32. 113
      striker-ui/components/Text/SensitiveText.tsx
  33. 6
      striker-ui/components/Text/SmallText.tsx
  34. 10
      striker-ui/components/Text/index.tsx
  35. 1
      striker-ui/lib/consts/DEFAULT_THEME.ts
  36. 7
      striker-ui/lib/createInputOnChangeHandler.ts
  37. 26
      striker-ui/pages/manage-element/index.tsx
  38. 43
      striker-ui/types/APIFence.d.ts
  39. 12
      striker-ui/types/AddFenceInputGroup.d.ts
  40. 28
      striker-ui/types/CommonFenceInputGroup.d.ts
  41. 9
      striker-ui/types/ConfirmDialog.d.ts
  42. 6
      striker-ui/types/CreateInputOnChangeHandlerFunction.d.ts
  43. 12
      striker-ui/types/EditFenceInputGroup.d.ts
  44. 10
      striker-ui/types/ExpandablePanel.d.ts
  45. 16
      striker-ui/types/IconButton.d.ts
  46. 1
      striker-ui/types/InnerPanel.d.ts
  47. 23
      striker-ui/types/List.d.ts
  48. 5
      striker-ui/types/Select.d.ts
  49. 23
      striker-ui/types/SelectWithLabel.d.ts
  50. 8
      striker-ui/types/SensitiveText.d.ts
  51. 12
      striker-ui/types/SwitchWithLabel.d.ts
  52. 13
      striker-ui/types/TabContent.d.ts

@ -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;

@ -3,6 +3,7 @@ import { Router } from 'express';
import anvilRouter from './anvil';
import commandRouter from './command';
import echoRouter from './echo';
import fenceRouter from './fence';
import fileRouter from './file';
import hostRouter from './host';
import jobRouter from './job';
@ -15,6 +16,7 @@ const routes: Readonly<Record<string, Router>> = {
anvil: anvilRouter,
command: commandRouter,
echo: echoRouter,
fence: fenceRouter,
file: fileRouter,
host: hostRouter,
job: jobRouter,

@ -0,0 +1,10 @@
type FenceParameters = {
[parameterId: string]: string;
};
type FenceOverview = {
fenceAgent: string;
fenceParameters: FenceParameters;
fenceName: string;
fenceUUID: string;
};

@ -1,5 +1,14 @@
import { Box, Dialog } from '@mui/material';
import { forwardRef, useImperativeHandle, useMemo, useState } from 'react';
import { Box, Dialog as MUIDialog, SxProps, Theme } from '@mui/material';
import {
ButtonHTMLAttributes,
ElementType,
FormEventHandler,
forwardRef,
MouseEventHandler,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import { BLUE, RED, TEXT } from '../lib/consts/DEFAULT_THEME';
@ -25,6 +34,7 @@ const ConfirmDialog = forwardRef<
{
actionCancelText = 'Cancel',
actionProceedText,
contentContainerProps = {},
closeOnProceed: isCloseOnProceed = false,
content,
dialogProps: {
@ -32,13 +42,17 @@ const ConfirmDialog = forwardRef<
PaperProps: paperProps = {},
...restDialogProps
} = {},
formContent: isFormContent,
loadingAction: isLoadingAction = false,
onActionAppend,
onCancelAppend,
onProceedAppend,
onSubmitAppend,
openInitially = false,
proceedButtonProps = {},
proceedColour: proceedColourKey = 'blue',
scrollContent: isScrollContent = false,
scrollBoxProps: { sx: scrollBoxSx, ...restScrollBoxProps } = {},
titleText,
},
ref,
@ -59,6 +73,54 @@ const ConfirmDialog = forwardRef<
() => MAP_TO_COLOUR[proceedColourKey],
[proceedColourKey],
);
const {
contentContainerComponent,
contentContainerSubmitEventHandler,
proceedButtonClickEventHandler,
proceedButtonType,
} = useMemo(() => {
let ccComponent: ElementType | undefined;
let ccSubmitEventHandler: FormEventHandler<HTMLDivElement> | undefined;
let pbClickEventHandler:
| MouseEventHandler<HTMLButtonElement>
| undefined = (...args) => {
if (isCloseOnProceed) {
setIsOpen(false);
}
onActionAppend?.call(null, ...args);
onProceedAppend?.call(null, ...args);
};
let pbType: ButtonHTMLAttributes<HTMLButtonElement>['type'] | undefined;
if (isFormContent) {
ccComponent = 'form';
ccSubmitEventHandler = (event, ...restArgs) => {
event.preventDefault();
if (isCloseOnProceed) {
setIsOpen(false);
}
onSubmitAppend?.call(null, event, ...restArgs);
};
pbClickEventHandler = undefined;
pbType = 'submit';
}
return {
contentContainerComponent: ccComponent,
contentContainerSubmitEventHandler: ccSubmitEventHandler,
proceedButtonClickEventHandler: pbClickEventHandler,
proceedButtonType: pbType,
};
}, [
isCloseOnProceed,
isFormContent,
onActionAppend,
onProceedAppend,
onSubmitAppend,
]);
const cancelButtonElement = useMemo(
() => (
@ -78,14 +140,8 @@ const ConfirmDialog = forwardRef<
const proceedButtonElement = useMemo(
() => (
<ContainedButton
onClick={(...args) => {
if (isCloseOnProceed) {
setIsOpen(false);
}
onActionAppend?.call(null, ...args);
onProceedAppend?.call(null, ...args);
}}
onClick={proceedButtonClickEventHandler}
type={proceedButtonType}
{...restProceedButtonProps}
sx={{
backgroundColor: proceedColour,
@ -101,14 +157,14 @@ const ConfirmDialog = forwardRef<
),
[
actionProceedText,
isCloseOnProceed,
onActionAppend,
onProceedAppend,
proceedButtonClickEventHandler,
proceedButtonSx,
proceedButtonType,
proceedColour,
restProceedButtonProps,
],
);
const actionAreaElement = useMemo(
() =>
isLoadingAction ? (
@ -125,6 +181,31 @@ const ConfirmDialog = forwardRef<
),
[cancelButtonElement, isLoadingAction, proceedButtonElement],
);
const contentElement = useMemo(
() =>
typeof content === 'string' ? <BodyText text={content} /> : content,
[content],
);
const headerElement = useMemo(
() =>
typeof titleText === 'string' ? (
<HeaderText>{titleText}</HeaderText>
) : (
titleText
),
[titleText],
);
const combinedScrollBoxSx = useMemo<SxProps<Theme> | undefined>(
() =>
isScrollContent
? {
maxHeight: '60vh',
overflowY: 'scroll',
...scrollBoxSx,
}
: undefined,
[isScrollContent, scrollBoxSx],
);
useImperativeHandle(
ref,
@ -135,7 +216,7 @@ const ConfirmDialog = forwardRef<
);
return (
<Dialog
<MUIDialog
open={open}
PaperComponent={Panel}
PaperProps={{
@ -144,14 +225,18 @@ const ConfirmDialog = forwardRef<
}}
{...restDialogProps}
>
<PanelHeader>
<HeaderText text={titleText} />
</PanelHeader>
<Box sx={{ marginBottom: '1em' }}>
{typeof content === 'string' ? <BodyText text={content} /> : content}
</Box>
{actionAreaElement}
</Dialog>
<PanelHeader>{headerElement}</PanelHeader>
<FlexBox
component={contentContainerComponent}
onSubmit={contentContainerSubmitEventHandler}
{...contentContainerProps}
>
<Box {...restScrollBoxProps} sx={combinedScrollBoxSx}>
{contentElement}
</Box>
{actionAreaElement}
</FlexBox>
</MUIDialog>
);
},
);

@ -1,17 +1,17 @@
import { Button as MUIButton, SxProps, Theme } from '@mui/material';
import { FC, useMemo } from 'react';
import { BLACK, GREY, TEXT } from '../lib/consts/DEFAULT_THEME';
import { BLACK, GREY } from '../lib/consts/DEFAULT_THEME';
const ContainedButton: FC<ContainedButtonProps> = ({ sx, ...restProps }) => {
const combinedSx = useMemo<SxProps<Theme>>(
() => ({
backgroundColor: TEXT,
backgroundColor: GREY,
color: BLACK,
textTransform: 'none',
'&:hover': {
backgroundColor: GREY,
backgroundColor: `${GREY}F0`,
},
...sx,
@ -19,15 +19,7 @@ const ContainedButton: FC<ContainedButtonProps> = ({ sx, ...restProps }) => {
[sx],
);
return (
<MUIButton
{...{
variant: 'contained',
...restProps,
sx: combinedSx,
}}
/>
);
return <MUIButton variant="contained" {...restProps} sx={combinedSx} />;
};
export default ContainedButton;

@ -6,6 +6,8 @@ type FlexBoxDirection = 'column' | 'row';
type FlexBoxSpacing = number | string;
type FlexBoxOptionalPropsWithDefault = {
fullWidth?: boolean;
growFirst?: boolean;
row?: boolean;
spacing?: FlexBoxSpacing;
xs?: FlexBoxDirection;
@ -28,6 +30,8 @@ type FlexBoxProps = MUIBoxProps & FlexBoxOptionalProps;
const FLEX_BOX_DEFAULT_PROPS: Required<FlexBoxOptionalPropsWithDefault> &
FlexBoxOptionalPropsWithoutDefault = {
columnSpacing: undefined,
fullWidth: false,
growFirst: false,
row: false,
rowSpacing: undefined,
lg: undefined,
@ -39,6 +43,8 @@ const FLEX_BOX_DEFAULT_PROPS: Required<FlexBoxOptionalPropsWithDefault> &
};
const FlexBox: FC<FlexBoxProps> = ({
fullWidth,
growFirst,
lg: dLg = FLEX_BOX_DEFAULT_PROPS.lg,
md: dMd = FLEX_BOX_DEFAULT_PROPS.md,
row: isRow,
@ -50,7 +56,6 @@ const FlexBox: FC<FlexBoxProps> = ({
// Input props that depend on other input props.
columnSpacing = spacing,
rowSpacing = spacing,
...muiBoxRestProps
}) => {
const xs = useMemo(() => (isRow ? 'row' : dXs), [dXs, isRow]);
@ -81,6 +86,11 @@ const FlexBox: FC<FlexBoxProps> = ({
}),
[columnSpacing, rowSpacing],
);
const firstChildFlexGrow = useMemo(
() => (growFirst ? 1 : undefined),
[growFirst],
);
const width = useMemo(() => (fullWidth ? '100%' : undefined), [fullWidth]);
return (
<MUIBox
@ -96,6 +106,11 @@ const FlexBox: FC<FlexBoxProps> = ({
},
display: 'flex',
flexDirection: { xs, sm, md, lg, xl },
width,
'& > :first-child': {
flexGrow: firstChildFlexGrow,
},
'& > :not(:first-child)': {
marginLeft: {

@ -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 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> = ({
children,
sx,
...iconButtonRestProps
}) => (
<MUIIconButton
{...{
...iconButtonRestProps,
sx: {
borderRadius: BORDER_RADIUS,
backgroundColor: GREY,
color: BLACK,
'&:hover': {
backgroundColor: TEXT,
},
[`&.${muiInputClasses.disabled}`]: {
backgroundColor: DISABLED,
},
...sx,
},
}}
>
{children}
</MUIIconButton>
);
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;

@ -22,14 +22,17 @@ type InputWithRefOptionalPropsWithDefault<
required?: boolean;
valueType?: TypeName;
};
type InputWithRefOptionalPropsWithoutDefault = {
type InputWithRefOptionalPropsWithoutDefault<
TypeName extends keyof MapToInputType,
> = {
inputTestBatch?: InputTestBatch;
onFirstRender?: (args: { isRequired: boolean }) => void;
valueKey?: CreateInputOnChangeHandlerOptions<TypeName>['valueKey'];
};
type InputWithRefOptionalProps<TypeName extends keyof MapToInputType> =
InputWithRefOptionalPropsWithDefault<TypeName> &
InputWithRefOptionalPropsWithoutDefault;
InputWithRefOptionalPropsWithoutDefault<TypeName>;
type InputWithRefProps<
TypeName extends keyof MapToInputType,
@ -47,6 +50,7 @@ type InputForwardedRefContent<TypeName extends keyof MapToInputType> = {
const INPUT_TEST_ID = 'input';
const MAP_TO_INITIAL_VALUE: MapToInputType = {
boolean: false,
number: 0,
string: '',
};
@ -54,7 +58,7 @@ const MAP_TO_INITIAL_VALUE: MapToInputType = {
const INPUT_WITH_REF_DEFAULT_PROPS: Required<
InputWithRefOptionalPropsWithDefault<'string'>
> &
InputWithRefOptionalPropsWithoutDefault = {
InputWithRefOptionalPropsWithoutDefault<'string'> = {
createInputOnChangeHandlerOptions: {},
required: false,
valueType: 'string',
@ -63,27 +67,35 @@ const INPUT_WITH_REF_DEFAULT_PROPS: Required<
const InputWithRef = forwardRef(
<TypeName extends keyof MapToInputType, InputComponent extends ReactElement>(
{
createInputOnChangeHandlerOptions: {
postSet: postSetAppend,
...restCreateInputOnChangeHandlerOptions
} = INPUT_WITH_REF_DEFAULT_PROPS.createInputOnChangeHandlerOptions as CreateInputOnChangeHandlerOptions<TypeName>,
input,
inputTestBatch,
onFirstRender,
required: isRequired = INPUT_WITH_REF_DEFAULT_PROPS.required,
valueKey,
valueType = INPUT_WITH_REF_DEFAULT_PROPS.valueType as TypeName,
// Props with initial value that depend on others.
createInputOnChangeHandlerOptions: {
postSet: postSetAppend,
valueKey: onChangeValueKey = valueKey,
...restCreateInputOnChangeHandlerOptions
} = INPUT_WITH_REF_DEFAULT_PROPS.createInputOnChangeHandlerOptions as CreateInputOnChangeHandlerOptions<TypeName>,
}: InputWithRefProps<TypeName, InputComponent>,
ref: ForwardedRef<InputForwardedRefContent<TypeName>>,
) => {
const { props: inputProps } = input;
const vKey = useMemo(
() => onChangeValueKey ?? ('checked' in inputProps ? 'checked' : 'value'),
[inputProps, onChangeValueKey],
);
const {
props: {
onBlur: initOnBlur,
onChange: initOnChange,
onFocus: initOnFocus,
value: initValue = MAP_TO_INITIAL_VALUE[valueType],
...restInitProps
},
} = input;
onBlur: initOnBlur,
onChange: initOnChange,
onFocus: initOnFocus,
[vKey]: initValue = MAP_TO_INITIAL_VALUE[valueType],
...restInitProps
} = inputProps;
const isFirstRender = useIsFirstRender();
@ -123,6 +135,28 @@ const InputWithRef = forwardRef(
})),
[initOnBlur, testInput],
);
const onChange = useMemo(
() =>
createInputOnChangeHandler<TypeName>({
postSet: (...args) => {
setIsChangedByUser(true);
initOnChange?.call(null, ...args);
postSetAppend?.call(null, ...args);
},
set: setValue,
setType: valueType,
valueKey: vKey,
...restCreateInputOnChangeHandlerOptions,
}),
[
initOnChange,
postSetAppend,
restCreateInputOnChangeHandlerOptions,
setValue,
vKey,
valueType,
],
);
const onFocus = useMemo<InputBaseProps['onFocus']>(
() =>
initOnFocus ??
@ -133,17 +167,6 @@ const InputWithRef = forwardRef(
[initOnFocus, inputTestBatch],
);
const onChange = createInputOnChangeHandler<TypeName>({
postSet: (...args) => {
setIsChangedByUser(true);
initOnChange?.call(null, ...args);
postSetAppend?.call(null, ...args);
},
set: setValue,
setType: valueType,
...restCreateInputOnChangeHandlerOptions,
});
useEffect(() => {
if (isFirstRender) {
onFirstRender?.call(null, { isRequired });
@ -167,7 +190,7 @@ const InputWithRef = forwardRef(
onChange,
onFocus,
required: isRequired,
value: inputValue,
[vKey]: inputValue,
});
},
);

@ -8,6 +8,7 @@ import {
Box as MUIBox,
List as MUIList,
ListItem as MUIListItem,
ListItemButton,
ListItemIcon as MUIListItemIcon,
SxProps,
Theme,
@ -23,7 +24,7 @@ import {
} from 'react';
import { v4 as uuidv4 } from 'uuid';
import { BLUE, GREY, RED } from '../lib/consts/DEFAULT_THEME';
import { BLUE, BORDER_RADIUS, GREY, RED } from '../lib/consts/DEFAULT_THEME';
import Checkbox from './Checkbox';
import Divider from './Divider';
@ -36,6 +37,7 @@ const List = forwardRef(
{
allowCheckAll: isAllowCheckAll = false,
allowEdit: isAllowEdit = false,
allowItemButton: isAllowItemButton = false,
edit: isEdit = false,
flexBoxProps,
header,
@ -53,6 +55,7 @@ const List = forwardRef(
onEdit,
onAllCheckboxChange,
onItemCheckboxChange,
onItemClick,
renderListItem = (key) => <BodyText>{key}</BodyText>,
renderListItemCheckboxState,
scroll: isScroll = false,
@ -140,36 +143,36 @@ const List = forwardRef(
isEdit,
onAllCheckboxChange,
]);
const headerElement = useMemo(
() =>
isInsertHeader && header ? (
<FlexBox row spacing={headerSpacing} sx={{ height: '2.4em' }}>
{checkAllElement}
{typeof header === 'string' ? (
<>
<BodyText>{header}</BodyText>
<Divider sx={{ flexGrow: 1 }} />
</>
) : (
header
)}
{deleteItemButton}
{editItemButton}
{addItemButton}
</FlexBox>
) : (
header
),
[
addItemButton,
checkAllElement,
deleteItemButton,
editItemButton,
header,
headerSpacing,
isInsertHeader,
],
);
const headerElement = useMemo(() => {
const headerType = typeof header;
return isInsertHeader && header ? (
<FlexBox row spacing={headerSpacing} sx={{ height: '2.4em' }}>
{checkAllElement}
{['boolean', 'string'].includes(headerType) ? (
<>
{headerType === 'string' && <BodyText>{header}</BodyText>}
<Divider sx={{ flexGrow: 1 }} />
</>
) : (
header
)}
{deleteItemButton}
{editItemButton}
{addItemButton}
</FlexBox>
) : (
header
);
}, [
addItemButton,
checkAllElement,
deleteItemButton,
editItemButton,
header,
headerSpacing,
isInsertHeader,
]);
const listEmptyElement = useMemo(
() =>
typeof listEmpty === 'string' ? (
@ -203,32 +206,49 @@ const List = forwardRef(
const entries = Object.entries(listItems);
if (entries.length > 0) {
result = entries.map(([key, value]) => (
<MUIListItem
{...restListItemProps}
key={`${listItemKeyPrefix}-${key}`}
sx={{ paddingLeft: 0, paddingRight: 0, ...listItemSx }}
>
{listItemCheckbox(
key,
renderListItemCheckboxState?.call(null, key, value),
)}
{renderListItem(key, value)}
</MUIListItem>
));
result = entries.map(([key, value]) => {
const listItem = renderListItem(key, value);
return (
<MUIListItem
{...restListItemProps}
key={`${listItemKeyPrefix}-${key}`}
sx={{ paddingLeft: 0, paddingRight: 0, ...listItemSx }}
>
{listItemCheckbox(
key,
renderListItemCheckboxState?.call(null, key, value),
)}
{isAllowItemButton ? (
<ListItemButton
onClick={(...args) => {
onItemClick?.call(null, value, key, ...args);
}}
sx={{ borderRadius: BORDER_RADIUS }}
>
{listItem}
</ListItemButton>
) : (
listItem
)}
</MUIListItem>
);
});
}
}
return result;
}, [
listEmptyElement,
listItemCheckbox,
listItemKeyPrefix,
listItems,
listItemSx,
renderListItem,
renderListItemCheckboxState,
restListItemProps,
listItemKeyPrefix,
listItemSx,
listItemCheckbox,
renderListItemCheckboxState,
isAllowItemButton,
onItemClick,
]);
const listScrollSx: SxProps<Theme> | undefined = useMemo(
() => (isScroll ? { maxHeight: '100%', overflowY: 'scroll' } : undefined),

@ -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}=&quot;</>;
current = /passw/i.test(parameterId) ? (
<>
{current}
<SensitiveText inline>{parameterValue}</SensitiveText>
</>
) : (
<>
{current}
{parameterValue}
</>
);
return (
<>
{previous} {current}&quot;
</>
);
},
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;

@ -31,9 +31,6 @@ type OutlinedInputWithLabelOptionalPropsWithDefault = {
};
type OutlinedInputWithLabelOptionalPropsWithoutDefault = {
onBlur?: OutlinedInputProps['onBlur'];
onChange?: OutlinedInputProps['onChange'];
onFocus?: OutlinedInputProps['onFocus'];
onHelp?: MUIIconButtonProps['onClick'];
onHelpAppend?: MUIIconButtonProps['onClick'];
type?: string;
@ -43,9 +40,13 @@ type OutlinedInputWithLabelOptionalProps =
OutlinedInputWithLabelOptionalPropsWithDefault &
OutlinedInputWithLabelOptionalPropsWithoutDefault;
type OutlinedInputWithLabelProps = OutlinedInputWithLabelOptionalProps & {
label: string;
};
type OutlinedInputWithLabelProps = Pick<
OutlinedInputProps,
'name' | 'onBlur' | 'onChange' | 'onFocus'
> &
OutlinedInputWithLabelOptionalProps & {
label: string;
};
const OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS: Required<OutlinedInputWithLabelOptionalPropsWithDefault> &
OutlinedInputWithLabelOptionalPropsWithoutDefault = {
@ -56,9 +57,6 @@ const OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS: Required<OutlinedInputWithLabelOp
inputProps: {},
inputLabelProps: {},
messageBoxProps: {},
onBlur: undefined,
onChange: undefined,
onFocus: undefined,
onHelp: undefined,
onHelpAppend: undefined,
required: false,
@ -78,6 +76,7 @@ const OutlinedInputWithLabel: FC<OutlinedInputWithLabelProps> = ({
inputLabelProps = OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS.inputLabelProps,
label,
messageBoxProps = OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS.messageBoxProps,
name,
onBlur,
onChange,
onFocus,
@ -133,6 +132,7 @@ const OutlinedInputWithLabel: FC<OutlinedInputWithLabelProps> = ({
return (
<MUIFormControl
fullWidth
{...restFormControlProps}
sx={{ width: formControlWidth, ...formControlSx }}
>
@ -172,6 +172,7 @@ const OutlinedInputWithLabel: FC<OutlinedInputWithLabelProps> = ({
fullWidth={formControlProps.fullWidth}
id={id}
label={label}
name={name}
onBlur={onBlur}
onChange={onChange}
onFocus={onFocus}

@ -12,7 +12,7 @@ import { MessageBoxProps } from './MessageBox';
import OutlinedInputWithLabel, {
OutlinedInputWithLabelProps,
} from './OutlinedInputWithLabel';
import SelectWithLabel, { SelectWithLabelProps } from './SelectWithLabel';
import SelectWithLabel from './SelectWithLabel';
type OutlinedLabeledInputWithSelectOptionalProps = {
inputWithLabelProps?: Partial<OutlinedInputWithLabelProps>;
@ -66,19 +66,11 @@ const OutlinedLabeledInputWithSelect: FC<
},
}}
>
<OutlinedInputWithLabel
{...{
id,
label,
...inputWithLabelProps,
}}
/>
<OutlinedInputWithLabel id={id} label={label} {...inputWithLabelProps} />
<SelectWithLabel
{...{
id: `${id}-nested-select`,
selectItems,
...selectWithLabelProps,
}}
id={`${id}-nested-select`}
selectItems={selectItems}
{...selectWithLabelProps}
/>
</Box>
<InputMessageBox {...messageBoxProps} />

@ -3,7 +3,7 @@ import {
ExpandMore as ExpandMoreIcon,
} from '@mui/icons-material';
import { Box, IconButton } from '@mui/material';
import { FC, ReactNode, useMemo, useState } from 'react';
import { FC, useMemo, useState } from 'react';
import { GREY } from '../../lib/consts/DEFAULT_THEME';
@ -12,32 +12,17 @@ import InnerPanel from './InnerPanel';
import InnerPanelBody from './InnerPanelBody';
import InnerPanelHeader from './InnerPanelHeader';
import Spinner from '../Spinner';
import { BodyText } from '../Text';
type ExpandablePanelOptionalProps = {
expandInitially?: boolean;
loading?: boolean;
showHeaderSpinner?: boolean;
};
type ExpandablePanelProps = ExpandablePanelOptionalProps & {
header: ReactNode;
};
const EXPANDABLE_PANEL_DEFAULT_PROPS: Required<ExpandablePanelOptionalProps> = {
expandInitially: false,
loading: false,
showHeaderSpinner: false,
};
const HEADER_SPINNER_LENGTH = '1.2em';
const ExpandablePanel: FC<ExpandablePanelProps> = ({
children,
expandInitially:
isExpandInitially = EXPANDABLE_PANEL_DEFAULT_PROPS.expandInitially,
expandInitially: isExpandInitially = false,
header,
loading: isLoading = EXPANDABLE_PANEL_DEFAULT_PROPS.loading,
showHeaderSpinner:
isShowHeaderSpinner = EXPANDABLE_PANEL_DEFAULT_PROPS.showHeaderSpinner,
loading: isLoading = false,
panelProps,
showHeaderSpinner: isShowHeaderSpinner = false,
}) => {
const [isExpand, setIsExpand] = useState<boolean>(isExpandInitially);
@ -46,6 +31,10 @@ const ExpandablePanel: FC<ExpandablePanelProps> = ({
[isExpand],
);
const contentHeight = useMemo(() => (isExpand ? 'auto' : '.2em'), [isExpand]);
const headerElement = useMemo(
() => (typeof header === 'string' ? <BodyText>{header}</BodyText> : header),
[header],
);
const headerSpinner = useMemo(
() =>
isShowHeaderSpinner && !isExpand && isLoading ? (
@ -71,10 +60,10 @@ const ExpandablePanel: FC<ExpandablePanelProps> = ({
);
return (
<InnerPanel>
<InnerPanel {...panelProps}>
<InnerPanelHeader>
<FlexBox row>
{header}
{headerElement}
{headerSpinner}
</FlexBox>
<IconButton
@ -91,6 +80,4 @@ const ExpandablePanel: FC<ExpandablePanelProps> = ({
);
};
ExpandablePanel.defaultProps = EXPANDABLE_PANEL_DEFAULT_PROPS;
export default ExpandablePanel;

@ -1,28 +1,26 @@
import { FC } from 'react';
import { Box as MUIBox, BoxProps as MUIBoxProps } from '@mui/material';
import { FC, useMemo } from 'react';
import { Box as MUIBox, SxProps, Theme } from '@mui/material';
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 }) => (
<MUIBox
{...{
sx: {
borderWidth: '1px',
borderRadius: BORDER_RADIUS,
borderStyle: 'solid',
borderColor: DIVIDER,
marginTop: '1.4em',
marginBottom: '1.4em',
paddingBottom: 0,
position: 'relative',
...sx,
}),
[sx],
);
...sx,
},
...muiBoxRestProps,
}}
/>
);
return <MUIBox {...muiBoxRestProps} sx={combinedSx} />;
};
export default InnerPanel;

@ -1,82 +1,70 @@
import { FC } from 'react';
import { Close as CloseIcon } from '@mui/icons-material';
import {
IconButton as MUIIconButton,
IconButtonProps as MUIIconButtonProps,
iconButtonClasses as muiIconButtonClasses,
inputClasses,
Select as MUISelect,
selectClasses as muiSelectClasses,
SelectProps as MUISelectProps,
InputAdornment as MUIInputAdornment,
inputAdornmentClasses as muiInputAdornmentClasses,
} from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material';
import { FC, useMemo } from 'react';
import { GREY } from '../lib/consts/DEFAULT_THEME';
type SelectOptionalProps = {
onClearIndicatorClick?: MUIIconButtonProps['onClick'] | null;
};
type SelectProps = MUISelectProps & SelectOptionalProps;
const Select: FC<SelectProps> = ({
onClearIndicatorClick,
...muiSelectProps
}) => {
const { sx: selectSx, value, ...restMuiSelectProps } = muiSelectProps;
const SELECT_DEFAULT_PROPS: Required<SelectOptionalProps> = {
onClearIndicatorClick: null,
};
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,
},
const combinedSx = useMemo(
() => ({
[`& .${muiSelectClasses.icon}`]: {
color: GREY,
},
[`& .${muiInputAdornmentClasses.root}`]: {
marginRight: '.8em',
},
[`& .${muiInputAdornmentClasses.root}`]: {
marginRight: '.8em',
},
[`& .${muiIconButtonClasses.root}`]: {
color: GREY,
visibility: 'hidden',
},
[`& .${muiIconButtonClasses.root}`]: {
color: GREY,
visibility: 'hidden',
},
[`&:hover .${muiInputAdornmentClasses.root} .${muiIconButtonClasses.root},
[`&:hover .${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 (
<MUISelect
{...{
endAdornment: clearIndicator,
...muiSelectProps,
sx: combinedSx,
}}
>
{children}
</MUISelect>
endAdornment={clearIndicatorElement}
value={value}
{...restMuiSelectProps}
sx={combinedSx}
/>
);
};
Select.defaultProps = SELECT_DEFAULT_PROPS;
export type { SelectProps };
export default Select;

@ -1,48 +1,15 @@
import { FC } from 'react';
import {
Checkbox as MUICheckbox,
FormControl as MUIFormControl,
selectClasses as muiSelectClasses,
} from '@mui/material';
import { FC, useCallback, useMemo } from 'react';
import InputMessageBox from './InputMessageBox';
import MenuItem from './MenuItem';
import { MessageBoxProps } from './MessageBox';
import OutlinedInput from './OutlinedInput';
import OutlinedInputLabel, {
OutlinedInputLabelProps,
} from './OutlinedInputLabel';
import Select, { SelectProps } from './Select';
type SelectWithLabelOptionalProps = {
checkItem?: ((value: string) => boolean) | null;
disableItem?: ((value: string) => boolean) | null;
hideItem?: ((value: string) => boolean) | null;
isCheckableItems?: boolean;
isReadOnly?: boolean;
inputLabelProps?: Partial<OutlinedInputLabelProps>;
label?: string | null;
messageBoxProps?: Partial<MessageBoxProps>;
selectProps?: Partial<SelectProps>;
};
type SelectWithLabelProps = SelectWithLabelOptionalProps & {
id: string;
selectItems: SelectItem[];
};
const SELECT_WITH_LABEL_DEFAULT_PROPS: Required<SelectWithLabelOptionalProps> =
{
checkItem: null,
disableItem: null,
hideItem: null,
isReadOnly: false,
isCheckableItems: false,
inputLabelProps: {},
label: null,
messageBoxProps: {},
selectProps: {},
};
import OutlinedInputLabel from './OutlinedInputLabel';
import Select from './Select';
const SelectWithLabel: FC<SelectWithLabelProps> = ({
id,
@ -50,58 +17,100 @@ const SelectWithLabel: FC<SelectWithLabelProps> = ({
selectItems,
checkItem,
disableItem,
formControlProps,
hideItem,
inputLabelProps,
isReadOnly,
messageBoxProps,
selectProps,
isCheckableItems = selectProps?.multiple,
}) => (
<MUIFormControl>
{label && (
<OutlinedInputLabel {...{ htmlFor: id, ...inputLabelProps }}>
{label}
</OutlinedInputLabel>
)}
<Select
{...{
id,
input: <OutlinedInput {...{ label }} />,
readOnly: isReadOnly,
...selectProps,
sx: isReadOnly
? {
[`& .${muiSelectClasses.icon}`]: {
visibility: 'hidden',
},
inputLabelProps = {},
isReadOnly = false,
messageBoxProps = {},
name,
onChange,
selectProps: {
multiple: selectMultiple,
sx: selectSx,
...restSelectProps
} = {},
value: selectValue,
// Props with initial value that depend on others.
isCheckableItems = selectMultiple,
}) => {
const combinedSx = useMemo(
() =>
isReadOnly
? {
[`& .${muiSelectClasses.icon}`]: {
visibility: 'hidden',
},
...selectSx,
}
: selectSx,
[isReadOnly, selectSx],
);
...selectProps?.sx,
}
: selectProps?.sx,
}}
>
{selectItems.map(({ value, displayValue = value }) => (
<MenuItem
disabled={disableItem?.call(null, value)}
key={`${id}-${value}`}
sx={{
display: hideItem?.call(null, value) ? 'none' : undefined,
}}
value={value}
>
{isCheckableItems && (
<MUICheckbox checked={checkItem?.call(null, value)} />
)}
{displayValue}
</MenuItem>
))}
</Select>
<InputMessageBox {...messageBoxProps} />
</MUIFormControl>
);
const createCheckbox = useCallback(
(value) =>
isCheckableItems && (
<MUICheckbox checked={checkItem?.call(null, value)} />
),
[checkItem, isCheckableItems],
);
const createMenuItem = useCallback(
(value, displayValue) => (
<MenuItem
disabled={disableItem?.call(null, value)}
key={`${id}-${value}`}
sx={{
display: hideItem?.call(null, value) ? 'none' : undefined,
}}
value={value}
>
{createCheckbox(value)}
{displayValue}
</MenuItem>
),
[createCheckbox, disableItem, hideItem, id],
);
SelectWithLabel.defaultProps = SELECT_WITH_LABEL_DEFAULT_PROPS;
const inputElement = useMemo(() => <OutlinedInput label={label} />, [label]);
const labelElement = useMemo(
() =>
label && (
<OutlinedInputLabel htmlFor={id} {...inputLabelProps}>
{label}
</OutlinedInputLabel>
),
[id, inputLabelProps, label],
);
const menuItemElements = useMemo(
() =>
selectItems.map((item) => {
const { value, displayValue = value }: SelectItem =
typeof item === 'string' ? { value: item } : item;
export type { SelectWithLabelProps };
return createMenuItem(value, displayValue);
}),
[createMenuItem, selectItems],
);
return (
<MUIFormControl fullWidth {...formControlProps}>
{labelElement}
<Select
id={id}
input={inputElement}
multiple={selectMultiple}
name={name}
onChange={onChange}
readOnly={isReadOnly}
value={selectValue}
{...restSelectProps}
sx={combinedSx}
>
{menuItemElements}
</Select>
<InputMessageBox {...messageBoxProps} />
</MUIFormControl>
);
};
export default SelectWithLabel;

@ -127,10 +127,7 @@ const ConfigPeersForm: FC<ConfigPeerFormProps> = ({
return (
<>
<ExpandablePanel
header={<BodyText>Configure striker peers</BodyText>}
loading={isLoading}
>
<ExpandablePanel header="Configure striker peers" loading={isLoading}>
<Grid columns={{ xs: 1, sm: 2 }} container spacing="1em">
<Grid item xs={1}>
<List

@ -85,10 +85,7 @@ const ManageChangedSSHKeysForm: FC<ManageChangedSSHKeysFormProps> = ({
return (
<>
<ExpandablePanel
header={<BodyText>Manage changed SSH keys</BodyText>}
loading={isLoading}
>
<ExpandablePanel header="Manage changed SSH keys" loading={isLoading}>
<FlexBox spacing=".2em">
<BodyText>
The identity of the following targets have unexpectedly changed.

@ -36,10 +36,7 @@ const ManageUsersForm: FC = () => {
}, [setListMessage, setUsers, users]);
return (
<ExpandablePanel
header={<BodyText>Manage users</BodyText>}
loading={!users}
>
<ExpandablePanel header="Manage users" loading={!users}>
<List
allowEdit={false}
listEmpty={<MessageBox {...listMessage} />}

@ -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;

@ -8,6 +8,7 @@ import { BLACK, TEXT, UNSELECTED } from '../../lib/consts/DEFAULT_THEME';
type BodyTextOptionalProps = {
inheritColour?: boolean;
inline?: boolean;
inverted?: boolean;
monospaced?: boolean;
selected?: boolean;
@ -20,6 +21,7 @@ const BODY_TEXT_CLASS_PREFIX = 'BodyText';
const BODY_TEXT_DEFAULT_PROPS: Required<BodyTextOptionalProps> = {
inheritColour: false,
inline: false,
inverted: false,
monospaced: false,
selected: true,
@ -68,6 +70,7 @@ const BodyText: FC<BodyTextProps> = ({
children,
className,
inheritColour: isInheritColour = BODY_TEXT_DEFAULT_PROPS.inheritColour,
inline: isInline = BODY_TEXT_DEFAULT_PROPS.inline,
inverted: isInvert = BODY_TEXT_DEFAULT_PROPS.inverted,
monospaced: isMonospace = BODY_TEXT_DEFAULT_PROPS.monospaced,
selected: isSelect = BODY_TEXT_DEFAULT_PROPS.selected,
@ -75,6 +78,11 @@ const BodyText: FC<BodyTextProps> = ({
text = BODY_TEXT_DEFAULT_PROPS.text,
...muiTypographyRestProps
}) => {
const sxDisplay = useMemo<string | undefined>(
() => (isInline ? 'inline' : undefined),
[isInline],
);
const baseClassName = useMemo(
() =>
buildBodyTextClasses({
@ -89,30 +97,30 @@ const BodyText: FC<BodyTextProps> = ({
return (
<MUITypography
{...{
className: `${baseClassName} ${className}`,
variant: 'subtitle1',
...muiTypographyRestProps,
sx: {
[`&.${BODY_TEXT_CLASSES.inverted}`]: {
color: BLACK,
},
[`&.${BODY_TEXT_CLASSES.monospaced}`]: {
fontFamily: 'Source Code Pro',
fontWeight: 400,
},
[`&.${BODY_TEXT_CLASSES.selected}`]: {
color: TEXT,
},
[`&.${BODY_TEXT_CLASSES.unselected}`]: {
color: UNSELECTED,
},
...sx,
className={`${baseClassName} ${className}`}
variant="subtitle1"
{...muiTypographyRestProps}
sx={{
display: sxDisplay,
[`&.${BODY_TEXT_CLASSES.inverted}`]: {
color: BLACK,
},
[`&.${BODY_TEXT_CLASSES.monospaced}`]: {
fontFamily: 'Source Code Pro',
fontWeight: 400,
},
[`&.${BODY_TEXT_CLASSES.selected}`]: {
color: TEXT,
},
[`&.${BODY_TEXT_CLASSES.unselected}`]: {
color: UNSELECTED,
},
...sx,
}}
>
{content}

@ -3,12 +3,7 @@ import { FC } from 'react';
import { BodyTextProps } from './BodyText';
import SmallText from './SmallText';
type InlineMonoTextProps = BodyTextProps;
const InlineMonoText: FC<InlineMonoTextProps> = ({
sx,
...bodyTextRestProps
}) => (
const InlineMonoText: FC<BodyTextProps> = ({ sx, ...bodyTextRestProps }) => (
<SmallText
{...{
...bodyTextRestProps,
@ -23,6 +18,4 @@ const InlineMonoText: FC<InlineMonoTextProps> = ({
/>
);
export type { InlineMonoTextProps };
export default InlineMonoText;

@ -3,9 +3,7 @@ import { FC } from 'react';
import { BodyTextProps } from './BodyText';
import SmallText from './SmallText';
type MonoTextProps = BodyTextProps;
const MonoText: FC<MonoTextProps> = ({ sx, ...bodyTextRestProps }) => (
const MonoText: FC<BodyTextProps> = ({ sx, ...bodyTextRestProps }) => (
<SmallText
monospaced
sx={{ alignItems: 'center', display: 'flex', height: '100%', ...sx }}
@ -13,6 +11,4 @@ const MonoText: FC<MonoTextProps> = ({ sx, ...bodyTextRestProps }) => (
/>
);
export type { MonoTextProps };
export default MonoText;

@ -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;

@ -2,12 +2,8 @@ import { FC } from 'react';
import BodyText, { BodyTextProps } from './BodyText';
type SmallTextProps = BodyTextProps;
const SmallText: FC<SmallTextProps> = ({ ...bodyTextRestProps }) => (
const SmallText: FC<BodyTextProps> = ({ ...bodyTextRestProps }) => (
<BodyText {...{ variant: 'body2', ...bodyTextRestProps }} />
);
export type { SmallTextProps };
export default SmallText;

@ -2,8 +2,16 @@ import BodyText, { BodyTextProps } from './BodyText';
import HeaderText from './HeaderText';
import InlineMonoText from './InlineMonoText';
import MonoText from './MonoText';
import SensitiveText from './SensitiveText';
import SmallText from './SmallText';
export type { BodyTextProps };
export { BodyText, HeaderText, InlineMonoText, MonoText, SmallText };
export {
BodyText,
HeaderText,
InlineMonoText,
MonoText,
SensitiveText,
SmallText,
};

@ -13,6 +13,7 @@ export const DIVIDER = '#888';
export const SELECTED_ANVIL = '#00ff00';
export const DISABLED = '#AAA';
export const BLACK = '#343434';
export const EERIE_BLACK = '#1F1F1F';
// TODO: remove when old icons are completely replaced.
export const OLD_ICON = '#9da2a7';

@ -1,4 +1,4 @@
import { InputProps as MUIInputProps } from '@mui/material';
import { ChangeEventHandler } from 'react';
import MAP_TO_VALUE_CONVERTER from './consts/MAP_TO_VALUE_CONVERTER';
@ -8,10 +8,11 @@ const createInputOnChangeHandler =
preSet,
set,
setType = 'string' as TypeName,
}: CreateInputOnChangeHandlerOptions<TypeName> = {}): MUIInputProps['onChange'] =>
valueKey = 'value',
}: CreateInputOnChangeHandlerOptions<TypeName> = {}): ChangeEventHandler<HTMLInputElement> =>
(event) => {
const {
target: { value },
target: { [valueKey]: value },
} = event;
const postConvertValue = MAP_TO_VALUE_CONVERTER[setType](
value,

@ -7,6 +7,7 @@ import getQueryParam from '../../lib/getQueryParam';
import Grid from '../../components/Grid';
import handleAPIError from '../../lib/handleAPIError';
import Header from '../../components/Header';
import ManageFencePanel from '../../components/ManageFence';
import { Panel } from '../../components/Panels';
import PrepareHostForm from '../../components/PrepareHostForm';
import PrepareNetworkForm from '../../components/PrepareNetworkForm';
@ -21,9 +22,9 @@ import useProtectedState from '../../hooks/useProtectedState';
const MAP_TO_PAGE_TITLE: Record<string, string> = {
'prepare-host': 'Prepare Host',
'prepare-network': 'Prepare Network',
'manage-fence-devices': 'Manage Fence Devices',
'manage-upses': 'Manage UPSes',
'manage-manifests': 'Manage Manifests',
'manage-fence': 'Manage Fence Devices',
'manage-ups': 'Manage UPSes',
'manage-manifest': 'Manage Manifests',
};
const PAGE_TITLE_LOADING = 'Loading';
const STEP_CONTENT_GRID_COLUMNS = { md: 8, sm: 6, xs: 1 };
@ -116,6 +117,19 @@ const PrepareNetworkTabContent: FC = () => {
);
};
const ManageFenceTabContent: FC = () => (
<Grid
columns={STEP_CONTENT_GRID_COLUMNS}
layout={{
'managefence-left-column': {},
'managefence-center-column': {
children: <ManageFencePanel />,
...STEP_CONTENT_GRID_CENTER_COLUMN,
},
}}
/>
);
const ManageElement: FC = () => {
const {
isReady,
@ -162,7 +176,7 @@ const ManageElement: FC = () => {
>
<Tab label="Prepare host" value="prepare-host" />
<Tab label="Prepare network" value="prepare-network" />
<Tab label="Manage fence devices" value="manage-fence-devices" />
<Tab label="Manage fence devices" value="manage-fence" />
</Tabs>
</Panel>
<TabContent changingTabId={pageTabId} tabId="prepare-host">
@ -171,8 +185,8 @@ const ManageElement: FC = () => {
<TabContent changingTabId={pageTabId} tabId="prepare-network">
<PrepareNetworkTabContent />
</TabContent>
<TabContent changingTabId={pageTabId} tabId="manage-fence-devices">
{}
<TabContent changingTabId={pageTabId} tabId="manage-fence">
<ManageFenceTabContent />
</TabContent>
</>
);

@ -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;

@ -1,20 +1,25 @@
type ConfirmDialogOptionalProps = {
actionCancelText?: string;
closeOnProceed?: boolean;
contentContainerProps?: import('../components/FlexBox').FlexBoxProps;
dialogProps?: Partial<import('@mui/material').DialogProps>;
formContent?: boolean;
loadingAction?: boolean;
onActionAppend?: ContainedButtonProps['onClick'];
onProceedAppend?: ContainedButtonProps['onClick'];
onCancelAppend?: ContainedButtonProps['onClick'];
onSubmitAppend?: import('react').FormEventHandler<HTMLDivElement>;
openInitially?: boolean;
proceedButtonProps?: ContainedButtonProps;
proceedColour?: 'blue' | 'red';
scrollContent?: boolean;
scrollBoxProps?: import('@mui/material').BoxProps;
};
type ConfirmDialogProps = ConfirmDialogOptionalProps & {
actionProceedText: string;
content: import('@mui/material').ReactNode;
titleText: string;
content: import('react').ReactNode;
titleText: import('react').ReactNode;
};
type ConfirmDialogForwardedRefContent = {

@ -1,4 +1,4 @@
type MapToInputType = Pick<MapToType, 'number' | 'string'>;
type MapToInputType = Pick<MapToType, 'boolean' | 'number' | 'string'>;
type InputOnChangeParameters = Parameters<
Exclude<import('@mui/material').InputBaseProps['onChange'], undefined>
@ -12,4 +12,8 @@ type CreateInputOnChangeHandlerOptions<TypeName extends keyof MapToInputType> =
preSet?: (...args: InputOnChangeParameters) => void;
set?: StateSetter;
setType?: TypeName;
valueKey?: Extract<
keyof import('react').ChangeEvent<HTMLInputElement>['target'],
'checked' | 'value'
>;
};

@ -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;

@ -1,12 +1,18 @@
type OnCheckboxChange = Exclude<CheckboxProps['onChange'], undefined>;
type CheckboxChangeEventHandler = Exclude<CheckboxProps['onChange'], undefined>;
type ListItemButtonChangeEventHandler = Exclude<
import('@mui/material').ListItemButtonProps['onClick'],
undefined
>;
type ListOptionalProps<T extends unknown = unknown> = {
allowCheckAll?: boolean;
allowAddItem?: boolean;
allowCheckAll?: boolean;
allowCheckItem?: boolean;
allowEdit?: boolean;
allowDelete?: boolean;
allowEdit?: boolean;
allowEditItem?: boolean;
allowItemButton?: boolean;
edit?: boolean;
flexBoxProps?: import('../components/FlexBox').FlexBoxProps;
header?: import('react').ReactNode;
@ -22,11 +28,16 @@ type ListOptionalProps<T extends unknown = unknown> = {
onAdd?: import('../components/IconButton').IconButtonProps['onClick'];
onDelete?: import('../components/IconButton').IconButtonProps['onClick'];
onEdit?: import('../components/IconButton').IconButtonProps['onClick'];
onAllCheckboxChange?: OnCheckboxChange;
onAllCheckboxChange?: CheckboxChangeEventHandler;
onItemCheckboxChange?: (
key: string,
...onChangeParams: Parameters<OnCheckboxChange>
) => ReturnType<OnCheckboxChange>;
...checkboxChangeEventHandlerArgs: Parameters<CheckboxChangeEventHandler>
) => ReturnType<CheckboxChangeEventHandler>;
onItemClick?: (
value: T,
key: string,
...listItemButtonChangeEventHandlerArgs: Parameters<ListItemButtonChangeEventHandler>
) => ReturnType<ListItemButtonChangeEventHandler>;
renderListItem?: (key: string, value: T) => import('react').ReactNode;
renderListItemCheckboxState?: (key: string, value: T) => boolean;
scroll?: boolean;

@ -0,0 +1,5 @@
type SelectOptionalProps = {
onClearIndicatorClick?: import('@mui/material').IconButtonProps['onClick'];
};
type SelectProps = import('@mui/material').SelectProps & SelectOptionalProps;

@ -5,3 +5,26 @@ type SelectItem<
displayValue?: DisplayValueType;
value: ValueType;
};
type OperateSelectItemFunction = (value: string) => boolean;
type SelectWithLabelOptionalProps = {
checkItem?: OperateSelectItemFunction;
disableItem?: OperateSelectItemFunction;
formControlProps?: import('@mui/material').FormControlProps;
hideItem?: OperateSelectItemFunction;
isCheckableItems?: boolean;
isReadOnly?: boolean;
inputLabelProps?: Partial<
import('../components/OutlinedInputLabel').OutlinedInputLabelProps
>;
label?: string;
messageBoxProps?: Partial<import('../components/MessageBox').MessageBoxProps>;
selectProps?: Partial<SelectProps>;
};
type SelectWithLabelProps = SelectWithLabelOptionalProps &
Pick<SelectProps, 'name' | 'onChange' | 'value'> & {
id: string;
selectItems: Array<SelectItem | string>;
};

@ -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<{
changingTabId: T;
tabId: T;
}>;
type TabContentOptionalProps = {
retain?: boolean;
};
type TabContentProps<T> = TabContentOptionalProps &
import('react').PropsWithChildren<{
changingTabId: T;
tabId: T;
}>;

Loading…
Cancel
Save