From deac1fc6a8633b47546eb730c67d315064412a2b Mon Sep 17 00:00:00 2001 From: Deezzir Date: Thu, 30 Mar 2023 20:57:17 -0400 Subject: [PATCH 01/75] fix: introduced optional arg for clean_spaces --- Anvil/Tools/Words.pm | 10 ++++++---- scancore-agents/scan-storcli/scan-storcli | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Anvil/Tools/Words.pm b/Anvil/Tools/Words.pm index a038e879..acd619c7 100644 --- a/Anvil/Tools/Words.pm +++ b/Anvil/Tools/Words.pm @@ -203,10 +203,12 @@ sub clean_spaces # Setup default values my $string = defined $parameter->{string} ? $parameter->{string} : ""; - $string =~ s/^\s+//; - $string =~ s/\s+$//; - $string =~ s/\r//g; - $string =~ s/\s+/ /g; + my $merge_spaces = defined $parameter->{merge_spaces} ? $parameter->{merge_spaces} : 1; + + $string =~ s/^\s+//; + $string =~ s/\s+$//; + $string =~ s/\r//g; + $string =~ s/\s+/ /g if $merge_spaces; return($string); } diff --git a/scancore-agents/scan-storcli/scan-storcli b/scancore-agents/scan-storcli/scan-storcli index e45b66be..a499ff9e 100755 --- a/scancore-agents/scan-storcli/scan-storcli +++ b/scancore-agents/scan-storcli/scan-storcli @@ -8755,7 +8755,7 @@ sub get_bbu_data }}); foreach my $line (split/\n/, $output) { - $line = $anvil->Words->clean_spaces({string => $line}); + $line = $anvil->Words->clean_spaces({string => $line, merge_spaces => 0}); $line =~ s/\s+:/:/; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { line => $line }}); last if $line =~ /$adapter Failed /i; @@ -8987,7 +8987,7 @@ sub get_cachevault_data }}); foreach my $line (split/\n/, $output) { - $line = $anvil->Words->clean_spaces({string => $line}); + $line = $anvil->Words->clean_spaces({string => $line, merge_spaces => 0}); $line =~ s/\s+:/:/; $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => 2, list => { line => $line }}); last if $line =~ /Cachevault doesn't exist/i; From 9241b5ef6a38c75a867f5e6e5478a298b43e5a96 Mon Sep 17 00:00:00 2001 From: Deezzir Date: Thu, 30 Mar 2023 21:03:05 -0400 Subject: [PATCH 02/75] docs: added annotation for the new arg --- Anvil/Tools/Words.pm | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Anvil/Tools/Words.pm b/Anvil/Tools/Words.pm index acd619c7..4746e8e5 100644 --- a/Anvil/Tools/Words.pm +++ b/Anvil/Tools/Words.pm @@ -192,6 +192,10 @@ Parameters; This sets the string to be cleaned. If it is not passed in, or if the string is empty, then an empty string will be returned without error. +=head3 merge_spaces (optional) + +This is a boolean value (0 or 1) that, if set, will merge multiple spaces into a single space. If not set, multiple spaces will be left as is. The default is '1'. + =cut sub clean_spaces { From 916ec54dd0682e0d1ad82c718b8e18888d79a446 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 14 Feb 2023 22:40:29 -0500 Subject: [PATCH 03/75] feat(striker-ui): add AddFenceDeviceForm --- striker-ui/components/AddFenceDeviceForm.tsx | 125 +++++++++++++++++++ striker-ui/types/APIFence.d.ts | 23 ++++ 2 files changed, 148 insertions(+) create mode 100644 striker-ui/components/AddFenceDeviceForm.tsx create mode 100644 striker-ui/types/APIFence.d.ts diff --git a/striker-ui/components/AddFenceDeviceForm.tsx b/striker-ui/components/AddFenceDeviceForm.tsx new file mode 100644 index 00000000..b30b7150 --- /dev/null +++ b/striker-ui/components/AddFenceDeviceForm.tsx @@ -0,0 +1,125 @@ +import { Box } from '@mui/material'; +import { FC, useMemo, useState } from 'react'; +import useIsFirstRender from '../hooks/useIsFirstRender'; +import useProtectedState from '../hooks/useProtectedState'; +import api from '../lib/api'; +import handleAPIError from '../lib/handleAPIError'; +import Autocomplete from './Autocomplete'; +import FlexBox from './FlexBox'; +import Spinner from './Spinner'; +import { BodyText } from './Text'; + +type FenceDeviceAutocompleteOption = { + fenceDeviceDescription: string; + fenceDeviceId: string; + label: string; +}; + +const AddFenceDeivceForm: FC = () => { + const isFirstRender = useIsFirstRender(); + + const [fenceDeviceTemplate, setFenceDeviceTemplate] = useProtectedState< + APIFenceTemplate | undefined + >(undefined); + const [inputFenceDeviceTypeValue, setInputFenceDeviceTypeValue] = + useState(null); + const [isLoadingTemplate, setIsLoadingTemplate] = + useProtectedState(true); + + const fenceDeviceTypeOptions = useMemo( + () => + fenceDeviceTemplate + ? Object.entries(fenceDeviceTemplate).map( + ([id, { description: rawDescription }]) => { + const description = + typeof rawDescription === 'string' + ? rawDescription + : 'No description.'; + + return { + fenceDeviceDescription: description, + fenceDeviceId: id, + label: id, + }; + }, + ) + : [], + [fenceDeviceTemplate], + ); + + const autocompleteFenceDeviceType = useMemo( + () => ( + + option.fenceDeviceId === value.fenceDeviceId + } + label="Fence device type" + onChange={(event, newFenceDeviceType) => { + setInputFenceDeviceTypeValue(newFenceDeviceType); + }} + openOnFocus + options={fenceDeviceTypeOptions} + renderOption={( + props, + { fenceDeviceDescription, fenceDeviceId }, + { selected }, + ) => ( + *': { + width: '100%', + }, + }} + {...props} + > + + {fenceDeviceId} + + {fenceDeviceDescription} + + )} + value={inputFenceDeviceTypeValue} + /> + ), + [fenceDeviceTypeOptions, inputFenceDeviceTypeValue], + ); + + const formContent = useMemo( + () => + isLoadingTemplate ? ( + + ) : ( + <>{autocompleteFenceDeviceType} + ), + [autocompleteFenceDeviceType, isLoadingTemplate], + ); + + if (isFirstRender) { + api + .get(`/fence/template`) + .then(({ data }) => { + setFenceDeviceTemplate(data); + }) + .catch((error) => { + handleAPIError(error); + }) + .finally(() => { + setIsLoadingTemplate(false); + }); + } + + return {formContent}; +}; + +export default AddFenceDeivceForm; diff --git a/striker-ui/types/APIFence.d.ts b/striker-ui/types/APIFence.d.ts new file mode 100644 index 00000000..e44d522d --- /dev/null +++ b/striker-ui/types/APIFence.d.ts @@ -0,0 +1,23 @@ +type APIFenceTemplate = { + [fenceId: string]: { + actions: string[]; + description: string; + parameters: { + [parameterId: string]: { + content_type: 'boolean' | 'integer' | 'second' | 'select' | 'string'; + 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 }; + }; + }; +}; From 182837c93b7d504fe37b5ab5ca2949b15915de8d Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 15 Feb 2023 16:51:08 -0500 Subject: [PATCH 04/75] fix(striker-ui): allow array options in SelectWithLabel --- striker-ui/components/SelectWithLabel.tsx | 125 ++++++++++++++-------- 1 file changed, 80 insertions(+), 45 deletions(-) diff --git a/striker-ui/components/SelectWithLabel.tsx b/striker-ui/components/SelectWithLabel.tsx index b062403a..8da8980f 100644 --- a/striker-ui/components/SelectWithLabel.tsx +++ b/striker-ui/components/SelectWithLabel.tsx @@ -1,4 +1,4 @@ -import { FC } from 'react'; +import { FC, useCallback, useMemo } from 'react'; import { Checkbox as MUICheckbox, FormControl as MUIFormControl, @@ -28,7 +28,7 @@ type SelectWithLabelOptionalProps = { type SelectWithLabelProps = SelectWithLabelOptionalProps & { id: string; - selectItems: SelectItem[]; + selectItems: Array; }; const SELECT_WITH_LABEL_DEFAULT_PROPS: Required = @@ -54,51 +54,86 @@ const SelectWithLabel: FC = ({ inputLabelProps, isReadOnly, messageBoxProps, - selectProps, + selectProps = {}, isCheckableItems = selectProps?.multiple, -}) => ( - - {label && ( - - {label} - - )} - - - -); + const combinedSx = useMemo( + () => + isReadOnly + ? { + [`& .${muiSelectClasses.icon}`]: { + visibility: 'hidden', + }, + + ...selectSx, + } + : selectSx, + [isReadOnly, selectSx], + ); + + const createCheckbox = useCallback( + (value) => + isCheckableItems && ( + + ), + [checkItem, isCheckableItems], + ); + const createMenuItem = useCallback( + (value, displayValue) => ( + + {createCheckbox(value)} + {displayValue} + + ), + [createCheckbox, disableItem, hideItem, id], + ); + + const inputElement = useMemo(() => , [label]); + const labelElement = useMemo( + () => + label && ( + + {label} + + ), + [id, inputLabelProps, label], + ); + const menuItemElements = useMemo( + () => + selectItems.map((item) => { + const { value, displayValue = value }: SelectItem = + typeof item === 'string' ? { value: item } : item; + + return createMenuItem(value, displayValue); + }), + [createMenuItem, selectItems], + ); + + return ( + + {labelElement} + + + + ); +}; SelectWithLabel.defaultProps = SELECT_WITH_LABEL_DEFAULT_PROPS; From 40febdae87518dac186146b269008922f0cd5c5c Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 15 Feb 2023 16:55:47 -0500 Subject: [PATCH 05/75] fix(striker-ui): allow string header in ExpandablePanel --- striker-ui/components/Panels/ExpandablePanel.tsx | 7 ++++++- striker-ui/components/StrikerConfig/ConfigPeersForm.tsx | 5 +---- .../components/StrikerConfig/ManageChangedSSHKeysForm.tsx | 5 +---- striker-ui/components/StrikerConfig/ManageUsersForm.tsx | 5 +---- 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/striker-ui/components/Panels/ExpandablePanel.tsx b/striker-ui/components/Panels/ExpandablePanel.tsx index 63fd0bcf..d64e6f97 100644 --- a/striker-ui/components/Panels/ExpandablePanel.tsx +++ b/striker-ui/components/Panels/ExpandablePanel.tsx @@ -12,6 +12,7 @@ import InnerPanel from './InnerPanel'; import InnerPanelBody from './InnerPanelBody'; import InnerPanelHeader from './InnerPanelHeader'; import Spinner from '../Spinner'; +import { BodyText } from '../Text'; type ExpandablePanelOptionalProps = { expandInitially?: boolean; @@ -46,6 +47,10 @@ const ExpandablePanel: FC = ({ [isExpand], ); const contentHeight = useMemo(() => (isExpand ? 'auto' : '.2em'), [isExpand]); + const headerElement = useMemo( + () => (typeof header === 'string' ? {header} : header), + [header], + ); const headerSpinner = useMemo( () => isShowHeaderSpinner && !isExpand && isLoading ? ( @@ -74,7 +79,7 @@ const ExpandablePanel: FC = ({ - {header} + {headerElement} {headerSpinner} = ({ return ( <> - Configure striker peers} - loading={isLoading} - > + = ({ return ( <> - Manage changed SSH keys} - loading={isLoading} - > + The identity of the following targets have unexpectedly changed. diff --git a/striker-ui/components/StrikerConfig/ManageUsersForm.tsx b/striker-ui/components/StrikerConfig/ManageUsersForm.tsx index 4af37416..a861b3e4 100644 --- a/striker-ui/components/StrikerConfig/ManageUsersForm.tsx +++ b/striker-ui/components/StrikerConfig/ManageUsersForm.tsx @@ -36,10 +36,7 @@ const ManageUsersForm: FC = () => { }, [setListMessage, setUsers, users]); return ( - Manage users} - loading={!users} - > + } From 0f40ffe604d130088ce6e40ec2c9355da72a05da Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 15 Feb 2023 19:00:16 -0500 Subject: [PATCH 06/75] fix(striker-ui): allow specify valueKey in InputWithRef --- striker-ui/components/InputWithRef.tsx | 36 +++++++++++++++++--------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/striker-ui/components/InputWithRef.tsx b/striker-ui/components/InputWithRef.tsx index 13dedb74..7600e3d7 100644 --- a/striker-ui/components/InputWithRef.tsx +++ b/striker-ui/components/InputWithRef.tsx @@ -20,6 +20,7 @@ type InputWithRefOptionalPropsWithDefault< > = { createInputOnChangeHandlerOptions?: CreateInputOnChangeHandlerOptions; required?: boolean; + valueKey?: string; valueType?: TypeName; }; type InputWithRefOptionalPropsWithoutDefault = { @@ -57,6 +58,7 @@ const INPUT_WITH_REF_DEFAULT_PROPS: Required< InputWithRefOptionalPropsWithoutDefault = { createInputOnChangeHandlerOptions: {}, required: false, + valueKey: 'value', valueType: 'string', }; @@ -71,6 +73,7 @@ const InputWithRef = forwardRef( inputTestBatch, onFirstRender, required: isRequired = INPUT_WITH_REF_DEFAULT_PROPS.required, + valueKey = INPUT_WITH_REF_DEFAULT_PROPS.valueKey, valueType = INPUT_WITH_REF_DEFAULT_PROPS.valueType as TypeName, }: InputWithRefProps, ref: ForwardedRef>, @@ -123,6 +126,26 @@ const InputWithRef = forwardRef( })), [initOnBlur, testInput], ); + const onChange = useMemo( + () => + createInputOnChangeHandler({ + postSet: (...args) => { + setIsChangedByUser(true); + initOnChange?.call(null, ...args); + postSetAppend?.call(null, ...args); + }, + set: setValue, + setType: valueType, + ...restCreateInputOnChangeHandlerOptions, + }), + [ + initOnChange, + postSetAppend, + restCreateInputOnChangeHandlerOptions, + setValue, + valueType, + ], + ); const onFocus = useMemo( () => initOnFocus ?? @@ -133,17 +156,6 @@ const InputWithRef = forwardRef( [initOnFocus, inputTestBatch], ); - const onChange = createInputOnChangeHandler({ - 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 +179,7 @@ const InputWithRef = forwardRef( onChange, onFocus, required: isRequired, - value: inputValue, + [valueKey]: inputValue, }); }, ); From d9685250b6fe9f7c6924094949a7eb140d0d2d16 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 15 Feb 2023 23:05:01 -0500 Subject: [PATCH 07/75] fix(striker-ui): revise inner and expandable panel types --- .../components/Panels/ExpandablePanel.tsx | 30 +++----------- striker-ui/components/Panels/InnerPanel.tsx | 40 +++++++++---------- striker-ui/types/ExpandablePanel.d.ts | 10 +++++ striker-ui/types/InnerPanel.d.ts | 1 + 4 files changed, 36 insertions(+), 45 deletions(-) create mode 100644 striker-ui/types/ExpandablePanel.d.ts create mode 100644 striker-ui/types/InnerPanel.d.ts diff --git a/striker-ui/components/Panels/ExpandablePanel.tsx b/striker-ui/components/Panels/ExpandablePanel.tsx index d64e6f97..e2ab8081 100644 --- a/striker-ui/components/Panels/ExpandablePanel.tsx +++ b/striker-ui/components/Panels/ExpandablePanel.tsx @@ -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'; @@ -14,31 +14,15 @@ 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 = { - expandInitially: false, - loading: false, - showHeaderSpinner: false, -}; const HEADER_SPINNER_LENGTH = '1.2em'; const ExpandablePanel: FC = ({ 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(isExpandInitially); @@ -76,7 +60,7 @@ const ExpandablePanel: FC = ({ ); return ( - + {headerElement} @@ -96,6 +80,4 @@ const ExpandablePanel: FC = ({ ); }; -ExpandablePanel.defaultProps = EXPANDABLE_PANEL_DEFAULT_PROPS; - export default ExpandablePanel; diff --git a/striker-ui/components/Panels/InnerPanel.tsx b/striker-ui/components/Panels/InnerPanel.tsx index 32896fef..65239514 100644 --- a/striker-ui/components/Panels/InnerPanel.tsx +++ b/striker-ui/components/Panels/InnerPanel.tsx @@ -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 = ({ sx, ...muiBoxRestProps }) => { + const combinedSx = useMemo>( + () => ({ + borderWidth: '1px', + borderRadius: BORDER_RADIUS, + borderStyle: 'solid', + borderColor: DIVIDER, + marginTop: '1.4em', + marginBottom: '1.4em', + paddingBottom: 0, + position: 'relative', -const InnerPanel: FC = ({ sx, ...muiBoxRestProps }) => ( - -); + return ; +}; export default InnerPanel; diff --git a/striker-ui/types/ExpandablePanel.d.ts b/striker-ui/types/ExpandablePanel.d.ts new file mode 100644 index 00000000..5bbc2d79 --- /dev/null +++ b/striker-ui/types/ExpandablePanel.d.ts @@ -0,0 +1,10 @@ +type ExpandablePanelOptionalProps = { + expandInitially?: boolean; + loading?: boolean; + panelProps?: InnerPanelProps; + showHeaderSpinner?: boolean; +}; + +type ExpandablePanelProps = ExpandablePanelOptionalProps & { + header: import('react').ReactNode; +}; diff --git a/striker-ui/types/InnerPanel.d.ts b/striker-ui/types/InnerPanel.d.ts new file mode 100644 index 00000000..0f0b3adb --- /dev/null +++ b/striker-ui/types/InnerPanel.d.ts @@ -0,0 +1 @@ +type InnerPanelProps = import('@mui/material').BoxProps; From 4a0d4b5cb3067cccd09f2df512874ba72dbd3e56 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 15 Feb 2023 23:06:25 -0500 Subject: [PATCH 08/75] fix(striker-ui): generate fence parameter inputs --- striker-ui/components/AddFenceDeviceForm.tsx | 157 +++++++++++++++++-- striker-ui/types/APIFence.d.ts | 11 +- 2 files changed, 154 insertions(+), 14 deletions(-) diff --git a/striker-ui/components/AddFenceDeviceForm.tsx b/striker-ui/components/AddFenceDeviceForm.tsx index b30b7150..fd67584d 100644 --- a/striker-ui/components/AddFenceDeviceForm.tsx +++ b/striker-ui/components/AddFenceDeviceForm.tsx @@ -1,13 +1,19 @@ -import { Box } from '@mui/material'; -import { FC, useMemo, useState } from 'react'; -import useIsFirstRender from '../hooks/useIsFirstRender'; -import useProtectedState from '../hooks/useProtectedState'; +import { Box, Switch } from '@mui/material'; +import { FC, ReactElement, ReactNode, useMemo, useState } from 'react'; + import api from '../lib/api'; -import handleAPIError from '../lib/handleAPIError'; import Autocomplete from './Autocomplete'; +import ContainedButton from './ContainedButton'; import FlexBox from './FlexBox'; +import handleAPIError from '../lib/handleAPIError'; +import InputWithRef from './InputWithRef'; +import OutlinedInputWithLabel from './OutlinedInputWithLabel'; +import { ExpandablePanel } from './Panels'; +import SelectWithLabel from './SelectWithLabel'; import Spinner from './Spinner'; import { BodyText } from './Text'; +import useIsFirstRender from '../hooks/useIsFirstRender'; +import useProtectedState from '../hooks/useProtectedState'; type FenceDeviceAutocompleteOption = { fenceDeviceDescription: string; @@ -15,13 +21,54 @@ type FenceDeviceAutocompleteOption = { label: string; }; +type FenceParameterInputBuilder = (args: { + id: string; + isChecked?: boolean; + isRequired?: boolean; + label?: string; + selectOptions?: string[]; + value?: string; +}) => ReactElement; + +const MAP_TO_INPUT_BUILDER: Partial< + Record, FenceParameterInputBuilder> +> & { string: FenceParameterInputBuilder } = { + boolean: ({ id, isChecked = false, label }) => ( + + {label} + + + ), + select: ({ id, isRequired, label, selectOptions = [], value = '' }) => ( + + } + required={isRequired} + /> + ), + string: ({ id, isRequired, label = '', value }) => ( + } + required={isRequired} + /> + ), +}; + const AddFenceDeivceForm: FC = () => { const isFirstRender = useIsFirstRender(); const [fenceDeviceTemplate, setFenceDeviceTemplate] = useProtectedState< APIFenceTemplate | undefined >(undefined); - const [inputFenceDeviceTypeValue, setInputFenceDeviceTypeValue] = + const [fenceDeviceTypeValue, setInputFenceDeviceTypeValue] = useState(null); const [isLoadingTemplate, setIsLoadingTemplate] = useProtectedState(true); @@ -47,7 +94,7 @@ const AddFenceDeivceForm: FC = () => { [fenceDeviceTemplate], ); - const autocompleteFenceDeviceType = useMemo( + const fenceDeviceTypeElement = useMemo( () => ( { {fenceDeviceDescription} )} - value={inputFenceDeviceTypeValue} + value={fenceDeviceTypeValue} /> ), - [fenceDeviceTypeOptions, inputFenceDeviceTypeValue], + [fenceDeviceTypeOptions, fenceDeviceTypeValue], ); + const fenceParameterElements = useMemo(() => { + let result: ReactNode; + + if (fenceDeviceTemplate && fenceDeviceTypeValue) { + const { fenceDeviceId } = fenceDeviceTypeValue; + const { parameters: fenceDeviceParameters } = + fenceDeviceTemplate[fenceDeviceId]; + + const { optional: optionalInputs, required: requiredInputs } = + Object.entries(fenceDeviceParameters).reduce<{ + optional: ReactElement[]; + required: ReactElement[]; + }>( + ( + previous, + [ + parameterId, + { + content_type: contentType, + default: parameterDefault, + options: parameterSelectOptions, + required: isRequired, + }, + ], + ) => { + const { optional, required } = previous; + const buildInput = + MAP_TO_INPUT_BUILDER[contentType] ?? MAP_TO_INPUT_BUILDER.string; + + const fenceJoinParameterId = `${fenceDeviceId}-${parameterId}`; + const parameterIsRequired = isRequired === '1'; + const parameterInput = buildInput({ + id: fenceJoinParameterId, + isChecked: parameterDefault === '1', + isRequired: parameterIsRequired, + label: parameterId, + selectOptions: parameterSelectOptions, + value: parameterDefault, + }); + + if (parameterIsRequired) { + required.push(parameterInput); + } else { + optional.push(parameterInput); + } + + return previous; + }, + { + optional: [], + required: [ + MAP_TO_INPUT_BUILDER.string({ + id: `${fenceDeviceId}-name`, + isRequired: true, + label: 'Fence device name', + }), + ], + }, + ); + + result = ( + <> + + {requiredInputs} + + + {optionalInputs} + + + ); + } + + return result; + }, [fenceDeviceTemplate, fenceDeviceTypeValue]); const formContent = useMemo( () => isLoadingTemplate ? ( ) : ( - <>{autocompleteFenceDeviceType} + { + event.preventDefault(); + }} + sx={{ '& > div': { marginBottom: 0 } }} + > + {fenceDeviceTypeElement} + {fenceParameterElements} + + Add fence device + + ), - [autocompleteFenceDeviceType, isLoadingTemplate], + [fenceDeviceTypeElement, fenceParameterElements, isLoadingTemplate], ); if (isFirstRender) { @@ -119,7 +252,7 @@ const AddFenceDeivceForm: FC = () => { }); } - return {formContent}; + return <>{formContent}; }; export default AddFenceDeivceForm; diff --git a/striker-ui/types/APIFence.d.ts b/striker-ui/types/APIFence.d.ts index e44d522d..2ce11650 100644 --- a/striker-ui/types/APIFence.d.ts +++ b/striker-ui/types/APIFence.d.ts @@ -1,11 +1,18 @@ +type FenceParameterType = + | 'boolean' + | 'integer' + | 'second' + | 'select' + | 'string'; + type APIFenceTemplate = { [fenceId: string]: { actions: string[]; description: string; parameters: { [parameterId: string]: { - content_type: 'boolean' | 'integer' | 'second' | 'select' | 'string'; - default: string; + content_type: FenceParameterType; + default?: string; deprecated: number; description: string; obsoletes: number; From 4b52de463ea21ee61b361e420558b6b0ea9344e0 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 17 Feb 2023 00:19:48 -0500 Subject: [PATCH 09/75] fix(striker-ui): separate types from Select component --- striker-ui/components/Select.tsx | 102 ++++++++++++++----------------- striker-ui/types/Select.d.ts | 5 ++ 2 files changed, 50 insertions(+), 57 deletions(-) create mode 100644 striker-ui/types/Select.d.ts diff --git a/striker-ui/components/Select.tsx b/striker-ui/components/Select.tsx index 9e037ae8..fffbc715 100644 --- a/striker-ui/components/Select.tsx +++ b/striker-ui/components/Select.tsx @@ -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 = ({ + onClearIndicatorClick, + ...muiSelectProps +}) => { + const { sx: selectSx, value, ...restMuiSelectProps } = muiSelectProps; -const SELECT_DEFAULT_PROPS: Required = { - onClearIndicatorClick: null, -}; - -const Select: FC = (selectProps) => { - const { - onClearIndicatorClick = SELECT_DEFAULT_PROPS.onClearIndicatorClick, - ...muiSelectProps - } = selectProps; - const { children, sx, value } = muiSelectProps; - const clearIndicator: JSX.Element | undefined = - String(value).length > 0 && onClearIndicatorClick ? ( - - - - - - ) : 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 && ( + + + + + + ), + [onClearIndicatorClick, value], + ); return ( - {children} - + endAdornment={clearIndicatorElement} + value={value} + {...restMuiSelectProps} + sx={combinedSx} + /> ); }; -Select.defaultProps = SELECT_DEFAULT_PROPS; - -export type { SelectProps }; - export default Select; diff --git a/striker-ui/types/Select.d.ts b/striker-ui/types/Select.d.ts new file mode 100644 index 00000000..6b656304 --- /dev/null +++ b/striker-ui/types/Select.d.ts @@ -0,0 +1,5 @@ +type SelectOptionalProps = { + onClearIndicatorClick?: import('@mui/material').IconButtonProps['onClick']; +}; + +type SelectProps = import('@mui/material').SelectProps & SelectOptionalProps; From 5e5c767670cdcf01297ff007cdd80a68587a05c2 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 17 Feb 2023 00:28:21 -0500 Subject: [PATCH 10/75] fix(striker-ui): separate types from SelectWithLabel component --- .../OutlinedLabeledInputWithSelect.tsx | 18 ++--- striker-ui/components/SelectWithLabel.tsx | 67 ++++++------------- striker-ui/types/SelectWithLabel.d.ts | 22 ++++++ 3 files changed, 46 insertions(+), 61 deletions(-) diff --git a/striker-ui/components/OutlinedLabeledInputWithSelect.tsx b/striker-ui/components/OutlinedLabeledInputWithSelect.tsx index c473810c..2dce2deb 100644 --- a/striker-ui/components/OutlinedLabeledInputWithSelect.tsx +++ b/striker-ui/components/OutlinedLabeledInputWithSelect.tsx @@ -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; @@ -66,19 +66,11 @@ const OutlinedLabeledInputWithSelect: FC< }, }} > - + diff --git a/striker-ui/components/SelectWithLabel.tsx b/striker-ui/components/SelectWithLabel.tsx index 8da8980f..e2a343d6 100644 --- a/striker-ui/components/SelectWithLabel.tsx +++ b/striker-ui/components/SelectWithLabel.tsx @@ -1,48 +1,15 @@ -import { FC, useCallback, useMemo } 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; - label?: string | null; - messageBoxProps?: Partial; - selectProps?: Partial; -}; - -type SelectWithLabelProps = SelectWithLabelOptionalProps & { - id: string; - selectItems: Array; -}; - -const SELECT_WITH_LABEL_DEFAULT_PROPS: Required = - { - 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 = ({ id, @@ -51,14 +18,19 @@ const SelectWithLabel: FC = ({ checkItem, disableItem, hideItem, - inputLabelProps, - isReadOnly, - messageBoxProps, - selectProps = {}, - isCheckableItems = selectProps?.multiple, + inputLabelProps = {}, + isReadOnly = false, + messageBoxProps = {}, + onChange, + selectProps: { + multiple: selectMultiple, + sx: selectSx, + ...restSelectProps + } = {}, + value: selectValue, + // Props with initial value that depend on others. + isCheckableItems = selectMultiple, }) => { - const { sx: selectSx } = selectProps; - const combinedSx = useMemo( () => isReadOnly @@ -124,8 +96,11 @@ const SelectWithLabel: FC = ({ boolean; type SelectWithLabelOptionalProps = { checkItem?: OperateSelectItemFunction; disableItem?: OperateSelectItemFunction; + formControlProps?: import('@mui/material').FormControlProps; hideItem?: OperateSelectItemFunction; isCheckableItems?: boolean; isReadOnly?: boolean; From 5b03bcca5c1194fc9512d1d21ddd2d00e30a1140 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 22 Feb 2023 22:19:37 -0500 Subject: [PATCH 35/75] fix(striker-ui): expand OutlinedInputWithLabel to full width --- striker-ui/components/OutlinedInputWithLabel.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/striker-ui/components/OutlinedInputWithLabel.tsx b/striker-ui/components/OutlinedInputWithLabel.tsx index ca16c777..7ded6bfe 100644 --- a/striker-ui/components/OutlinedInputWithLabel.tsx +++ b/striker-ui/components/OutlinedInputWithLabel.tsx @@ -132,6 +132,7 @@ const OutlinedInputWithLabel: FC = ({ return ( From f928da854e84765d706b23a4940f72b02f1ea590 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Mon, 27 Feb 2023 18:16:51 -0500 Subject: [PATCH 36/75] fix(striker-ui): add growFirst and fullWidth switches to FlexBox --- striker-ui/components/FlexBox.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/striker-ui/components/FlexBox.tsx b/striker-ui/components/FlexBox.tsx index 4a2f8899..08f6750f 100644 --- a/striker-ui/components/FlexBox.tsx +++ b/striker-ui/components/FlexBox.tsx @@ -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 & FlexBoxOptionalPropsWithoutDefault = { columnSpacing: undefined, + fullWidth: false, + growFirst: false, row: false, rowSpacing: undefined, lg: undefined, @@ -39,6 +43,8 @@ const FLEX_BOX_DEFAULT_PROPS: Required & }; const FlexBox: FC = ({ + 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 = ({ // 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 = ({ }), [columnSpacing, rowSpacing], ); + const firstChildFlexGrow = useMemo( + () => (growFirst ? 1 : undefined), + [growFirst], + ); + const width = useMemo(() => (fullWidth ? '100%' : undefined), [fullWidth]); return ( = ({ }, display: 'flex', flexDirection: { xs, sm, md, lg, xl }, + width, + + '& > :first-child': { + flexGrow: firstChildFlexGrow, + }, '& > :not(:first-child)': { marginLeft: { From f62342317d4525b89ee1614650d67ca01913eea1 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Mon, 27 Feb 2023 18:19:59 -0500 Subject: [PATCH 37/75] fix(striker-ui): add input sensitivity and expose tooltip props in CommonFenceInputGroup --- .../components/CommonFenceInputGroup.tsx | 184 ++++++++++++------ striker-ui/types/CommonFenceInputGroup.d.ts | 10 +- 2 files changed, 131 insertions(+), 63 deletions(-) diff --git a/striker-ui/components/CommonFenceInputGroup.tsx b/striker-ui/components/CommonFenceInputGroup.tsx index b1fb333d..95756237 100644 --- a/striker-ui/components/CommonFenceInputGroup.tsx +++ b/striker-ui/components/CommonFenceInputGroup.tsx @@ -1,72 +1,105 @@ +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 = ['1', 'on']; const ID_SEPARATOR = '-'; const MAP_TO_INPUT_BUILDER: MapToInputBuilder = { - boolean: ({ id, isChecked = false, label, name = id }) => ( - - } - valueType="boolean" - /> - ), - select: ({ - id, - isRequired, - label, - name = id, - selectOptions = [], - value = '', - }) => ( - - } - required={isRequired} - /> - ), - string: ({ id, isRequired, label = '', name = id, value }) => ( - - } - required={isRequired} - /> - ), + boolean: (args) => { + const { id, isChecked = false, label, name = id } = args; + + return ( + + } + valueType="boolean" + /> + ); + }, + select: (args) => { + const { + id, + isRequired, + label, + name = id, + selectOptions = [], + value = '', + } = args; + + return ( + + } + required={isRequired} + /> + ); + }, + string: (args) => { + const { + id, + isRequired, + isSensitive = false, + label = '', + name = id, + value, + } = args; + + return ( + + } + required={isRequired} + /> + ); + }, }; const combineIds = (...pieces: string[]) => pieces.join(ID_SEPARATOR); +const FenceInputWrapper = styled(FlexBox)({ + margin: '.4em 0', +}); + const CommonFenceInputGroup: FC = ({ fenceId, + fenceParameterTooltipProps, fenceTemplate, previousFenceName, previousFenceParameters, @@ -103,41 +136,64 @@ const CommonFenceInputGroup: FC = ({ [ parameterId, { - content_type: contentType, + content_type: parameterType, default: parameterDefault, - deprecated: rawDeprecated, + deprecated: rawParameterDeprecated, + description: parameterDescription, options: parameterSelectOptions, - required: rawRequired, + required: rawParameterRequired, }, ], ) => { - const isParameterDeprecated = String(rawDeprecated) === '1'; + const isParameterDeprecated = + String(rawParameterDeprecated) === '1'; if (!isParameterDeprecated) { const { optional, required } = previous; const buildInput = - MAP_TO_INPUT_BUILDER[contentType] ?? + MAP_TO_INPUT_BUILDER[parameterType] ?? MAP_TO_INPUT_BUILDER.string; const fenceJoinParameterId = combineIds(fenceId, parameterId); const initialValue = mapToPreviousFenceParameterValues[fenceJoinParameterId] ?? parameterDefault; - const isParameterRequired = String(rawRequired) === '1'; + 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 = ( + {parameterDescription}} + {...fenceParameterTooltipProps} + > + {parameterInput} + + ); if (isParameterRequired) { - required.push(parameterInput); + required.push(parameterInputWithTooltip); } else { - optional.push(parameterInput); + optional.push(parameterInputWithTooltip); } } @@ -164,17 +220,23 @@ const CommonFenceInputGroup: FC = ({ }} > - {requiredInputs} + {requiredInputs} - {optionalInputs} + {optionalInputs} ); } return result; - }, [fenceId, fenceTemplate, previousFenceName, previousFenceParameters]); + }, [ + fenceId, + fenceParameterTooltipProps, + fenceTemplate, + previousFenceName, + previousFenceParameters, + ]); return <>{fenceParameterElements}; }; diff --git a/striker-ui/types/CommonFenceInputGroup.d.ts b/striker-ui/types/CommonFenceInputGroup.d.ts index fca8a606..4e1ca15d 100644 --- a/striker-ui/types/CommonFenceInputGroup.d.ts +++ b/striker-ui/types/CommonFenceInputGroup.d.ts @@ -1,12 +1,17 @@ -type FenceParameterInputBuilder = (args: { +type FenceParameterInputBuilderParameters = { id: string; isChecked?: boolean; isRequired?: boolean; + isSensitive?: boolean; label?: string; name?: string; selectOptions?: string[]; value?: string; -}) => ReactElement; +}; + +type FenceParameterInputBuilder = ( + args: FenceParameterInputBuilderParameters, +) => ReactElement; type MapToInputBuilder = Partial< Record, FenceParameterInputBuilder> @@ -17,6 +22,7 @@ type CommonFenceInputGroupOptionalProps = { fenceTemplate?: APIFenceTemplate; previousFenceName?: string; previousFenceParameters?: FenceParameters; + fenceParameterTooltipProps?: import('@mui/material').TooltipProps; }; type CommonFenceInputGroupProps = CommonFenceInputGroupOptionalProps; From 53ae8ecdbd0ccd5aa4368a9016bc9bdc1cf96e69 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 01:31:18 -0500 Subject: [PATCH 38/75] fix(striker-ui): make IconButton change based on state --- .../components/IconButton/IconButton.tsx | 121 ++++++++++++++---- striker-ui/types/IconButton.d.ts | 16 +++ 2 files changed, 109 insertions(+), 28 deletions(-) create mode 100644 striker-ui/types/IconButton.d.ts diff --git a/striker-ui/components/IconButton/IconButton.tsx b/striker-ui/components/IconButton/IconButton.tsx index fb22451a..2782db4a 100644 --- a/striker-ui/components/IconButton/IconButton.tsx +++ b/striker-ui/components/IconButton/IconButton.tsx @@ -1,9 +1,16 @@ -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, @@ -13,35 +20,93 @@ import { 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: TEXT, + }, + + [`&.${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 = { + contained: ContainedIconButton, + normal: NormalIconButton, +}; const IconButton: FC = ({ children, - sx, - ...iconButtonRestProps -}) => ( - - {children} - -); + defaultIcon, + iconProps, + mapPreset, + mapToIcon: externalMapToIcon, + state, + variant = 'contained', + ...restIconButtonProps +}) => { + const mapToIcon = useMemo( + () => 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; diff --git a/striker-ui/types/IconButton.d.ts b/striker-ui/types/IconButton.d.ts new file mode 100644 index 00000000..af8ecf5a --- /dev/null +++ b/striker-ui/types/IconButton.d.ts @@ -0,0 +1,16 @@ +type CreatableComponent = Parameters[0]; + +type IconButtonPresetMapToStateIcon = 'edit' | 'visibility'; + +type IconButtonMapToStateIcon = Record; + +type IconButtonVariant = 'contained' | 'normal'; + +type IconButtonOptionalProps = { + defaultIcon?: CreatableComponent; + iconProps?: import('@mui/material').SvgIconProps; + mapPreset?: IconButtonPresetMapToStateIcon; + mapToIcon?: IconButtonMapToStateIcon; + state?: string; + variant?: IconButtonVariant; +}; From 92b722c118220bd9ac3554b65a95aa051857abf9 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 01:33:18 -0500 Subject: [PATCH 39/75] feat(striker-ui): add SensitiveText --- striker-ui/components/Text/InlineMonoText.tsx | 9 +-- striker-ui/components/Text/MonoText.tsx | 6 +- striker-ui/components/Text/SensitiveText.tsx | 64 +++++++++++++++++++ striker-ui/components/Text/SmallText.tsx | 6 +- striker-ui/components/Text/index.tsx | 10 ++- striker-ui/types/SensitiveText.d.ts | 7 ++ 6 files changed, 83 insertions(+), 19 deletions(-) create mode 100644 striker-ui/components/Text/SensitiveText.tsx create mode 100644 striker-ui/types/SensitiveText.d.ts diff --git a/striker-ui/components/Text/InlineMonoText.tsx b/striker-ui/components/Text/InlineMonoText.tsx index 2032152f..85823d5c 100644 --- a/striker-ui/components/Text/InlineMonoText.tsx +++ b/striker-ui/components/Text/InlineMonoText.tsx @@ -3,12 +3,7 @@ import { FC } from 'react'; import { BodyTextProps } from './BodyText'; import SmallText from './SmallText'; -type InlineMonoTextProps = BodyTextProps; - -const InlineMonoText: FC = ({ - sx, - ...bodyTextRestProps -}) => ( +const InlineMonoText: FC = ({ sx, ...bodyTextRestProps }) => ( = ({ /> ); -export type { InlineMonoTextProps }; - export default InlineMonoText; diff --git a/striker-ui/components/Text/MonoText.tsx b/striker-ui/components/Text/MonoText.tsx index 2c409219..c2cffa45 100644 --- a/striker-ui/components/Text/MonoText.tsx +++ b/striker-ui/components/Text/MonoText.tsx @@ -3,9 +3,7 @@ import { FC } from 'react'; import { BodyTextProps } from './BodyText'; import SmallText from './SmallText'; -type MonoTextProps = BodyTextProps; - -const MonoText: FC = ({ sx, ...bodyTextRestProps }) => ( +const MonoText: FC = ({ sx, ...bodyTextRestProps }) => ( = ({ sx, ...bodyTextRestProps }) => ( /> ); -export type { MonoTextProps }; - export default MonoText; diff --git a/striker-ui/components/Text/SensitiveText.tsx b/striker-ui/components/Text/SensitiveText.tsx new file mode 100644 index 00000000..16aa3509 --- /dev/null +++ b/striker-ui/components/Text/SensitiveText.tsx @@ -0,0 +1,64 @@ +import { createElement, FC, ReactNode, useMemo, useState } from 'react'; + +import BodyText from './BodyText'; +import FlexBox from '../FlexBox'; +import IconButton from '../IconButton'; +import MonoText from './MonoText'; + +const SensitiveText: FC = ({ + children, + monospaced: isMonospaced = false, + revealInitially: isRevealInitially = false, + textProps, +}) => { + const [isReveal, setIsReveal] = useState(isRevealInitially); + + const textElementType = useMemo( + () => (isMonospaced ? MonoText : BodyText), + [isMonospaced], + ); + const contentElement = useMemo(() => { + let content: ReactNode; + + if (isReveal) { + content = + typeof children === 'string' + ? createElement( + textElementType, + { + sx: { + lineHeight: 2.8, + maxWidth: '20em', + overflowY: 'scroll', + whiteSpace: 'nowrap', + }, + ...textProps, + }, + children, + ) + : children; + } else { + content = createElement(textElementType, textProps, '*****'); + } + + return content; + }, [children, isReveal, textElementType, textProps]); + + return ( + + {contentElement} + { + setIsReveal((previous) => !previous); + }} + state={String(isReveal)} + sx={{ marginRight: '-.2em', padding: '.2em' }} + variant="normal" + /> + + ); +}; + +export default SensitiveText; diff --git a/striker-ui/components/Text/SmallText.tsx b/striker-ui/components/Text/SmallText.tsx index 9fdc7bb1..bfe56ee7 100644 --- a/striker-ui/components/Text/SmallText.tsx +++ b/striker-ui/components/Text/SmallText.tsx @@ -2,12 +2,8 @@ import { FC } from 'react'; import BodyText, { BodyTextProps } from './BodyText'; -type SmallTextProps = BodyTextProps; - -const SmallText: FC = ({ ...bodyTextRestProps }) => ( +const SmallText: FC = ({ ...bodyTextRestProps }) => ( ); -export type { SmallTextProps }; - export default SmallText; diff --git a/striker-ui/components/Text/index.tsx b/striker-ui/components/Text/index.tsx index 13f0260b..f72cbbb2 100644 --- a/striker-ui/components/Text/index.tsx +++ b/striker-ui/components/Text/index.tsx @@ -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, +}; diff --git a/striker-ui/types/SensitiveText.d.ts b/striker-ui/types/SensitiveText.d.ts new file mode 100644 index 00000000..2a7921cb --- /dev/null +++ b/striker-ui/types/SensitiveText.d.ts @@ -0,0 +1,7 @@ +type SensitiveTextOptionalProps = { + monospaced?: boolean; + revealInitially?: boolean; + textProps?: import('../components/Text').BodyTextProps; +}; + +type SensitiveTextProps = SensitiveTextOptionalProps; From 24aef66c31bccbdbc3aa1f8c7f8c09cacf27424a Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 01:34:52 -0500 Subject: [PATCH 40/75] fix(striker-ui): add confirm dialog to ManageFencesPanel --- striker-ui/components/ManageFencesPanel.tsx | 164 +++++++++++++++++--- 1 file changed, 144 insertions(+), 20 deletions(-) diff --git a/striker-ui/components/ManageFencesPanel.tsx b/striker-ui/components/ManageFencesPanel.tsx index 081e65c2..6b52d587 100644 --- a/striker-ui/components/ManageFencesPanel.tsx +++ b/striker-ui/components/ManageFencesPanel.tsx @@ -1,4 +1,12 @@ -import { FC, FormEventHandler, useMemo, useRef, useState } from 'react'; +import { Box } from '@mui/material'; +import { + FC, + FormEventHandler, + ReactElement, + useMemo, + useRef, + useState, +} from 'react'; import API_BASE_URL from '../lib/consts/API_BASE_URL'; @@ -13,9 +21,29 @@ import List from './List'; import { Panel, PanelHeader } from './Panels'; import periodicFetch from '../lib/fetchers/periodicFetch'; import Spinner from './Spinner'; -import { BodyText, HeaderText, InlineMonoText } from './Text'; +import { + BodyText, + HeaderText, + InlineMonoText, + MonoText, + SmallText, +} from './Text'; import useIsFirstRender from '../hooks/useIsFirstRender'; import useProtectedState from '../hooks/useProtectedState'; +import SensitiveText from './Text/SensitiveText'; + +type FormFenceParameterData = { + fenceAgent: string; + fenceName: string; + parameterInputs: { + [parameterInputId: string]: { + isParameterSensitive: boolean; + parameterId: string; + parameterType: string; + parameterValue: string; + }; + }; +}; const fenceParameterBooleanToString = (value: boolean) => (value ? '1' : '0'); @@ -25,12 +53,7 @@ const getFormFenceParameters = ( ) => { const { elements } = target as HTMLFormElement; - return Object.values(elements).reduce<{ - fenceAgent: string; - parameters: { - [parameterId: string]: { type: string; value: string }; - }; - }>( + return Object.values(elements).reduce( (previous, formElement) => { const { id: inputId } = formElement; const reExtract = new RegExp(`^(fence[^-]+)${ID_SEPARATOR}([^\\s]+)$`); @@ -42,7 +65,16 @@ const getFormFenceParameters = ( previous.fenceAgent = fenceId; const inputElement = formElement as HTMLInputElement; - const { checked, value } = inputElement; + const { + checked, + dataset: { sensitive: rawSensitive }, + value, + } = inputElement; + + if (parameterId === 'name') { + previous.fenceName = value; + } + const { [fenceId]: { parameters: { @@ -51,9 +83,11 @@ const getFormFenceParameters = ( }, } = fenceTemplate; - previous.parameters[parameterId] = { - type: parameterType, - value: + previous.parameterInputs[inputId] = { + isParameterSensitive: rawSensitive === 'true', + parameterId, + parameterType, + parameterValue: parameterType === 'boolean' ? fenceParameterBooleanToString(checked) : value, @@ -62,14 +96,58 @@ const getFormFenceParameters = ( return previous; }, - { fenceAgent: '', parameters: {} }, + { fenceAgent: '', fenceName: '', parameterInputs: {} }, ); }; +const buildConfirmFenceParameters = ( + parameterInputs: FormFenceParameterData['parameterInputs'], +) => ( + { + let textElement: ReactElement; + + if (parameterValue) { + textElement = isParameterSensitive ? ( + {parameterValue} + ) : ( + + + {parameterValue} + + + ); + } else { + textElement = none; + } + + return ( + + {parameterId} + {textElement} + + ); + }} + /> +); + const ManageFencesPanel: FC = () => { const isFirstRender = useIsFirstRender(); const confirmDialogRef = useRef({}); + const formDialogRef = useRef({}); const [confirmDialogProps, setConfirmDialogProps] = useState({ @@ -77,6 +155,11 @@ const ManageFencesPanel: FC = () => { content: '', titleText: '', }); + const [formDialogProps, setFormDialogProps] = useState({ + actionProceedText: '', + content: '', + titleText: '', + }); const [fenceTemplate, setFenceTemplate] = useProtectedState< APIFenceTemplate | undefined >(undefined); @@ -98,7 +181,7 @@ const ManageFencesPanel: FC = () => { header listItems={fenceOverviews} onAdd={() => { - setConfirmDialogProps({ + setFormDialogProps({ actionProceedText: 'Add', content: , onSubmitAppend: (event) => { @@ -106,18 +189,34 @@ const ManageFencesPanel: FC = () => { return; } - getFormFenceParameters(fenceTemplate, event); + const addData = getFormFenceParameters(fenceTemplate, event); + + setConfirmDialogProps({ + actionProceedText: 'Add', + content: buildConfirmFenceParameters(addData.parameterInputs), + titleText: ( + + Add a{' '} + + {addData.fenceAgent} + {' '} + fence device with the following parameters? + + ), + }); + + confirmDialogRef.current.setOpen?.call(null, true); }, titleText: 'Add a fence device', }); - confirmDialogRef.current.setOpen?.call(null, true); + formDialogRef.current.setOpen?.call(null, true); }} onEdit={() => { setIsEditFences((previous) => !previous); }} onItemClick={({ fenceAgent: fenceId, fenceName, fenceParameters }) => { - setConfirmDialogProps({ + setFormDialogProps({ actionProceedText: 'Update', content: ( { return; } - getFormFenceParameters(fenceTemplate, event); + const editData = getFormFenceParameters(fenceTemplate, event); + + setConfirmDialogProps({ + actionProceedText: 'Update', + content: buildConfirmFenceParameters(editData.parameterInputs), + titleText: ( + + Update{' '} + + {editData.fenceName} + {' '} + fence device with the following parameters? + + ), + }); + + confirmDialogRef.current.setOpen?.call(null, true); }, titleText: ( Update fence device{' '} - {fenceName}{' '} + {fenceName}{' '} parameters ), }); - confirmDialogRef.current.setOpen?.call(null, true); + formDialogRef.current.setOpen?.call(null, true); }} renderListItem={( fenceUUID, @@ -201,6 +316,15 @@ const ManageFencesPanel: FC = () => { PaperProps: { sx: { minWidth: { xs: '90%', md: '50em' } } }, }} formContent + scrollBoxProps={{ + padding: '.3em .5em', + }} + scrollContent + {...formDialogProps} + ref={formDialogRef} + /> + Date: Tue, 28 Feb 2023 14:03:37 -0500 Subject: [PATCH 41/75] refactor(striker-ui): group manage fence related components --- .../{ => ManageFence}/AddFenceInputGroup.tsx | 8 ++--- .../CommonFenceInputGroup.tsx | 18 +++++------ .../{ => ManageFence}/EditFenceInputGroup.tsx | 2 +- .../ManageFencePanel.tsx} | 30 +++++++++---------- striker-ui/components/ManageFence/index.tsx | 3 ++ striker-ui/pages/manage-element/index.tsx | 26 ++++++++++++---- 6 files changed, 52 insertions(+), 35 deletions(-) rename striker-ui/components/{ => ManageFence}/AddFenceInputGroup.tsx (94%) rename striker-ui/components/{ => ManageFence}/CommonFenceInputGroup.tsx (94%) rename striker-ui/components/{ => ManageFence}/EditFenceInputGroup.tsx (96%) rename striker-ui/components/{ManageFencesPanel.tsx => ManageFence/ManageFencePanel.tsx} (93%) create mode 100644 striker-ui/components/ManageFence/index.tsx diff --git a/striker-ui/components/AddFenceInputGroup.tsx b/striker-ui/components/ManageFence/AddFenceInputGroup.tsx similarity index 94% rename from striker-ui/components/AddFenceInputGroup.tsx rename to striker-ui/components/ManageFence/AddFenceInputGroup.tsx index 5f083fdf..8657a6d5 100644 --- a/striker-ui/components/AddFenceInputGroup.tsx +++ b/striker-ui/components/ManageFence/AddFenceInputGroup.tsx @@ -1,11 +1,11 @@ import { Box } from '@mui/material'; import { FC, useMemo, useState } from 'react'; -import Autocomplete from './Autocomplete'; +import Autocomplete from '../Autocomplete'; import CommonFenceInputGroup from './CommonFenceInputGroup'; -import FlexBox from './FlexBox'; -import Spinner from './Spinner'; -import { BodyText } from './Text'; +import FlexBox from '../FlexBox'; +import Spinner from '../Spinner'; +import { BodyText } from '../Text'; const AddFenceInputGroup: FC = ({ fenceTemplate: externalFenceTemplate, diff --git a/striker-ui/components/CommonFenceInputGroup.tsx b/striker-ui/components/ManageFence/CommonFenceInputGroup.tsx similarity index 94% rename from striker-ui/components/CommonFenceInputGroup.tsx rename to striker-ui/components/ManageFence/CommonFenceInputGroup.tsx index 95756237..9c70146e 100644 --- a/striker-ui/components/CommonFenceInputGroup.tsx +++ b/striker-ui/components/ManageFence/CommonFenceInputGroup.tsx @@ -1,15 +1,15 @@ 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'; +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 = ['1', 'on']; const ID_SEPARATOR = '-'; diff --git a/striker-ui/components/EditFenceInputGroup.tsx b/striker-ui/components/ManageFence/EditFenceInputGroup.tsx similarity index 96% rename from striker-ui/components/EditFenceInputGroup.tsx rename to striker-ui/components/ManageFence/EditFenceInputGroup.tsx index 2c382b54..f9989d14 100644 --- a/striker-ui/components/EditFenceInputGroup.tsx +++ b/striker-ui/components/ManageFence/EditFenceInputGroup.tsx @@ -1,7 +1,7 @@ import { FC, useMemo } from 'react'; import CommonFenceInputGroup from './CommonFenceInputGroup'; -import Spinner from './Spinner'; +import Spinner from '../Spinner'; const EditFenceInputGroup: FC = ({ fenceId, diff --git a/striker-ui/components/ManageFencesPanel.tsx b/striker-ui/components/ManageFence/ManageFencePanel.tsx similarity index 93% rename from striker-ui/components/ManageFencesPanel.tsx rename to striker-ui/components/ManageFence/ManageFencePanel.tsx index 6b52d587..efa60324 100644 --- a/striker-ui/components/ManageFencesPanel.tsx +++ b/striker-ui/components/ManageFence/ManageFencePanel.tsx @@ -8,29 +8,29 @@ import { useState, } from 'react'; -import API_BASE_URL from '../lib/consts/API_BASE_URL'; +import API_BASE_URL from '../../lib/consts/API_BASE_URL'; import AddFenceInputGroup from './AddFenceInputGroup'; -import api from '../lib/api'; +import api from '../../lib/api'; import { ID_SEPARATOR } from './CommonFenceInputGroup'; -import ConfirmDialog from './ConfirmDialog'; +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 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'; -import SensitiveText from './Text/SensitiveText'; +} from '../Text'; +import useIsFirstRender from '../../hooks/useIsFirstRender'; +import useProtectedState from '../../hooks/useProtectedState'; type FormFenceParameterData = { fenceAgent: string; @@ -143,7 +143,7 @@ const buildConfirmFenceParameters = ( /> ); -const ManageFencesPanel: FC = () => { +const ManageFencePanel: FC = () => { const isFirstRender = useIsFirstRender(); const confirmDialogRef = useRef({}); @@ -333,4 +333,4 @@ const ManageFencesPanel: FC = () => { ); }; -export default ManageFencesPanel; +export default ManageFencePanel; diff --git a/striker-ui/components/ManageFence/index.tsx b/striker-ui/components/ManageFence/index.tsx new file mode 100644 index 00000000..b2bd1a47 --- /dev/null +++ b/striker-ui/components/ManageFence/index.tsx @@ -0,0 +1,3 @@ +import ManageFencePanel from './ManageFencePanel'; + +export default ManageFencePanel; diff --git a/striker-ui/pages/manage-element/index.tsx b/striker-ui/pages/manage-element/index.tsx index 5add77d6..18eceb99 100644 --- a/striker-ui/pages/manage-element/index.tsx +++ b/striker-ui/pages/manage-element/index.tsx @@ -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 = { '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 = () => ( + , + ...STEP_CONTENT_GRID_CENTER_COLUMN, + }, + }} + /> +); + const ManageElement: FC = () => { const { isReady, @@ -162,7 +176,7 @@ const ManageElement: FC = () => { > - + @@ -171,8 +185,8 @@ const ManageElement: FC = () => { - - {} + + ); From 06425958a6432e7bf49c9e745505bb911df372c4 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 16:03:32 -0500 Subject: [PATCH 42/75] fix(striker-ui): align colouring in buttons --- striker-ui/components/ContainedButton.tsx | 16 ++++------------ striker-ui/components/IconButton/IconButton.tsx | 3 +-- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/striker-ui/components/ContainedButton.tsx b/striker-ui/components/ContainedButton.tsx index bd807dee..97138438 100644 --- a/striker-ui/components/ContainedButton.tsx +++ b/striker-ui/components/ContainedButton.tsx @@ -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 = ({ sx, ...restProps }) => { const combinedSx = useMemo>( () => ({ - backgroundColor: TEXT, + backgroundColor: GREY, color: BLACK, textTransform: 'none', '&:hover': { - backgroundColor: GREY, + backgroundColor: `${GREY}F0`, }, ...sx, @@ -19,15 +19,7 @@ const ContainedButton: FC = ({ sx, ...restProps }) => { [sx], ); - return ( - - ); + return ; }; export default ContainedButton; diff --git a/striker-ui/components/IconButton/IconButton.tsx b/striker-ui/components/IconButton/IconButton.tsx index 2782db4a..1eae04e5 100644 --- a/striker-ui/components/IconButton/IconButton.tsx +++ b/striker-ui/components/IconButton/IconButton.tsx @@ -17,7 +17,6 @@ import { BORDER_RADIUS, DISABLED, GREY, - TEXT, } from '../../lib/consts/DEFAULT_THEME'; type IconButtonProps = IconButtonOptionalProps & MUIIconButtonProps; @@ -28,7 +27,7 @@ const ContainedIconButton = styled(MUIIconButton)({ color: BLACK, '&:hover': { - backgroundColor: TEXT, + backgroundColor: `${GREY}F0`, }, [`&.${muiInputClasses.disabled}`]: { From 476352ba0ede4eee0f17ce1aad733f7fafece52f Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 16:08:22 -0500 Subject: [PATCH 43/75] fix(striker-ui): add inline type SensitiveText and BodyText --- striker-ui/components/Text/BodyText.tsx | 54 ++++++------ striker-ui/components/Text/SensitiveText.tsx | 87 +++++++++++++++----- striker-ui/lib/consts/DEFAULT_THEME.ts | 1 + striker-ui/types/SensitiveText.d.ts | 1 + 4 files changed, 101 insertions(+), 42 deletions(-) diff --git a/striker-ui/components/Text/BodyText.tsx b/striker-ui/components/Text/BodyText.tsx index 8fef39e9..434727ed 100644 --- a/striker-ui/components/Text/BodyText.tsx +++ b/striker-ui/components/Text/BodyText.tsx @@ -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 = { inheritColour: false, + inline: false, inverted: false, monospaced: false, selected: true, @@ -68,6 +70,7 @@ const BodyText: FC = ({ 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 = ({ text = BODY_TEXT_DEFAULT_PROPS.text, ...muiTypographyRestProps }) => { + const sxDisplay = useMemo( + () => (isInline ? 'inline' : undefined), + [isInline], + ); + const baseClassName = useMemo( () => buildBodyTextClasses({ @@ -89,30 +97,30 @@ const BodyText: FC = ({ return ( {content} diff --git a/striker-ui/components/Text/SensitiveText.tsx b/striker-ui/components/Text/SensitiveText.tsx index 16aa3509..2d1ca11c 100644 --- a/striker-ui/components/Text/SensitiveText.tsx +++ b/striker-ui/components/Text/SensitiveText.tsx @@ -1,18 +1,51 @@ -import { createElement, FC, ReactNode, useMemo, useState } from 'react'; +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 = ({ children, + inline: isInline = false, monospaced: isMonospaced = false, revealInitially: isRevealInitially = false, textProps, }) => { const [isReveal, setIsReveal] = useState(isRevealInitially); + const clickEventHandler = useCallback(() => { + setIsReveal((previous) => !previous); + }, []); + + const textSxLineHeight = useMemo( + () => (isInline ? undefined : 2.8), + [isInline], + ); + const textElementType = useMemo( () => (isMonospaced ? MonoText : BodyText), [isMonospaced], @@ -27,7 +60,7 @@ const SensitiveText: FC = ({ textElementType, { sx: { - lineHeight: 2.8, + lineHeight: textSxLineHeight, maxWidth: '20em', overflowY: 'scroll', whiteSpace: 'nowrap', @@ -38,27 +71,43 @@ const SensitiveText: FC = ({ ) : children; } else { - content = createElement(textElementType, textProps, '*****'); + content = createElement( + textElementType, + { + sx: { + lineHeight: textSxLineHeight, + }, + ...textProps, + }, + '*****', + ); } return content; - }, [children, isReveal, textElementType, textProps]); - - return ( - - {contentElement} - { - setIsReveal((previous) => !previous); - }} - state={String(isReveal)} - sx={{ marginRight: '-.2em', padding: '.2em' }} - variant="normal" - /> - + }, [children, isReveal, textElementType, textProps, textSxLineHeight]); + const rootElement = useMemo( + () => + isInline ? ( + + {contentElement} + + ) : ( + + {contentElement} + + + ), + [clickEventHandler, contentElement, isInline, isReveal], ); + + return rootElement; }; export default SensitiveText; diff --git a/striker-ui/lib/consts/DEFAULT_THEME.ts b/striker-ui/lib/consts/DEFAULT_THEME.ts index 6c0b27c4..e226100f 100644 --- a/striker-ui/lib/consts/DEFAULT_THEME.ts +++ b/striker-ui/lib/consts/DEFAULT_THEME.ts @@ -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'; diff --git a/striker-ui/types/SensitiveText.d.ts b/striker-ui/types/SensitiveText.d.ts index 2a7921cb..65081fa5 100644 --- a/striker-ui/types/SensitiveText.d.ts +++ b/striker-ui/types/SensitiveText.d.ts @@ -1,4 +1,5 @@ type SensitiveTextOptionalProps = { + inline?: boolean; monospaced?: boolean; revealInitially?: boolean; textProps?: import('../components/Text').BodyTextProps; From c7b0186b62e32f82b7ca0d2ab646d5e100b99edf Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 16:10:53 -0500 Subject: [PATCH 44/75] fix(striker-ui): hide password related in fence list --- .../ManageFence/ManageFencePanel.tsx | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/striker-ui/components/ManageFence/ManageFencePanel.tsx b/striker-ui/components/ManageFence/ManageFencePanel.tsx index efa60324..212d181a 100644 --- a/striker-ui/components/ManageFence/ManageFencePanel.tsx +++ b/striker-ui/components/ManageFence/ManageFencePanel.tsx @@ -3,6 +3,7 @@ import { FC, FormEventHandler, ReactElement, + ReactNode, useMemo, useRef, useState, @@ -267,9 +268,28 @@ const ManageFencePanel: FC = () => { {fenceName} - {Object.entries(fenceParameters).reduce( - (previous, [parameterId, parameterValue]) => - `${previous} ${parameterId}="${parameterValue}"`, + {Object.entries(fenceParameters).reduce( + (previous, [parameterId, parameterValue]) => { + let current: ReactNode = <>{parameterId}="; + + current = /passw/i.test(parameterId) ? ( + <> + {current} + {parameterValue} + + ) : ( + <> + {current} + {parameterValue} + + ); + + return ( + <> + {previous} {current}" + + ); + }, fenceAgent, )} From 508c7aa1f5b6e3b567f9ecd5370d525dadf8d921 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 16:40:49 -0500 Subject: [PATCH 45/75] fix(striker-ui-api): make getAnvilData generic to identify return value --- striker-ui-api/src/lib/accessModule.ts | 4 +-- .../fence/getFenceTemplate.ts | 2 +- .../host/createHostConnection.ts | 2 +- .../host/getHostConnection.ts | 8 +++--- striker-ui-api/src/types/AnvilDataStruct.d.ts | 3 --- striker-ui-api/src/types/DatabaseHash.d.ts | 10 ------- .../src/types/GetAnvilDataFunction.d.ts | 26 +++++++++++++++++++ .../src/types/GetAnvilDataOptions.d.ts | 3 --- 8 files changed, 35 insertions(+), 23 deletions(-) delete mode 100644 striker-ui-api/src/types/AnvilDataStruct.d.ts delete mode 100644 striker-ui-api/src/types/DatabaseHash.d.ts create mode 100644 striker-ui-api/src/types/GetAnvilDataFunction.d.ts delete mode 100644 striker-ui-api/src/types/GetAnvilDataOptions.d.ts diff --git a/striker-ui-api/src/lib/accessModule.ts b/striker-ui-api/src/lib/accessModule.ts index 94bed8fb..f1b02268 100644 --- a/striker-ui-api/src/lib/accessModule.ts +++ b/striker-ui-api/src/lib/accessModule.ts @@ -134,10 +134,10 @@ const dbWrite = (query: string, options?: SpawnSyncOptions) => { return execAnvilAccessModule(['--query', query, '--mode', 'write'], options); }; -const getAnvilData = ( +const getAnvilData = ( dataStruct: AnvilDataStruct, { predata, ...spawnSyncOptions }: GetAnvilDataOptions = {}, -) => +): HashType => execAnvilAccessModule( [ '--predata', diff --git a/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts b/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts index e285cc1a..beec0a0e 100644 --- a/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts +++ b/striker-ui-api/src/lib/request_handlers/fence/getFenceTemplate.ts @@ -6,7 +6,7 @@ export const getFenceTemplate: RequestHandler = (request, response) => { let rawFenceData; try { - ({ fence_data: rawFenceData } = getAnvilData( + ({ fence_data: rawFenceData } = getAnvilData<{ fence_data: unknown }>( { fence_data: true }, { predata: [['Striker->get_fence_data']] }, )); diff --git a/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts index c688dc9c..38a66989 100644 --- a/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts +++ b/striker-ui-api/src/lib/request_handlers/host/createHostConnection.ts @@ -154,7 +154,7 @@ export const createHostConnection: RequestHandler< database: { [localHostUUID]: { port: rawLocalDBPort }, }, - } = getAnvilData({ database: true }) as { database: DatabaseHash }; + } = getAnvilData<{ database: AnvilDataDatabaseHash }>({ database: true }); localDBPort = sanitize(rawLocalDBPort, 'number'); } catch (subError) { diff --git a/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts b/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts index 8671ac32..cf5e5453 100644 --- a/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts +++ b/striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts @@ -7,7 +7,7 @@ import { stdout } from '../../shell'; const buildHostConnections = ( fromHostUUID: string, - databaseHash: DatabaseHash, + databaseHash: AnvilDataDatabaseHash, { defaultPort = 5432, defaultUser = 'admin', @@ -42,7 +42,7 @@ export const getHostConnection = buildGetRequestHandler( (request, buildQueryOptions) => { const { hostUUIDs: rawHostUUIDs } = request.query; - let rawDatabaseData: DatabaseHash; + let rawDatabaseData: AnvilDataDatabaseHash; const hostUUIDField = 'ip_add.ip_address_host_uuid'; const localHostUUID: string = getLocalHostUUID(); @@ -59,7 +59,9 @@ export const getHostConnection = buildGetRequestHandler( stdout(`condHostUUIDs=[${condHostUUIDs}]`); try { - ({ database: rawDatabaseData } = getAnvilData({ database: true })); + ({ database: rawDatabaseData } = getAnvilData<{ database: AnvilDataDatabaseHash }>( + { database: true }, + )); } catch (subError) { throw new Error(`Failed to get anvil data; CAUSE: ${subError}`); } diff --git a/striker-ui-api/src/types/AnvilDataStruct.d.ts b/striker-ui-api/src/types/AnvilDataStruct.d.ts deleted file mode 100644 index 984875f9..00000000 --- a/striker-ui-api/src/types/AnvilDataStruct.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -interface AnvilDataStruct { - [key: string]: AnvilDataStruct | boolean; -} diff --git a/striker-ui-api/src/types/DatabaseHash.d.ts b/striker-ui-api/src/types/DatabaseHash.d.ts deleted file mode 100644 index 1acbab2d..00000000 --- a/striker-ui-api/src/types/DatabaseHash.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -type DatabaseHash = { - [hostUUID: string]: { - host: string; - name: string; - password: string; - ping: string; - port: string; - user: string; - }; -}; diff --git a/striker-ui-api/src/types/GetAnvilDataFunction.d.ts b/striker-ui-api/src/types/GetAnvilDataFunction.d.ts new file mode 100644 index 00000000..191337b3 --- /dev/null +++ b/striker-ui-api/src/types/GetAnvilDataFunction.d.ts @@ -0,0 +1,26 @@ +interface AnvilDataStruct { + [key: string]: AnvilDataStruct | boolean; +} + +type AnvilDataDatabaseHash = { + [hostUUID: string]: { + host: string; + name: string; + password: string; + ping: string; + port: string; + user: string; + }; +}; + +type AnvilDataUPSHash = { + [upsName: string]: { + agent: string; + brand: string; + description: string; + }; +}; + +type GetAnvilDataOptions = import('child_process').SpawnSyncOptions & { + predata?: Array<[string, ...unknown[]]>; +}; diff --git a/striker-ui-api/src/types/GetAnvilDataOptions.d.ts b/striker-ui-api/src/types/GetAnvilDataOptions.d.ts deleted file mode 100644 index baf39d7a..00000000 --- a/striker-ui-api/src/types/GetAnvilDataOptions.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -type GetAnvilDataOptions = import('child_process').SpawnSyncOptions & { - predata?: Array<[string, ...unknown[]]>; -}; From f3a2a9355ee008083c0aa1d88799425fce41d395 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 16:42:09 -0500 Subject: [PATCH 46/75] feat(striker-ui-api): add /ups --- .../request_handlers/ups/getUPSTemplate.ts | 23 +++++++++++++++++++ .../src/lib/request_handlers/ups/index.ts | 1 + striker-ui-api/src/routes/index.ts | 2 ++ striker-ui-api/src/routes/ups.ts | 9 ++++++++ 4 files changed, 35 insertions(+) create mode 100644 striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts create mode 100644 striker-ui-api/src/lib/request_handlers/ups/index.ts create mode 100644 striker-ui-api/src/routes/ups.ts diff --git a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts new file mode 100644 index 00000000..b966f806 --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts @@ -0,0 +1,23 @@ +import { RequestHandler } from 'express'; + +import { getAnvilData } from '../../accessModule'; +import { stderr } from '../../shell'; + +export const getUPSTemplate: RequestHandler = (request, response) => { + let rawUPSData; + + try { + ({ ups_data: rawUPSData } = getAnvilData<{ ups_data: AnvilDataUPSHash }>( + { ups_data: true }, + { predata: [['Striker->get_ups_data']] }, + )); + } catch (subError) { + stderr(`Failed to get ups template; CAUSE: ${subError}`); + + response.status(500).send(); + + return; + } + + response.status(200).send(rawUPSData); +}; diff --git a/striker-ui-api/src/lib/request_handlers/ups/index.ts b/striker-ui-api/src/lib/request_handlers/ups/index.ts new file mode 100644 index 00000000..969616cb --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/ups/index.ts @@ -0,0 +1 @@ +export * from './getUPSTemplate'; diff --git a/striker-ui-api/src/routes/index.ts b/striker-ui-api/src/routes/index.ts index e37e78f8..618e952c 100644 --- a/striker-ui-api/src/routes/index.ts +++ b/striker-ui-api/src/routes/index.ts @@ -10,6 +10,7 @@ import jobRouter from './job'; import networkInterfaceRouter from './network-interface'; import serverRouter from './server'; import sshKeyRouter from './ssh-key'; +import upsRouter from './ups'; import userRouter from './user'; const routes: Readonly> = { @@ -23,6 +24,7 @@ const routes: Readonly> = { 'network-interface': networkInterfaceRouter, server: serverRouter, 'ssh-key': sshKeyRouter, + ups: upsRouter, user: userRouter, }; diff --git a/striker-ui-api/src/routes/ups.ts b/striker-ui-api/src/routes/ups.ts new file mode 100644 index 00000000..1d19b86f --- /dev/null +++ b/striker-ui-api/src/routes/ups.ts @@ -0,0 +1,9 @@ +import express from 'express'; + +import { getUPSTemplate } from '../lib/request_handlers/ups'; + +const router = express.Router(); + +router.get('/template', getUPSTemplate); + +export default router; From 01dc0db18df6601343b6a2573c5b5a240789b8e5 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 22:14:43 -0500 Subject: [PATCH 47/75] fix(striker-ui): add FormDialog --- striker-ui/components/FormDialog.tsx | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 striker-ui/components/FormDialog.tsx diff --git a/striker-ui/components/FormDialog.tsx b/striker-ui/components/FormDialog.tsx new file mode 100644 index 00000000..09630391 --- /dev/null +++ b/striker-ui/components/FormDialog.tsx @@ -0,0 +1,34 @@ +import { forwardRef, useMemo } from 'react'; + +import ConfirmDialog from './ConfirmDialog'; + +const FormDialog = forwardRef< + ConfirmDialogForwardedRefContent, + ConfirmDialogProps +>((props, ref) => { + const { scrollContent: isScrollContent } = props; + + const scrollBoxPaddingRight = useMemo( + () => (isScrollContent ? '.5em' : undefined), + [isScrollContent], + ); + + return ( + + ); +}); + +FormDialog.displayName = 'FormDialog'; + +export default FormDialog; From 47d4a29bd78aa4a960b98a9f8eea7a07ef3265a7 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 22:15:50 -0500 Subject: [PATCH 48/75] fix(striker-ui): simplify ConfirmDialogProps init --- striker-ui/hooks/useConfirmDialogProps.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 striker-ui/hooks/useConfirmDialogProps.ts diff --git a/striker-ui/hooks/useConfirmDialogProps.ts b/striker-ui/hooks/useConfirmDialogProps.ts new file mode 100644 index 00000000..c638a41d --- /dev/null +++ b/striker-ui/hooks/useConfirmDialogProps.ts @@ -0,0 +1,19 @@ +import { Dispatch, SetStateAction, useState } from 'react'; + +const useConfirmDialogProps = ({ + actionProceedText = '', + content = '', + titleText = '', + ...restProps +}: Partial = {}): [ + ConfirmDialogProps, + Dispatch>, +] => + useState({ + actionProceedText, + content, + titleText, + ...restProps, + }); + +export default useConfirmDialogProps; From 9bfd5ec672674eaaa3f271a964f6e8cb7c1a14f6 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 22:21:15 -0500 Subject: [PATCH 49/75] feat(striker-ui): add manage UPS related components --- .../components/ManageUps/AddUpsInputGroup.tsx | 78 ++++++++++++++++ .../ManageUps/CommonUpsInputGroup.tsx | 58 ++++++++++++ .../components/ManageUps/ManageUpsPanel.tsx | 88 +++++++++++++++++++ striker-ui/components/ManageUps/index.tsx | 3 + striker-ui/types/APIUps.d.ts | 7 ++ striker-ui/types/AddUpsInputGroup.d.ts | 6 ++ striker-ui/types/CommonUpsInputGroup.d.ts | 9 ++ 7 files changed, 249 insertions(+) create mode 100644 striker-ui/components/ManageUps/AddUpsInputGroup.tsx create mode 100644 striker-ui/components/ManageUps/CommonUpsInputGroup.tsx create mode 100644 striker-ui/components/ManageUps/ManageUpsPanel.tsx create mode 100644 striker-ui/components/ManageUps/index.tsx create mode 100644 striker-ui/types/APIUps.d.ts create mode 100644 striker-ui/types/AddUpsInputGroup.d.ts create mode 100644 striker-ui/types/CommonUpsInputGroup.d.ts diff --git a/striker-ui/components/ManageUps/AddUpsInputGroup.tsx b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx new file mode 100644 index 00000000..43e4c749 --- /dev/null +++ b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx @@ -0,0 +1,78 @@ +import { FC, ReactElement, useMemo, useState } from 'react'; + +import CommonUpsInputGroup from './CommonUpsInputGroup'; +import FlexBox from '../FlexBox'; +import SelectWithLabel from '../SelectWithLabel'; +import Spinner from '../Spinner'; +import { BodyText } from '../Text'; + +const AddUpsInputGroup: FC = ({ + loading: isExternalLoading, + upsTemplate, +}) => { + const [inputUpsAgentValue, setInputUpsAgentValue] = useState(''); + + const upsAgentOptions = useMemo( + () => + upsTemplate + ? Object.entries(upsTemplate).map( + ([upsTypeId, { brand, description }]) => ({ + displayValue: ( + + {brand} + {description} + + ), + value: upsTypeId, + }), + ) + : [], + [upsTemplate], + ); + + const pickUpsAgentElement = useMemo( + () => + upsTemplate && ( + { + const newValue = String(rawNewValue); + + setInputUpsAgentValue(newValue); + }} + selectItems={upsAgentOptions} + selectProps={{ + onClearIndicatorClick: () => { + setInputUpsAgentValue(''); + }, + renderValue: (rawValue) => { + const upsTypeId = String(rawValue); + const { brand } = upsTemplate[upsTypeId]; + + return brand; + }, + }} + value={inputUpsAgentValue} + /> + ), + [inputUpsAgentValue, upsAgentOptions, upsTemplate], + ); + const content = useMemo( + () => + isExternalLoading ? ( + + ) : ( + + {pickUpsAgentElement} + {inputUpsAgentValue && } + + ), + [inputUpsAgentValue, isExternalLoading, pickUpsAgentElement], + ); + + return content; +}; + +export default AddUpsInputGroup; diff --git a/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx b/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx new file mode 100644 index 00000000..15477906 --- /dev/null +++ b/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx @@ -0,0 +1,58 @@ +import { FC } from 'react'; + +import Grid from '../Grid'; +import InputWithRef from '../InputWithRef'; +import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; + +const CommonUpsInputGroup: FC = ({ + previous: { + hostName: previousHostName, + ipAddress: previousIpAddress, + upsName: previousUpsName, + } = {}, +}) => ( + <> + + } + required + /> + ), + }, + 'common-ups-input-cell-ip-address': { + children: ( + + } + required + /> + ), + }, + }} + spacing="1em" + /> + + +); + +export default CommonUpsInputGroup; diff --git a/striker-ui/components/ManageUps/ManageUpsPanel.tsx b/striker-ui/components/ManageUps/ManageUpsPanel.tsx new file mode 100644 index 00000000..99b2799a --- /dev/null +++ b/striker-ui/components/ManageUps/ManageUpsPanel.tsx @@ -0,0 +1,88 @@ +import { FC, useMemo, useRef, useState } from 'react'; + +import AddUpsInputGroup from './AddUpsInputGroup'; +import api from '../../lib/api'; +import ConfirmDialog from '../ConfirmDialog'; +import FormDialog from '../FormDialog'; +import handleAPIError from '../../lib/handleAPIError'; +import List from '../List'; +import { Panel, PanelHeader } from '../Panels'; +import Spinner from '../Spinner'; +import { HeaderText } from '../Text'; +import useConfirmDialogProps from '../../hooks/useConfirmDialogProps'; +import useIsFirstRender from '../../hooks/useIsFirstRender'; +import useProtectedState from '../../hooks/useProtectedState'; + +const ManageUpsPanel: FC = () => { + const isFirstRender = useIsFirstRender(); + + const confirmDialogRef = useRef({}); + const formDialogRef = useRef({}); + + const [confirmDialogProps] = useConfirmDialogProps(); + const [formDialogProps, setFormDialogProps] = useConfirmDialogProps(); + const [isEditUpses, setIsEditUpses] = useState(false); + const [isLoadingUpsTemplate, setIsLoadingUpsTemplate] = + useProtectedState(true); + const [upsTemplate, setUpsTemplate] = useProtectedState< + APIUpsTemplate | undefined + >(undefined); + + const listElement = useMemo( + () => ( + { + setFormDialogProps({ + actionProceedText: 'Add', + content: , + titleText: 'Add a UPS', + }); + + formDialogRef.current.setOpen?.call(null, true); + }} + onEdit={() => { + setIsEditUpses((previous) => !previous); + }} + /> + ), + [isEditUpses, setFormDialogProps, upsTemplate], + ); + const panelContent = useMemo( + () => (isLoadingUpsTemplate ? : listElement), + [isLoadingUpsTemplate, listElement], + ); + + if (isFirstRender) { + api + .get('/ups/template') + .then(({ data }) => { + setUpsTemplate(data); + }) + .catch((error) => { + handleAPIError(error); + }) + .finally(() => { + setIsLoadingUpsTemplate(false); + }); + } + + return ( + <> + + + Manage Upses + + {panelContent} + + + + + ); +}; + +export default ManageUpsPanel; diff --git a/striker-ui/components/ManageUps/index.tsx b/striker-ui/components/ManageUps/index.tsx new file mode 100644 index 00000000..b99eceb8 --- /dev/null +++ b/striker-ui/components/ManageUps/index.tsx @@ -0,0 +1,3 @@ +import ManageUpsPanel from './ManageUpsPanel'; + +export default ManageUpsPanel; diff --git a/striker-ui/types/APIUps.d.ts b/striker-ui/types/APIUps.d.ts new file mode 100644 index 00000000..c73d68d4 --- /dev/null +++ b/striker-ui/types/APIUps.d.ts @@ -0,0 +1,7 @@ +type APIUpsTemplate = { + [upsTypeId: string]: { + agent: string; + brand: string; + description: string; + }; +}; diff --git a/striker-ui/types/AddUpsInputGroup.d.ts b/striker-ui/types/AddUpsInputGroup.d.ts new file mode 100644 index 00000000..03a891ec --- /dev/null +++ b/striker-ui/types/AddUpsInputGroup.d.ts @@ -0,0 +1,6 @@ +type AddUpsInputGroupOptionalProps = { + loading?: boolean; + upsTemplate?: APIUpsTemplate; +}; + +type AddUpsInputGroupProps = AddUpsInputGroupOptionalProps; diff --git a/striker-ui/types/CommonUpsInputGroup.d.ts b/striker-ui/types/CommonUpsInputGroup.d.ts new file mode 100644 index 00000000..f8af2908 --- /dev/null +++ b/striker-ui/types/CommonUpsInputGroup.d.ts @@ -0,0 +1,9 @@ +type CommonUpsInputGroupOptionalProps = { + previous?: { + hostName?: string; + ipAddress?: string; + upsName?: string; + }; +}; + +type CommonUpsInputGroupProps = CommonUpsInputGroupOptionalProps; From 288597a95f9bf987a6cdd6eb6b518bf94813ed77 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 22:32:00 -0500 Subject: [PATCH 50/75] fix(striker-ui-api): limit UPS types to APC --- .../lib/request_handlers/ups/getUPSTemplate.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts index b966f806..ea014049 100644 --- a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts +++ b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts @@ -4,7 +4,7 @@ import { getAnvilData } from '../../accessModule'; import { stderr } from '../../shell'; export const getUPSTemplate: RequestHandler = (request, response) => { - let rawUPSData; + let rawUPSData: AnvilDataUPSHash; try { ({ ups_data: rawUPSData } = getAnvilData<{ ups_data: AnvilDataUPSHash }>( @@ -19,5 +19,17 @@ export const getUPSTemplate: RequestHandler = (request, response) => { return; } - response.status(200).send(rawUPSData); + const upsData: AnvilDataUPSHash = Object.entries( + rawUPSData, + ).reduce((previous, [upsTypeId, value]) => { + const { brand } = value; + + if (/apc/i.test(brand)) { + previous[upsTypeId] = value; + } + + return previous; + }, {}); + + response.status(200).send(upsData); }; From ddb1cd61f1bd9ebbe05e63ae56197e074c1a0581 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Tue, 28 Feb 2023 23:22:07 -0500 Subject: [PATCH 51/75] fix(striker-ui-api): add GET UPS overviews --- .../src/lib/request_handlers/ups/getUPS.ts | 37 +++++++++++++++++++ .../src/lib/request_handlers/ups/index.ts | 1 + striker-ui-api/src/routes/ups.ts | 4 +- striker-ui-api/src/types/APIUPS.d.ts | 6 +++ 4 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 striker-ui-api/src/lib/request_handlers/ups/getUPS.ts create mode 100644 striker-ui-api/src/types/APIUPS.d.ts diff --git a/striker-ui-api/src/lib/request_handlers/ups/getUPS.ts b/striker-ui-api/src/lib/request_handlers/ups/getUPS.ts new file mode 100644 index 00000000..e884e54f --- /dev/null +++ b/striker-ui-api/src/lib/request_handlers/ups/getUPS.ts @@ -0,0 +1,37 @@ +import { RequestHandler } from 'express'; + +import buildGetRequestHandler from '../buildGetRequestHandler'; +import { buildQueryResultReducer } from '../../buildQueryResultModifier'; + +export const getUPS: RequestHandler = buildGetRequestHandler( + (request, buildQueryOptions) => { + const query = ` + SELECT + ups_uuid, + ups_name, + ups_agent, + ups_ip_address + FROM upses + ORDER BY ups_name ASC;`; + const afterQueryReturn: QueryResultModifierFunction | undefined = + buildQueryResultReducer<{ [upsUUID: string]: UPSOverview }>( + (previous, [upsUUID, upsName, upsAgent, upsIPAddress]) => { + previous[upsUUID] = { + upsAgent, + upsIPAddress, + upsName, + upsUUID, + }; + + return previous; + }, + {}, + ); + + if (buildQueryOptions) { + buildQueryOptions.afterQueryReturn = afterQueryReturn; + } + + return query; + }, +); diff --git a/striker-ui-api/src/lib/request_handlers/ups/index.ts b/striker-ui-api/src/lib/request_handlers/ups/index.ts index 969616cb..191a8f79 100644 --- a/striker-ui-api/src/lib/request_handlers/ups/index.ts +++ b/striker-ui-api/src/lib/request_handlers/ups/index.ts @@ -1 +1,2 @@ +export * from './getUPS'; export * from './getUPSTemplate'; diff --git a/striker-ui-api/src/routes/ups.ts b/striker-ui-api/src/routes/ups.ts index 1d19b86f..c6e6c637 100644 --- a/striker-ui-api/src/routes/ups.ts +++ b/striker-ui-api/src/routes/ups.ts @@ -1,9 +1,9 @@ import express from 'express'; -import { getUPSTemplate } from '../lib/request_handlers/ups'; +import { getUPS, getUPSTemplate } from '../lib/request_handlers/ups'; const router = express.Router(); -router.get('/template', getUPSTemplate); +router.get('/', getUPS).get('/template', getUPSTemplate); export default router; diff --git a/striker-ui-api/src/types/APIUPS.d.ts b/striker-ui-api/src/types/APIUPS.d.ts new file mode 100644 index 00000000..4eb25534 --- /dev/null +++ b/striker-ui-api/src/types/APIUPS.d.ts @@ -0,0 +1,6 @@ +type UPSOverview = { + upsAgent: string; + upsIPAddress: string; + upsName: string; + upsUUID: string; +}; From 28ff0c79bd0a758a0aa363d078e6520595ff0328 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 1 Mar 2023 15:23:27 -0500 Subject: [PATCH 52/75] fix(striker-ui): center List empty text --- striker-ui/components/List.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/striker-ui/components/List.tsx b/striker-ui/components/List.tsx index e73543e1..084c1039 100644 --- a/striker-ui/components/List.tsx +++ b/striker-ui/components/List.tsx @@ -176,7 +176,7 @@ const List = forwardRef( const listEmptyElement = useMemo( () => typeof listEmpty === 'string' ? ( - {listEmpty} + {listEmpty} ) : ( listEmpty ), From 831ac43d0311c6cacb66f1d060da8df03b30e421 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 1 Mar 2023 15:56:10 -0500 Subject: [PATCH 53/75] fix(striker-ui): remove duplicate parameter in CommonUpsInputGroup --- .../components/ManageUps/CommonUpsInputGroup.tsx | 14 ++------------ striker-ui/types/CommonUpsInputGroup.d.ts | 3 +-- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx b/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx index 15477906..3550ee24 100644 --- a/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx +++ b/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx @@ -5,11 +5,7 @@ import InputWithRef from '../InputWithRef'; import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; const CommonUpsInputGroup: FC = ({ - previous: { - hostName: previousHostName, - ipAddress: previousIpAddress, - upsName: previousUpsName, - } = {}, + previous: { upsIPAddress: previousIpAddress, upsName: previousUpsName } = {}, }) => ( <> = ({ } required @@ -46,12 +42,6 @@ const CommonUpsInputGroup: FC = ({ }} spacing="1em" /> - ); diff --git a/striker-ui/types/CommonUpsInputGroup.d.ts b/striker-ui/types/CommonUpsInputGroup.d.ts index f8af2908..da058a40 100644 --- a/striker-ui/types/CommonUpsInputGroup.d.ts +++ b/striker-ui/types/CommonUpsInputGroup.d.ts @@ -1,7 +1,6 @@ type CommonUpsInputGroupOptionalProps = { previous?: { - hostName?: string; - ipAddress?: string; + upsIPAddress?: string; upsName?: string; }; }; From 7337752573a49d7643ad2e37c9024e77855fe5e8 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 1 Mar 2023 16:07:36 -0500 Subject: [PATCH 54/75] feat(striker-ui): add EditUpsInputGroup --- .../ManageUps/EditUpsInputGroup.tsx | 27 +++++++++++++++++++ striker-ui/types/EditUpsInputGroup.d.ts | 6 +++++ 2 files changed, 33 insertions(+) create mode 100644 striker-ui/components/ManageUps/EditUpsInputGroup.tsx create mode 100644 striker-ui/types/EditUpsInputGroup.d.ts diff --git a/striker-ui/components/ManageUps/EditUpsInputGroup.tsx b/striker-ui/components/ManageUps/EditUpsInputGroup.tsx new file mode 100644 index 00000000..a9ab02f7 --- /dev/null +++ b/striker-ui/components/ManageUps/EditUpsInputGroup.tsx @@ -0,0 +1,27 @@ +import { FC, ReactElement, useMemo } from 'react'; + +import CommonUpsInputGroup from './CommonUpsInputGroup'; +import Spinner from '../Spinner'; + +const EditUpsInputGroup: FC = ({ + loading: isExternalLoading, + previous, + upsUUID, +}) => { + const content = useMemo( + () => + isExternalLoading ? ( + + ) : ( + <> + + + + ), + [isExternalLoading, previous, upsUUID], + ); + + return content; +}; + +export default EditUpsInputGroup; diff --git a/striker-ui/types/EditUpsInputGroup.d.ts b/striker-ui/types/EditUpsInputGroup.d.ts new file mode 100644 index 00000000..610b4ed8 --- /dev/null +++ b/striker-ui/types/EditUpsInputGroup.d.ts @@ -0,0 +1,6 @@ +type EditUpsInputGroupOptionalProps = { + loading?: boolean; +}; + +type EditUpsInputGroupProps = EditUpsInputGroupOptionalProps & + Pick & { upsUUID: string }; From d65c1880c0f03c316b2f90211739d89361836040 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 1 Mar 2023 18:23:12 -0500 Subject: [PATCH 55/75] fix(striker-ui): pass id to input element in SelectWithLabel --- striker-ui/components/SelectWithLabel.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/striker-ui/components/SelectWithLabel.tsx b/striker-ui/components/SelectWithLabel.tsx index eabda51a..00ed6e02 100644 --- a/striker-ui/components/SelectWithLabel.tsx +++ b/striker-ui/components/SelectWithLabel.tsx @@ -71,15 +71,20 @@ const SelectWithLabel: FC = ({ [createCheckbox, disableItem, hideItem, id], ); - const inputElement = useMemo(() => , [label]); + const selectId = useMemo(() => `${id}-select-element`, [id]); + + const inputElement = useMemo( + () => , + [id, label], + ); const labelElement = useMemo( () => label && ( - + {label} ), - [id, inputLabelProps, label], + [inputLabelProps, label, selectId], ); const menuItemElements = useMemo( () => @@ -96,7 +101,7 @@ const SelectWithLabel: FC = ({ {labelElement} + + ), - [isExternalLoading, previous, upsUUID], + [isExternalLoading, previous, upsTemplate, upsUUID], ); return content; }; +export { INPUT_ID_UPS_UUID }; + export default EditUpsInputGroup; diff --git a/striker-ui/types/EditUpsInputGroup.d.ts b/striker-ui/types/EditUpsInputGroup.d.ts index 610b4ed8..ebff2166 100644 --- a/striker-ui/types/EditUpsInputGroup.d.ts +++ b/striker-ui/types/EditUpsInputGroup.d.ts @@ -3,4 +3,6 @@ type EditUpsInputGroupOptionalProps = { }; type EditUpsInputGroupProps = EditUpsInputGroupOptionalProps & - Pick & { upsUUID: string }; + Pick & { + upsUUID: string; + }; From 4ff8905509f59e9ef34610e405c598f90bb3a2d3 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 1 Mar 2023 22:43:07 -0500 Subject: [PATCH 59/75] fix(striker-ui): connect add and edit forms with ManageUpsPanel --- .../components/ManageUps/ManageUpsPanel.tsx | 218 ++++++++++++++++-- striker-ui/types/APIUps.d.ts | 9 + 2 files changed, 213 insertions(+), 14 deletions(-) diff --git a/striker-ui/components/ManageUps/ManageUpsPanel.tsx b/striker-ui/components/ManageUps/ManageUpsPanel.tsx index 99b2799a..7c4d3f04 100644 --- a/striker-ui/components/ManageUps/ManageUpsPanel.tsx +++ b/striker-ui/components/ManageUps/ManageUpsPanel.tsx @@ -1,25 +1,108 @@ -import { FC, useMemo, useRef, useState } from 'react'; +import { + FC, + FormEventHandler, + useCallback, + useMemo, + useRef, + useState, +} from 'react'; -import AddUpsInputGroup from './AddUpsInputGroup'; +import API_BASE_URL from '../../lib/consts/API_BASE_URL'; + +import AddUpsInputGroup, { INPUT_ID_UPS_TYPE_ID } from './AddUpsInputGroup'; import api from '../../lib/api'; +import { INPUT_ID_UPS_IP, INPUT_ID_UPS_NAME } from './CommonUpsInputGroup'; import ConfirmDialog from '../ConfirmDialog'; +import EditUpsInputGroup, { INPUT_ID_UPS_UUID } from './EditUpsInputGroup'; +import FlexBox from '../FlexBox'; import FormDialog from '../FormDialog'; 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 { HeaderText } from '../Text'; +import { BodyText, HeaderText, InlineMonoText, MonoText } from '../Text'; import useConfirmDialogProps from '../../hooks/useConfirmDialogProps'; import useIsFirstRender from '../../hooks/useIsFirstRender'; import useProtectedState from '../../hooks/useProtectedState'; +type UpsFormData = { + upsAgent: string; + upsBrand: string; + upsIPAddress: string; + upsName: string; + upsTypeId: string; + upsUUID: string; +}; + +const getUpsFormData = ( + upsTemplate: APIUpsTemplate, + ...[{ target }]: Parameters> +): UpsFormData => { + const { elements } = target as HTMLFormElement; + + const { value: upsName } = elements.namedItem( + INPUT_ID_UPS_NAME, + ) as HTMLInputElement; + const { value: upsIPAddress } = elements.namedItem( + INPUT_ID_UPS_IP, + ) as HTMLInputElement; + + const inputUpsTypeId = elements.namedItem(INPUT_ID_UPS_TYPE_ID); + + let upsAgent = ''; + let upsBrand = ''; + let upsTypeId = ''; + + if (inputUpsTypeId) { + ({ value: upsTypeId } = inputUpsTypeId as HTMLInputElement); + ({ agent: upsAgent, brand: upsBrand } = upsTemplate[upsTypeId]); + } + + const inputUpsUUID = elements.namedItem(INPUT_ID_UPS_UUID); + + let upsUUID = ''; + + if (inputUpsUUID) { + ({ value: upsUUID } = inputUpsUUID as HTMLInputElement); + } + + return { upsAgent, upsBrand, upsIPAddress, upsName, upsTypeId, upsUUID }; +}; + +const buildConfirmUpsFormData = ({ + upsBrand, + upsIPAddress, + upsName, + upsUUID, +}: UpsFormData) => { + const listItems: Record = { + 'ups-brand': { label: 'Brand', value: upsBrand }, + 'ups-name': { label: 'Host name', value: upsName }, + 'ups-ip-address': { label: 'IP address', value: upsIPAddress }, + }; + + return ( + ( + + {label} + {value} + + )} + /> + ); +}; + const ManageUpsPanel: FC = () => { const isFirstRender = useIsFirstRender(); const confirmDialogRef = useRef({}); const formDialogRef = useRef({}); - const [confirmDialogProps] = useConfirmDialogProps(); + const [confirmDialogProps, setConfirmDialogProps] = useConfirmDialogProps(); const [formDialogProps, setFormDialogProps] = useConfirmDialogProps(); const [isEditUpses, setIsEditUpses] = useState(false); const [isLoadingUpsTemplate, setIsLoadingUpsTemplate] = @@ -28,6 +111,99 @@ const ManageUpsPanel: FC = () => { APIUpsTemplate | undefined >(undefined); + const { data: upsOverviews, isLoading: isUpsOverviewLoading } = + periodicFetch(`${API_BASE_URL}/ups`, { + refreshInterval: 60000, + }); + + const buildEditUpsFormDialogProps = useCallback< + (args: APIUpsOverview[string]) => ConfirmDialogProps + >( + ({ upsAgent, upsIPAddress, upsName, upsUUID }) => { + // Determine the type of existing UPS based on its scan agent. + // TODO: should identity an existing UPS's type in the DB. + const upsTypeId: string = + Object.entries(upsTemplate ?? {}).find( + ([, { agent }]) => upsAgent === agent, + )?.[0] ?? ''; + + return { + actionProceedText: 'Update', + content: ( + + ), + onSubmitAppend: (event) => { + if (!upsTemplate) { + return; + } + + const editData = getUpsFormData(upsTemplate, event); + const { upsName: newUpsName } = editData; + + setConfirmDialogProps({ + actionProceedText: 'Update', + content: buildConfirmUpsFormData(editData), + titleText: ( + + Update{' '} + {newUpsName}{' '} + with the following data? + + ), + }); + + confirmDialogRef.current.setOpen?.call(null, true); + }, + titleText: ( + + Update UPS{' '} + {upsName} + + ), + }; + }, + [setConfirmDialogProps, upsTemplate], + ); + + const addUpsFormDialogProps = useMemo( + () => ({ + actionProceedText: 'Add', + content: , + onSubmitAppend: (event) => { + if (!upsTemplate) { + return; + } + + const addData = getUpsFormData(upsTemplate, event); + const { upsBrand } = addData; + + setConfirmDialogProps({ + actionProceedText: 'Add', + content: buildConfirmUpsFormData(addData), + titleText: ( + + Add a{' '} + {upsBrand} UPS + with the following data? + + ), + }); + + confirmDialogRef.current.setOpen?.call(null, true); + }, + titleText: 'Add a UPS', + }), + [setConfirmDialogProps, upsTemplate], + ); + const listElement = useMemo( () => ( { edit={isEditUpses} header listEmpty="No Ups(es) registered." + listItems={upsOverviews} onAdd={() => { - setFormDialogProps({ - actionProceedText: 'Add', - content: , - titleText: 'Add a UPS', - }); - + setFormDialogProps(addUpsFormDialogProps); formDialogRef.current.setOpen?.call(null, true); }} onEdit={() => { setIsEditUpses((previous) => !previous); }} + onItemClick={(value) => { + setFormDialogProps(buildEditUpsFormDialogProps(value)); + formDialogRef.current.setOpen?.call(null, true); + }} + renderListItem={(upsUUID, { upsAgent, upsIPAddress, upsName }) => ( + + {upsName} + agent="{upsAgent}" + ip="{upsIPAddress}" + + )} /> ), - [isEditUpses, setFormDialogProps, upsTemplate], + [ + addUpsFormDialogProps, + buildEditUpsFormDialogProps, + isEditUpses, + setFormDialogProps, + upsOverviews, + ], ); const panelContent = useMemo( - () => (isLoadingUpsTemplate ? : listElement), - [isLoadingUpsTemplate, listElement], + () => + isLoadingUpsTemplate || isUpsOverviewLoading ? : listElement, + [isLoadingUpsTemplate, isUpsOverviewLoading, listElement], ); if (isFirstRender) { @@ -75,7 +265,7 @@ const ManageUpsPanel: FC = () => { <> - Manage Upses + Manage UPSes {panelContent} diff --git a/striker-ui/types/APIUps.d.ts b/striker-ui/types/APIUps.d.ts index c73d68d4..d0194de5 100644 --- a/striker-ui/types/APIUps.d.ts +++ b/striker-ui/types/APIUps.d.ts @@ -5,3 +5,12 @@ type APIUpsTemplate = { description: string; }; }; + +type APIUpsOverview = { + [upsUUID: string]: { + upsAgent: string; + upsIPAddress: string; + upsName: string; + upsUUID: string; + }; +}; From 58549eb3ed14a307c5246550187d5ed8ada04d5e Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Wed, 1 Mar 2023 23:34:36 -0500 Subject: [PATCH 60/75] fix(striker-ui-api): extract link from UPS type description --- .../request_handlers/ups/getUPSTemplate.ts | 23 ++++++++++++++++--- striker-ui-api/src/types/APIUPS.d.ts | 11 +++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts index ea014049..3c4a8b9c 100644 --- a/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts +++ b/striker-ui-api/src/lib/request_handlers/ups/getUPSTemplate.ts @@ -21,11 +21,28 @@ export const getUPSTemplate: RequestHandler = (request, response) => { const upsData: AnvilDataUPSHash = Object.entries( rawUPSData, - ).reduce((previous, [upsTypeId, value]) => { - const { brand } = value; + ).reduce((previous, [upsTypeId, value]) => { + const { brand, description: rawDescription, ...rest } = value; + + const matched = rawDescription.match( + /^(.+)\s+[-]\s+[<][^>]+href=[\\"]+([^\s]+)[\\"]+.+[>]([^<]+)[<]/, + ); + const result: UPSTemplate[string] = { + ...rest, + brand, + description: rawDescription, + links: {}, + }; + + if (matched) { + const [, description, linkHref, linkLabel] = matched; + + result.description = description; + result.links[0] = { linkHref, linkLabel }; + } if (/apc/i.test(brand)) { - previous[upsTypeId] = value; + previous[upsTypeId] = result; } return previous; diff --git a/striker-ui-api/src/types/APIUPS.d.ts b/striker-ui-api/src/types/APIUPS.d.ts index 4eb25534..5199e348 100644 --- a/striker-ui-api/src/types/APIUPS.d.ts +++ b/striker-ui-api/src/types/APIUPS.d.ts @@ -4,3 +4,14 @@ type UPSOverview = { upsName: string; upsUUID: string; }; + +type UPSTemplate = { + [upsName: string]: AnvilDataUPSHash[string] & { + links: { + [linkId: string]: { + linkHref: string; + linkLabel: string; + }; + }; + }; +}; From 230256a16322b15ad40539fc3124d04a129d5b53 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 2 Mar 2023 15:56:11 -0500 Subject: [PATCH 61/75] fix(striker-ui): add Link to UPS type options --- .../components/ManageUps/AddUpsInputGroup.tsx | 54 +++++++++++++++---- striker-ui/types/APIUps.d.ts | 6 +++ 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/striker-ui/components/ManageUps/AddUpsInputGroup.tsx b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx index 476d3310..2aa5bf51 100644 --- a/striker-ui/components/ManageUps/AddUpsInputGroup.tsx +++ b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx @@ -1,7 +1,10 @@ -import { FC, ReactElement, useMemo, useState } from 'react'; +import { FC, ReactElement, ReactNode, useMemo, useState } from 'react'; + +import { BLACK } from '../../lib/consts/DEFAULT_THEME'; import CommonUpsInputGroup from './CommonUpsInputGroup'; import FlexBox from '../FlexBox'; +import Link from '../Link'; import SelectWithLabel from '../SelectWithLabel'; import Spinner from '../Spinner'; import { BodyText } from '../Text'; @@ -22,15 +25,46 @@ const AddUpsInputGroup: FC = ({ () => upsTemplate ? Object.entries(upsTemplate).map( - ([upsTypeId, { brand, description }]) => ({ - displayValue: ( - - {brand} - {description} - - ), - value: upsTypeId, - }), + ([ + upsTypeId, + { + brand, + description, + links: { 0: link }, + }, + ]) => { + let linkElement: ReactNode; + + if (link) { + const { linkHref, linkLabel } = link; + + linkElement = ( + { + // Don't trigger the (parent) item selection event. + event.stopPropagation(); + }} + sx={{ display: 'inline-flex', color: BLACK }} + target="_blank" + > + {linkLabel} + + ); + } + + return { + displayValue: ( + + {brand} + + {description} ({linkElement}) + + + ), + value: upsTypeId, + }; + }, ) : [], [upsTemplate], diff --git a/striker-ui/types/APIUps.d.ts b/striker-ui/types/APIUps.d.ts index d0194de5..4956eada 100644 --- a/striker-ui/types/APIUps.d.ts +++ b/striker-ui/types/APIUps.d.ts @@ -3,6 +3,12 @@ type APIUpsTemplate = { agent: string; brand: string; description: string; + links: { + [linkId: string]: { + linkHref: string; + linkLabel: string; + }; + }; }; }; From 3610c05d2930d123d6a9b11415334a3bdae7910b Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 2 Mar 2023 16:25:58 -0500 Subject: [PATCH 62/75] fix(striker-ui): expose required in SelectWithLabel --- striker-ui/components/SelectWithLabel.tsx | 9 +++++++-- striker-ui/types/SelectWithLabel.d.ts | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/striker-ui/components/SelectWithLabel.tsx b/striker-ui/components/SelectWithLabel.tsx index 00ed6e02..8a7d63e2 100644 --- a/striker-ui/components/SelectWithLabel.tsx +++ b/striker-ui/components/SelectWithLabel.tsx @@ -24,6 +24,7 @@ const SelectWithLabel: FC = ({ messageBoxProps = {}, name, onChange, + required: isRequired, selectProps: { multiple: selectMultiple, sx: selectSx, @@ -80,11 +81,15 @@ const SelectWithLabel: FC = ({ const labelElement = useMemo( () => label && ( - + {label} ), - [inputLabelProps, label, selectId], + [inputLabelProps, isRequired, label, selectId], ); const menuItemElements = useMemo( () => diff --git a/striker-ui/types/SelectWithLabel.d.ts b/striker-ui/types/SelectWithLabel.d.ts index 384e48ff..d799a1b5 100644 --- a/striker-ui/types/SelectWithLabel.d.ts +++ b/striker-ui/types/SelectWithLabel.d.ts @@ -20,6 +20,7 @@ type SelectWithLabelOptionalProps = { >; label?: string; messageBoxProps?: Partial; + required?: boolean; selectProps?: Partial; }; From 5538f9bdaed7ff6a41875d323c65e1fd8d56867d Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 2 Mar 2023 16:28:22 -0500 Subject: [PATCH 63/75] fix(striker-ui): show UPS type as required --- striker-ui/components/ManageUps/AddUpsInputGroup.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/striker-ui/components/ManageUps/AddUpsInputGroup.tsx b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx index 2aa5bf51..a81a4e2f 100644 --- a/striker-ui/components/ManageUps/AddUpsInputGroup.tsx +++ b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx @@ -82,6 +82,7 @@ const AddUpsInputGroup: FC = ({ setInputUpsTypeIdValue(newValue); }} + required selectItems={upsTypeOptions} selectProps={{ onClearIndicatorClick: () => { From 3b9e3c6af0ea5b3b780266be9a722b7db042512d Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Thu, 2 Mar 2023 23:00:53 -0500 Subject: [PATCH 64/75] fix(striker-ui): make buildMapToMessageSetter() handle array ids --- striker-ui/lib/buildMapToMessageSetter.ts | 54 ++++++++++++++++------- striker-ui/types/MapToMessageSetter.d.ts | 16 +++++++ 2 files changed, 55 insertions(+), 15 deletions(-) create mode 100644 striker-ui/types/MapToMessageSetter.d.ts diff --git a/striker-ui/lib/buildMapToMessageSetter.ts b/striker-ui/lib/buildMapToMessageSetter.ts index fe0b5dbd..5152ab33 100644 --- a/striker-ui/lib/buildMapToMessageSetter.ts +++ b/striker-ui/lib/buildMapToMessageSetter.ts @@ -2,25 +2,49 @@ import { MutableRefObject } from 'react'; import { MessageGroupForwardedRefContent } from '../components/MessageGroup'; -type BuildMapToMessageSetterReturnType = { - [MessageSetterID in keyof T]: MessageSetterFunction; +const buildMessageSetter = ( + id: string, + messageGroupRef: MutableRefObject, + container?: MapToMessageSetter, + key: string = id, +): MessageSetterFunction => { + const setter: MessageSetterFunction = (message?) => { + messageGroupRef.current.setMessage?.call(null, id, message); + }; + + if (container) { + container[key as keyof T] = setter; + } + + return setter; }; -const buildMapToMessageSetter = ( - mapToID: T, +const buildMapToMessageSetter = < + U extends string, + I extends InputIds, + M extends MapToInputId, +>( + ids: I, messageGroupRef: MutableRefObject, -): BuildMapToMessageSetterReturnType => - Object.entries(mapToID).reduce>( - (previous, [key, id]) => { - const setter: MessageSetterFunction = (message?) => { - messageGroupRef.current.setMessage?.call(null, id, message); - }; - - previous[key as keyof T] = setter; +): MapToMessageSetter => { + let result: MapToMessageSetter = {} as MapToMessageSetter; + if (ids instanceof Array) { + result = ids.reduce>((previous, id) => { + buildMessageSetter(id, messageGroupRef, previous); return previous; - }, - {} as BuildMapToMessageSetterReturnType, - ); + }, result); + } else { + result = Object.entries(ids).reduce>( + (previous, [key, id]) => { + buildMessageSetter(id, messageGroupRef, previous, key); + return previous; + }, + result, + ); + } + + return result; +}; export default buildMapToMessageSetter; diff --git a/striker-ui/types/MapToMessageSetter.d.ts b/striker-ui/types/MapToMessageSetter.d.ts new file mode 100644 index 00000000..537f2644 --- /dev/null +++ b/striker-ui/types/MapToMessageSetter.d.ts @@ -0,0 +1,16 @@ +type MapToMessageSetter = { + [MessageSetterID in keyof T]: MessageSetterFunction; +}; + +type InputIds = ReadonlyArray | MapToInputTestID; + +/** + * Given either: + * 1. an array of input identifiers, or + * 2. a key-value object of input indentifiers, + * transform it into a key-value object of identifiers. + */ +type MapToInputId< + U extends string, + I extends InputIds, +> = I extends ReadonlyArray ? { [K in I[number]]: K } : I; From 4b14d2d568b3c32bdc8bfa76eca16e6b5cc335f6 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 00:01:45 -0500 Subject: [PATCH 65/75] fix(striker-ui): add arbitrary slot before action area in ConfirmDialog --- striker-ui/components/ConfirmDialog.tsx | 2 ++ striker-ui/types/ConfirmDialog.d.ts | 1 + 2 files changed, 3 insertions(+) diff --git a/striker-ui/components/ConfirmDialog.tsx b/striker-ui/components/ConfirmDialog.tsx index 3298375a..5259db49 100644 --- a/striker-ui/components/ConfirmDialog.tsx +++ b/striker-ui/components/ConfirmDialog.tsx @@ -49,6 +49,7 @@ const ConfirmDialog = forwardRef< onProceedAppend, onSubmitAppend, openInitially = false, + preActionArea, proceedButtonProps = {}, proceedColour: proceedColourKey = 'blue', scrollContent: isScrollContent = false, @@ -234,6 +235,7 @@ const ConfirmDialog = forwardRef< {contentElement} + {preActionArea} {actionAreaElement} diff --git a/striker-ui/types/ConfirmDialog.d.ts b/striker-ui/types/ConfirmDialog.d.ts index f9fdde02..24e94d11 100644 --- a/striker-ui/types/ConfirmDialog.d.ts +++ b/striker-ui/types/ConfirmDialog.d.ts @@ -10,6 +10,7 @@ type ConfirmDialogOptionalProps = { onCancelAppend?: ContainedButtonProps['onClick']; onSubmitAppend?: import('react').FormEventHandler; openInitially?: boolean; + preActionArea?: import('react').ReactNode; proceedButtonProps?: ContainedButtonProps; proceedColour?: 'blue' | 'red'; scrollContent?: boolean; From 1548bfd8bc55f804e834599039bbed6c4ac0e3c9 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 00:03:24 -0500 Subject: [PATCH 66/75] fix(striker-ui): add hook useFormUtils --- striker-ui/hooks/useFormUtils.ts | 78 ++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 striker-ui/hooks/useFormUtils.ts diff --git a/striker-ui/hooks/useFormUtils.ts b/striker-ui/hooks/useFormUtils.ts new file mode 100644 index 00000000..8f71dfc3 --- /dev/null +++ b/striker-ui/hooks/useFormUtils.ts @@ -0,0 +1,78 @@ +import { + Dispatch, + MutableRefObject, + SetStateAction, + useCallback, + useMemo, + useState, +} from 'react'; + +import buildMapToMessageSetter from '../lib/buildMapToMessageSetter'; +import buildObjectStateSetterCallback from '../lib/buildObjectStateSetterCallback'; +import { MessageGroupForwardedRefContent } from '../components/MessageGroup'; + +type FormValidity = { + [K in keyof T]?: boolean; +}; + +const useFormUtils = < + U extends string, + I extends InputIds, + M extends MapToInputId, +>( + ids: I, + messageGroupRef: MutableRefObject, +): { + buildFinishInputTestBatchFunction: ( + key: keyof M, + ) => (result: boolean) => void; + buildInputFirstRenderFunction: ( + key: keyof M, + ) => ({ isRequired }: { isRequired: boolean }) => void; + formValidity: FormValidity; + isFormInvalid: boolean; + msgSetters: MapToMessageSetter; + setFormValidity: Dispatch>>; +} => { + const [formValidity, setFormValidity] = useState>({}); + + const buildFinishInputTestBatchFunction = useCallback( + (key: keyof M) => (result: boolean) => { + setFormValidity( + buildObjectStateSetterCallback>(key, result), + ); + }, + [], + ); + + const buildInputFirstRenderFunction = useCallback( + (key: keyof M) => + ({ isRequired }: { isRequired: boolean }) => { + setFormValidity( + buildObjectStateSetterCallback>(key, !isRequired), + ); + }, + [], + ); + + const isFormInvalid = useMemo( + () => Object.values(formValidity).some((isInputValid) => !isInputValid), + [formValidity], + ); + + const msgSetters = useMemo( + () => buildMapToMessageSetter(ids, messageGroupRef), + [ids, messageGroupRef], + ); + + return { + buildFinishInputTestBatchFunction, + buildInputFirstRenderFunction, + formValidity, + isFormInvalid, + msgSetters, + setFormValidity, + }; +}; + +export default useFormUtils; From 37972bb5570e20992ccfc4367e91155a3366638e Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 19:28:48 -0500 Subject: [PATCH 67/75] fix(striker-ui): expose isRequired in build test batch functions --- striker-ui/lib/test_input/buildDomainTestBatch.tsx | 3 ++- striker-ui/lib/test_input/buildIPAddressTestBatch.tsx | 3 ++- striker-ui/lib/test_input/buildNumberTestBatch.tsx | 3 ++- striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx | 3 ++- striker-ui/lib/test_input/buildUUIDTestBatch.tsx | 3 ++- striker-ui/types/TestInputFunction.d.ts | 3 ++- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/striker-ui/lib/test_input/buildDomainTestBatch.tsx b/striker-ui/lib/test_input/buildDomainTestBatch.tsx index f7d4a23c..dad7374a 100644 --- a/striker-ui/lib/test_input/buildDomainTestBatch.tsx +++ b/striker-ui/lib/test_input/buildDomainTestBatch.tsx @@ -6,10 +6,11 @@ import { InlineMonoText } from '../../components/Text'; const buildDomainTestBatch: BuildInputTestBatchFunction = ( inputName, onSuccess, - { onFinishBatch, ...defaults } = {}, + { isRequired, onFinishBatch, ...defaults } = {}, onDomainTestFailure, ) => ({ defaults: { ...defaults, onSuccess }, + isRequired, onFinishBatch, tests: [ { diff --git a/striker-ui/lib/test_input/buildIPAddressTestBatch.tsx b/striker-ui/lib/test_input/buildIPAddressTestBatch.tsx index b8edddd7..418fb322 100644 --- a/striker-ui/lib/test_input/buildIPAddressTestBatch.tsx +++ b/striker-ui/lib/test_input/buildIPAddressTestBatch.tsx @@ -5,10 +5,11 @@ import testNotBlank from './testNotBlank'; const buildIPAddressTestBatch: BuildInputTestBatchFunction = ( inputName, onSuccess, - { onFinishBatch, ...defaults } = {}, + { isRequired, onFinishBatch, ...defaults } = {}, onIPv4TestFailure, ) => ({ defaults: { ...defaults, onSuccess }, + isRequired, onFinishBatch, tests: [ { diff --git a/striker-ui/lib/test_input/buildNumberTestBatch.tsx b/striker-ui/lib/test_input/buildNumberTestBatch.tsx index 5a5d0447..846a5138 100644 --- a/striker-ui/lib/test_input/buildNumberTestBatch.tsx +++ b/striker-ui/lib/test_input/buildNumberTestBatch.tsx @@ -4,7 +4,7 @@ import toNumber from '../toNumber'; const buildNumberTestBatch: BuildInputTestBatchFunction = ( inputName, onSuccess, - { onFinishBatch, ...defaults } = {}, + { isRequired, onFinishBatch, ...defaults } = {}, onIntTestFailure?, onFloatTestFailure?, onRangeTestFailure?, @@ -48,6 +48,7 @@ const buildNumberTestBatch: BuildInputTestBatchFunction = ( return { defaults: { ...defaults, onSuccess }, + isRequired, onFinishBatch, tests, }; diff --git a/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx b/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx index 93c3cfbf..594f387e 100644 --- a/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx +++ b/striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx @@ -6,10 +6,11 @@ import { InlineMonoText } from '../../components/Text'; const buildPeacefulStringTestBatch: BuildInputTestBatchFunction = ( inputName, onSuccess, - { onFinishBatch, ...defaults } = {}, + { isRequired, onFinishBatch, ...defaults } = {}, onTestPeacefulStringFailureAppend, ) => ({ defaults: { ...defaults, onSuccess }, + isRequired, onFinishBatch, tests: [ { diff --git a/striker-ui/lib/test_input/buildUUIDTestBatch.tsx b/striker-ui/lib/test_input/buildUUIDTestBatch.tsx index f417f548..d5173fbf 100644 --- a/striker-ui/lib/test_input/buildUUIDTestBatch.tsx +++ b/striker-ui/lib/test_input/buildUUIDTestBatch.tsx @@ -5,10 +5,11 @@ import testNotBlank from './testNotBlank'; const buildUUIDTestBatch: BuildInputTestBatchFunction = ( inputName, onSuccess, - { onFinishBatch, ...defaults } = {}, + { isRequired, onFinishBatch, ...defaults } = {}, onUUIDTestFailure, ) => ({ defaults: { ...defaults, onSuccess }, + isRequired, onFinishBatch, tests: [ { diff --git a/striker-ui/types/TestInputFunction.d.ts b/striker-ui/types/TestInputFunction.d.ts index 0cf81cb4..a8f5ce63 100644 --- a/striker-ui/types/TestInputFunction.d.ts +++ b/striker-ui/types/TestInputFunction.d.ts @@ -65,7 +65,8 @@ type InputTestBatch = { type BuildInputTestBatchFunction = ( inputName: string, onSuccess: InputTestSuccessCallback, - options?: InputTestBatch['defaults'] & Pick, + options?: InputTestBatch['defaults'] & + Pick, ...onFailureAppends: InputTestFailureAppendCallback[] ) => InputTestBatch; From 92f2c3626c4aebbef7a5202d0d79585d1391789a Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:04:21 -0500 Subject: [PATCH 68/75] fix(striker-ui): organize types in useFormUtils hook --- striker-ui/hooks/useFormUtils.ts | 47 ++++++++++---------------------- striker-ui/types/FormUtils.d.ts | 27 ++++++++++++++++++ 2 files changed, 41 insertions(+), 33 deletions(-) create mode 100644 striker-ui/types/FormUtils.d.ts diff --git a/striker-ui/hooks/useFormUtils.ts b/striker-ui/hooks/useFormUtils.ts index 8f71dfc3..70727b88 100644 --- a/striker-ui/hooks/useFormUtils.ts +++ b/striker-ui/hooks/useFormUtils.ts @@ -1,20 +1,9 @@ -import { - Dispatch, - MutableRefObject, - SetStateAction, - useCallback, - useMemo, - useState, -} from 'react'; +import { MutableRefObject, useCallback, useMemo, useState } from 'react'; import buildMapToMessageSetter from '../lib/buildMapToMessageSetter'; import buildObjectStateSetterCallback from '../lib/buildObjectStateSetterCallback'; import { MessageGroupForwardedRefContent } from '../components/MessageGroup'; -type FormValidity = { - [K in keyof T]?: boolean; -}; - const useFormUtils = < U extends string, I extends InputIds, @@ -22,37 +11,28 @@ const useFormUtils = < >( ids: I, messageGroupRef: MutableRefObject, -): { - buildFinishInputTestBatchFunction: ( - key: keyof M, - ) => (result: boolean) => void; - buildInputFirstRenderFunction: ( - key: keyof M, - ) => ({ isRequired }: { isRequired: boolean }) => void; - formValidity: FormValidity; - isFormInvalid: boolean; - msgSetters: MapToMessageSetter; - setFormValidity: Dispatch>>; -} => { +): FormUtils => { const [formValidity, setFormValidity] = useState>({}); + const setValidity = useCallback((key: keyof M, value: boolean) => { + setFormValidity( + buildObjectStateSetterCallback>(key, value), + ); + }, []); + const buildFinishInputTestBatchFunction = useCallback( (key: keyof M) => (result: boolean) => { - setFormValidity( - buildObjectStateSetterCallback>(key, result), - ); + setValidity(key, result); }, - [], + [setValidity], ); const buildInputFirstRenderFunction = useCallback( (key: keyof M) => - ({ isRequired }: { isRequired: boolean }) => { - setFormValidity( - buildObjectStateSetterCallback>(key, !isRequired), - ); + ({ isValid }: InputFirstRenderFunctionArgs) => { + setValidity(key, isValid); }, - [], + [setValidity], ); const isFormInvalid = useMemo( @@ -72,6 +52,7 @@ const useFormUtils = < isFormInvalid, msgSetters, setFormValidity, + setValidity, }; }; diff --git a/striker-ui/types/FormUtils.d.ts b/striker-ui/types/FormUtils.d.ts new file mode 100644 index 00000000..1f106121 --- /dev/null +++ b/striker-ui/types/FormUtils.d.ts @@ -0,0 +1,27 @@ +type FormValidity = { + [K in keyof T]?: boolean; +}; + +type InputTestBatchFinishCallbackBuilder = ( + key: keyof M, +) => InputTestBatchFinishCallback; + +type InputFirstRenderFunctionArgs = { isValid: boolean }; + +type InputFirstRenderFunction = (args: InputFirstRenderFunctionArgs) => void; + +type InputFirstRenderFunctionBuilder = ( + key: keyof M, +) => InputFirstRenderFunction; + +type FormUtils = { + buildFinishInputTestBatchFunction: InputTestBatchFinishCallbackBuilder; + buildInputFirstRenderFunction: InputFirstRenderFunctionBuilder; + formValidity: FormValidity; + isFormInvalid: boolean; + msgSetters: MapToMessageSetter; + setFormValidity: import('react').Dispatch< + import('react').SetStateAction> + >; + setValidity: (key: keyof M, value: boolean) => void; +}; From 10126a5a95636c39ccafb7bad2e3eeb4d2df3a56 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:06:07 -0500 Subject: [PATCH 69/75] fix(striker-ui): expose blur and focus event handler slots in SelectWithLabel --- striker-ui/components/SelectWithLabel.tsx | 4 ++++ striker-ui/types/SelectWithLabel.d.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/striker-ui/components/SelectWithLabel.tsx b/striker-ui/components/SelectWithLabel.tsx index 8a7d63e2..fabac9d8 100644 --- a/striker-ui/components/SelectWithLabel.tsx +++ b/striker-ui/components/SelectWithLabel.tsx @@ -23,7 +23,9 @@ const SelectWithLabel: FC = ({ isReadOnly = false, messageBoxProps = {}, name, + onBlur, onChange, + onFocus, required: isRequired, selectProps: { multiple: selectMultiple, @@ -110,7 +112,9 @@ const SelectWithLabel: FC = ({ input={inputElement} multiple={selectMultiple} name={name} + onBlur={onBlur} onChange={onChange} + onFocus={onFocus} readOnly={isReadOnly} value={selectValue} {...restSelectProps} diff --git a/striker-ui/types/SelectWithLabel.d.ts b/striker-ui/types/SelectWithLabel.d.ts index d799a1b5..5d020eb5 100644 --- a/striker-ui/types/SelectWithLabel.d.ts +++ b/striker-ui/types/SelectWithLabel.d.ts @@ -25,7 +25,7 @@ type SelectWithLabelOptionalProps = { }; type SelectWithLabelProps = SelectWithLabelOptionalProps & - Pick & { + Pick & { id: string; selectItems: Array; }; From 8abd47eb18ddb6d470335c7c328f935da3c30cd4 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:09:44 -0500 Subject: [PATCH 70/75] fix(striker-ui): correct validity test on first render in InputWithRef --- striker-ui/components/InputWithRef.tsx | 17 ++++++++++------- .../components/StrikerConfig/AddPeerDialog.tsx | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/striker-ui/components/InputWithRef.tsx b/striker-ui/components/InputWithRef.tsx index 89c016f4..16f769aa 100644 --- a/striker-ui/components/InputWithRef.tsx +++ b/striker-ui/components/InputWithRef.tsx @@ -5,7 +5,6 @@ import { forwardRef, ReactElement, useCallback, - useEffect, useImperativeHandle, useMemo, useState, @@ -26,7 +25,7 @@ type InputWithRefOptionalPropsWithoutDefault< TypeName extends keyof MapToInputType, > = { inputTestBatch?: InputTestBatch; - onFirstRender?: (args: { isRequired: boolean }) => void; + onFirstRender?: InputFirstRenderFunction; valueKey?: CreateInputOnChangeHandlerOptions['valueKey']; }; @@ -167,11 +166,15 @@ const InputWithRef = forwardRef( [initOnFocus, inputTestBatch], ); - useEffect(() => { - if (isFirstRender) { - onFirstRender?.call(null, { isRequired }); - } - }, [isFirstRender, isRequired, onFirstRender]); + if (isFirstRender) { + const isValid = + testInput?.call(null, { + inputs: { [INPUT_TEST_ID]: { value: inputValue } }, + isIgnoreOnCallbacks: true, + }) ?? false; + + onFirstRender?.call(null, { isValid }); + } useImperativeHandle( ref, diff --git a/striker-ui/components/StrikerConfig/AddPeerDialog.tsx b/striker-ui/components/StrikerConfig/AddPeerDialog.tsx index e8cb8dc3..1ccfcaad 100644 --- a/striker-ui/components/StrikerConfig/AddPeerDialog.tsx +++ b/striker-ui/components/StrikerConfig/AddPeerDialog.tsx @@ -61,8 +61,8 @@ const AddPeerDialog = forwardRef< const buildInputFirstRenderFunction = useCallback( (key: string) => - ({ isRequired }: { isRequired: boolean }) => { - setFormValidity(buildObjectStateSetterCallback(key, !isRequired)); + ({ isValid }: InputFirstRenderFunctionArgs) => { + setFormValidity(buildObjectStateSetterCallback(key, isValid)); }, [], ); From 5f4ecf7bcf583af3977c5521ab6cc3ad2cc1f22f Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:12:24 -0500 Subject: [PATCH 71/75] fix(striker-ui): add input tests to CommonUpsInputGroup --- .../ManageUps/CommonUpsInputGroup.tsx | 131 ++++++++++++------ striker-ui/types/CommonUpsInputGroup.d.ts | 5 +- 2 files changed, 95 insertions(+), 41 deletions(-) diff --git a/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx b/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx index 6e1d49a9..7e744061 100644 --- a/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx +++ b/striker-ui/components/ManageUps/CommonUpsInputGroup.tsx @@ -1,53 +1,104 @@ -import { FC } from 'react'; +import { ReactElement } from 'react'; import Grid from '../Grid'; import InputWithRef from '../InputWithRef'; import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; +import { + buildIPAddressTestBatch, + buildPeacefulStringTestBatch, +} from '../../lib/test_input'; const INPUT_ID_UPS_IP = 'common-ups-input-ip-address'; const INPUT_ID_UPS_NAME = 'common-ups-input-host-name'; -const CommonUpsInputGroup: FC = ({ +const INPUT_LABEL_UPS_IP = 'IP address'; +const INPUT_LABEL_UPS_NAME = 'Host name'; + +const CommonUpsInputGroup = < + M extends { + [K in typeof INPUT_ID_UPS_IP | typeof INPUT_ID_UPS_NAME]: string; + }, +>({ + formUtils: { + buildFinishInputTestBatchFunction, + buildInputFirstRenderFunction, + msgSetters, + }, previous: { upsIPAddress: previousIpAddress, upsName: previousUpsName } = {}, -}) => ( - <> - - } - required - /> - ), - }, - 'common-ups-input-cell-ip-address': { - children: ( - - } - required - /> - ), - }, - }} - spacing="1em" - /> - +}: CommonUpsInputGroupProps): ReactElement => ( + + } + inputTestBatch={buildPeacefulStringTestBatch( + INPUT_LABEL_UPS_NAME, + () => { + msgSetters[INPUT_ID_UPS_NAME](); + }, + { + onFinishBatch: + buildFinishInputTestBatchFunction(INPUT_ID_UPS_NAME), + }, + (message) => { + msgSetters[INPUT_ID_UPS_NAME]({ + children: message, + }); + }, + )} + onFirstRender={buildInputFirstRenderFunction(INPUT_ID_UPS_NAME)} + required + /> + ), + }, + 'common-ups-input-cell-ip-address': { + children: ( + + } + inputTestBatch={buildIPAddressTestBatch( + INPUT_LABEL_UPS_IP, + () => { + msgSetters[INPUT_ID_UPS_IP](); + }, + { + onFinishBatch: + buildFinishInputTestBatchFunction(INPUT_ID_UPS_IP), + }, + (message) => { + msgSetters[INPUT_ID_UPS_IP]({ + children: message, + }); + }, + )} + onFirstRender={buildInputFirstRenderFunction(INPUT_ID_UPS_IP)} + required + /> + ), + }, + }} + spacing="1em" + /> ); -export { INPUT_ID_UPS_IP, INPUT_ID_UPS_NAME }; +export { + INPUT_ID_UPS_IP, + INPUT_ID_UPS_NAME, + INPUT_LABEL_UPS_IP, + INPUT_LABEL_UPS_NAME, +}; export default CommonUpsInputGroup; diff --git a/striker-ui/types/CommonUpsInputGroup.d.ts b/striker-ui/types/CommonUpsInputGroup.d.ts index da058a40..020f814f 100644 --- a/striker-ui/types/CommonUpsInputGroup.d.ts +++ b/striker-ui/types/CommonUpsInputGroup.d.ts @@ -5,4 +5,7 @@ type CommonUpsInputGroupOptionalProps = { }; }; -type CommonUpsInputGroupProps = CommonUpsInputGroupOptionalProps; +type CommonUpsInputGroupProps = + CommonUpsInputGroupOptionalProps & { + formUtils: FormUtils; + }; From c0fb71ea29ca7582e78aa5084ec16200af43e092 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:13:40 -0500 Subject: [PATCH 72/75] fix(striker-ui): add input validation to AddUpsInputGroup --- .../components/ManageUps/AddUpsInputGroup.tsx | 57 +++++++++++++++---- striker-ui/types/AddUpsInputGroup.d.ts | 8 ++- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/striker-ui/components/ManageUps/AddUpsInputGroup.tsx b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx index a81a4e2f..e0896c23 100644 --- a/striker-ui/components/ManageUps/AddUpsInputGroup.tsx +++ b/striker-ui/components/ManageUps/AddUpsInputGroup.tsx @@ -1,23 +1,41 @@ -import { FC, ReactElement, ReactNode, useMemo, useState } from 'react'; +import { ReactElement, ReactNode, useMemo, useState } from 'react'; import { BLACK } from '../../lib/consts/DEFAULT_THEME'; -import CommonUpsInputGroup from './CommonUpsInputGroup'; +import CommonUpsInputGroup, { + INPUT_ID_UPS_IP, + INPUT_ID_UPS_NAME, +} from './CommonUpsInputGroup'; import FlexBox from '../FlexBox'; import Link from '../Link'; import SelectWithLabel from '../SelectWithLabel'; import Spinner from '../Spinner'; import { BodyText } from '../Text'; +import useIsFirstRender from '../../hooks/useIsFirstRender'; -const INPUT_ID_UPS_TYPE_ID = 'add-ups-select-ups-type-id'; +const INPUT_ID_UPS_TYPE = 'add-ups-select-ups-type-id'; -const AddUpsInputGroup: FC = ({ +const INPUT_LABEL_UPS_TYPE = 'UPS type'; + +const AddUpsInputGroup = < + M extends { + [K in + | typeof INPUT_ID_UPS_IP + | typeof INPUT_ID_UPS_NAME + | typeof INPUT_ID_UPS_TYPE]: string; + }, +>({ + formUtils, loading: isExternalLoading, previous = {}, upsTemplate, -}) => { +}: AddUpsInputGroupProps): ReactElement => { + const { buildInputFirstRenderFunction, setValidity } = formUtils; + const { upsTypeId: previousUpsTypeId = '' } = previous; + const isFirstRender = useIsFirstRender(); + const [inputUpsTypeIdValue, setInputUpsTypeIdValue] = useState(previousUpsTypeId); @@ -75,17 +93,19 @@ const AddUpsInputGroup: FC = ({ upsTemplate && ( { const newValue = String(rawNewValue); + setValidity(INPUT_ID_UPS_TYPE, true); setInputUpsTypeIdValue(newValue); }} required selectItems={upsTypeOptions} selectProps={{ onClearIndicatorClick: () => { + setValidity(INPUT_ID_UPS_TYPE, false); setInputUpsTypeIdValue(''); }, renderValue: (rawValue) => { @@ -98,8 +118,9 @@ const AddUpsInputGroup: FC = ({ value={inputUpsTypeIdValue} /> ), - [inputUpsTypeIdValue, upsTypeOptions, upsTemplate], + [upsTemplate, upsTypeOptions, inputUpsTypeIdValue, setValidity], ); + const content = useMemo( () => isExternalLoading ? ( @@ -107,15 +128,29 @@ const AddUpsInputGroup: FC = ({ ) : ( {pickUpsTypeElement} - {inputUpsTypeIdValue && } + {inputUpsTypeIdValue && ( + + )} ), - [inputUpsTypeIdValue, isExternalLoading, pickUpsTypeElement, previous], + [ + formUtils, + inputUpsTypeIdValue, + isExternalLoading, + pickUpsTypeElement, + previous, + ], ); + if (isFirstRender) { + buildInputFirstRenderFunction(INPUT_ID_UPS_TYPE)({ + isValid: Boolean(inputUpsTypeIdValue), + }); + } + return content; }; -export { INPUT_ID_UPS_TYPE_ID }; +export { INPUT_ID_UPS_TYPE, INPUT_LABEL_UPS_TYPE }; export default AddUpsInputGroup; diff --git a/striker-ui/types/AddUpsInputGroup.d.ts b/striker-ui/types/AddUpsInputGroup.d.ts index 76e43636..e053670f 100644 --- a/striker-ui/types/AddUpsInputGroup.d.ts +++ b/striker-ui/types/AddUpsInputGroup.d.ts @@ -1,7 +1,11 @@ type AddUpsInputGroupOptionalProps = { loading?: boolean; - previous?: CommonUpsInputGroupProps['previous'] & { upsTypeId?: string }; + previous?: CommonUpsInputGroupOptionalProps['previous'] & { + upsTypeId?: string; + }; upsTemplate?: APIUpsTemplate; }; -type AddUpsInputGroupProps = AddUpsInputGroupOptionalProps; +type AddUpsInputGroupProps = + AddUpsInputGroupOptionalProps & + Pick, 'formUtils'>; From 7976170ca25290628fb4360928a12748f3b4e919 Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:16:25 -0500 Subject: [PATCH 73/75] fix(striker-ui): passthrough input validation in EditUpsInputGroup --- .../ManageUps/EditUpsInputGroup.tsx | 25 ++++++++++++++----- striker-ui/types/EditUpsInputGroup.d.ts | 9 ++++--- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/striker-ui/components/ManageUps/EditUpsInputGroup.tsx b/striker-ui/components/ManageUps/EditUpsInputGroup.tsx index 963b89d9..47320378 100644 --- a/striker-ui/components/ManageUps/EditUpsInputGroup.tsx +++ b/striker-ui/components/ManageUps/EditUpsInputGroup.tsx @@ -1,27 +1,40 @@ -import { FC, ReactElement, useMemo } from 'react'; +import { ReactElement, useMemo } from 'react'; -import AddUpsInputGroup from './AddUpsInputGroup'; +import AddUpsInputGroup, { INPUT_ID_UPS_TYPE } from './AddUpsInputGroup'; +import { INPUT_ID_UPS_IP, INPUT_ID_UPS_NAME } from './CommonUpsInputGroup'; import Spinner from '../Spinner'; const INPUT_ID_UPS_UUID = 'edit-ups-input-ups-uuid'; -const EditUpsInputGroup: FC = ({ +const EditUpsInputGroup = < + M extends { + [K in + | typeof INPUT_ID_UPS_IP + | typeof INPUT_ID_UPS_NAME + | typeof INPUT_ID_UPS_TYPE]: string; + }, +>({ + formUtils, loading: isExternalLoading, previous, upsTemplate, upsUUID, -}) => { +}: EditUpsInputGroupProps): ReactElement => { const content = useMemo( () => isExternalLoading ? ( ) : ( <> - + ), - [isExternalLoading, previous, upsTemplate, upsUUID], + [formUtils, isExternalLoading, previous, upsTemplate, upsUUID], ); return content; diff --git a/striker-ui/types/EditUpsInputGroup.d.ts b/striker-ui/types/EditUpsInputGroup.d.ts index ebff2166..4c75a28b 100644 --- a/striker-ui/types/EditUpsInputGroup.d.ts +++ b/striker-ui/types/EditUpsInputGroup.d.ts @@ -2,7 +2,8 @@ type EditUpsInputGroupOptionalProps = { loading?: boolean; }; -type EditUpsInputGroupProps = EditUpsInputGroupOptionalProps & - Pick & { - upsUUID: string; - }; +type EditUpsInputGroupProps = + EditUpsInputGroupOptionalProps & + Pick, 'formUtils' | 'previous' | 'upsTemplate'> & { + upsUUID: string; + }; From 4eeb1d676fa4bbdf2926c6457cb35c26b3ae6b1c Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:18:07 -0500 Subject: [PATCH 74/75] fix(striker-ui): add form validation and message in ManageUpsPanel --- .../components/ManageUps/ManageUpsPanel.tsx | 35 +++++++++++++++---- 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/striker-ui/components/ManageUps/ManageUpsPanel.tsx b/striker-ui/components/ManageUps/ManageUpsPanel.tsx index 7c4d3f04..e6d10305 100644 --- a/striker-ui/components/ManageUps/ManageUpsPanel.tsx +++ b/striker-ui/components/ManageUps/ManageUpsPanel.tsx @@ -9,7 +9,7 @@ import { import API_BASE_URL from '../../lib/consts/API_BASE_URL'; -import AddUpsInputGroup, { INPUT_ID_UPS_TYPE_ID } from './AddUpsInputGroup'; +import AddUpsInputGroup, { INPUT_ID_UPS_TYPE } from './AddUpsInputGroup'; import api from '../../lib/api'; import { INPUT_ID_UPS_IP, INPUT_ID_UPS_NAME } from './CommonUpsInputGroup'; import ConfirmDialog from '../ConfirmDialog'; @@ -18,11 +18,13 @@ import FlexBox from '../FlexBox'; import FormDialog from '../FormDialog'; import handleAPIError from '../../lib/handleAPIError'; import List from '../List'; +import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup'; import { Panel, PanelHeader } from '../Panels'; import periodicFetch from '../../lib/fetchers/periodicFetch'; import Spinner from '../Spinner'; import { BodyText, HeaderText, InlineMonoText, MonoText } from '../Text'; import useConfirmDialogProps from '../../hooks/useConfirmDialogProps'; +import useFormUtils from '../../hooks/useFormUtils'; import useIsFirstRender from '../../hooks/useIsFirstRender'; import useProtectedState from '../../hooks/useProtectedState'; @@ -48,7 +50,7 @@ const getUpsFormData = ( INPUT_ID_UPS_IP, ) as HTMLInputElement; - const inputUpsTypeId = elements.namedItem(INPUT_ID_UPS_TYPE_ID); + const inputUpsTypeId = elements.namedItem(INPUT_ID_UPS_TYPE); let upsAgent = ''; let upsBrand = ''; @@ -101,6 +103,7 @@ const ManageUpsPanel: FC = () => { const confirmDialogRef = useRef({}); const formDialogRef = useRef({}); + const messageGroupRef = useRef({}); const [confirmDialogProps, setConfirmDialogProps] = useConfirmDialogProps(); const [formDialogProps, setFormDialogProps] = useConfirmDialogProps(); @@ -116,6 +119,12 @@ const ManageUpsPanel: FC = () => { refreshInterval: 60000, }); + const formUtils = useFormUtils( + [INPUT_ID_UPS_IP, INPUT_ID_UPS_NAME, INPUT_ID_UPS_TYPE], + messageGroupRef, + ); + const { isFormInvalid } = formUtils; + const buildEditUpsFormDialogProps = useCallback< (args: APIUpsOverview[string]) => ConfirmDialogProps >( @@ -131,6 +140,7 @@ const ManageUpsPanel: FC = () => { actionProceedText: 'Update', content: ( { ), }; }, - [setConfirmDialogProps, upsTemplate], + [formUtils, setConfirmDialogProps, upsTemplate], ); const addUpsFormDialogProps = useMemo( () => ({ actionProceedText: 'Add', - content: , + content: ( + + ), onSubmitAppend: (event) => { if (!upsTemplate) { return; @@ -201,7 +213,7 @@ const ManageUpsPanel: FC = () => { }, titleText: 'Add a UPS', }), - [setConfirmDialogProps, upsTemplate], + [formUtils, setConfirmDialogProps, upsTemplate], ); const listElement = useMemo( @@ -269,7 +281,18 @@ const ManageUpsPanel: FC = () => { {panelContent} - + + } + proceedButtonProps={{ disabled: isFormInvalid }} + /> ); From aba2c68f5c0e3526f064db52bcbabf894382f46a Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 3 Mar 2023 21:18:54 -0500 Subject: [PATCH 75/75] fix(striker-ui): add manage UPS tab --- striker-ui/pages/manage-element/index.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/striker-ui/pages/manage-element/index.tsx b/striker-ui/pages/manage-element/index.tsx index 18eceb99..54fdcb3a 100644 --- a/striker-ui/pages/manage-element/index.tsx +++ b/striker-ui/pages/manage-element/index.tsx @@ -8,6 +8,7 @@ import Grid from '../../components/Grid'; import handleAPIError from '../../lib/handleAPIError'; import Header from '../../components/Header'; import ManageFencePanel from '../../components/ManageFence'; +import ManageUpsPanel from '../../components/ManageUps'; import { Panel } from '../../components/Panels'; import PrepareHostForm from '../../components/PrepareHostForm'; import PrepareNetworkForm from '../../components/PrepareNetworkForm'; @@ -130,6 +131,19 @@ const ManageFenceTabContent: FC = () => ( /> ); +const ManageUpsTabContent: FC = () => ( + , + ...STEP_CONTENT_GRID_CENTER_COLUMN, + }, + }} + /> +); + const ManageElement: FC = () => { const { isReady, @@ -177,6 +191,7 @@ const ManageElement: FC = () => { + @@ -188,6 +203,9 @@ const ManageElement: FC = () => { + + + ); };