diff --git a/striker-ui/components/GeneralInitForm.tsx b/striker-ui/components/GeneralInitForm.tsx new file mode 100644 index 00000000..47432198 --- /dev/null +++ b/striker-ui/components/GeneralInitForm.tsx @@ -0,0 +1,279 @@ +import { Box as MUIBox } from '@mui/material'; +import { + forwardRef, + ForwardRefExoticComponent, + RefAttributes, + useImperativeHandle, + useState, +} from 'react'; + +import createFunction from '../lib/createFunction'; +import createInputOnChangeHandler from '../lib/createInputOnChangeHandler'; +import FlexBox from './FlexBox'; +import isEmpty from '../lib/isEmpty'; +import OutlinedInputWithLabel from './OutlinedInputWithLabel'; +import pad from '../lib/pad'; +import SuggestButton from './SuggestButton'; + +const MAX_ORGANIZATION_PREFIX_LENGTH = 5; +const MIN_ORGANIZATION_PREFIX_LENGTH = 2; +const MAX_HOST_NUMBER_LENGTH = 2; + +const MAP_TO_ORGANIZATION_PREFIX_BUILDER: Record< + number, + (words: string[]) => string +> = { + 0: () => '', + 1: ([word]) => + word.substring(0, MIN_ORGANIZATION_PREFIX_LENGTH).toLocaleLowerCase(), + 2: (words) => + words.map((word) => word.substring(0, 1).toLocaleLowerCase()).join(''), +}; + +const buildOrganizationPrefix = (organizationName: string) => { + const words: string[] = organizationName + .split(/\s/) + .filter((word) => !/and|of/.test(word)) + .slice(0, MAX_ORGANIZATION_PREFIX_LENGTH); + + const builderKey: number = words.length > 1 ? 2 : words.length; + + return MAP_TO_ORGANIZATION_PREFIX_BUILDER[builderKey](words); +}; + +const buildHostName = ( + organizationPrefix: string, + hostNumber: number, + domainName: string, +) => + isEmpty([organizationPrefix, hostNumber, domainName], { not: true }) + ? `${organizationPrefix}-striker${pad(hostNumber)}.${domainName}` + : ''; + +const GeneralInitForm: ForwardRefExoticComponent> = + forwardRef((generalInitFormProps, ref) => { + const [organizationNameInput, setOrganizationNameInput] = + useState(''); + const [organizationPrefixInput, setOrganizationPrefixInput] = + useState(''); + const [ + isOrganizationPrefixInputUserChanged, + setIsOrganizationPrefixInputUserChanged, + ] = useState(false); + const [domainNameInput, setDomainNameInput] = useState(''); + const [hostNumberInput, setHostNumberInput] = useState(0); + const [hostNameInput, setHostNameInput] = useState(''); + const [isHostNameInputUserChanged, setIsHostNameInputUserChanged] = + useState(false); + + const handleOrganizationNameInputOnChange = createInputOnChangeHandler({ + set: setOrganizationNameInput, + }); + const handleOrganizationPrefixInputOnChange = createInputOnChangeHandler({ + postSet: () => { + setIsOrganizationPrefixInputUserChanged(true); + }, + set: setOrganizationPrefixInput, + }); + const handleDomainNameInputOnChange = createInputOnChangeHandler({ + set: setDomainNameInput, + }); + const handleHostNumberInputOnChange = createInputOnChangeHandler({ + set: setHostNumberInput, + setType: 'number', + }); + const handleHostNameInputOnChange = createInputOnChangeHandler({ + postSet: () => { + setIsHostNameInputUserChanged(true); + }, + set: setHostNameInput, + }); + const populateOrganizationPrefixInput = ({ + organizationName = organizationNameInput, + } = {}) => { + const organizationPrefix = buildOrganizationPrefix(organizationName); + + setOrganizationPrefixInput(organizationPrefix); + + return organizationPrefix; + }; + const populateHostNameInput = ({ + organizationPrefix = organizationPrefixInput, + hostNumber = hostNumberInput, + domainName = domainNameInput, + } = {}) => { + const hostName = buildHostName( + organizationPrefix, + hostNumber, + domainName, + ); + + setHostNameInput(hostName); + + return hostName; + }; + const populateOrganizationPrefixInputOnBlur = createFunction( + { condition: !isOrganizationPrefixInputUserChanged }, + populateOrganizationPrefixInput, + ); + const populateHostNameInputOnBlur = createFunction( + { condition: !isHostNameInputUserChanged }, + populateHostNameInput, + ); + const handleOrganizationPrefixSuggest = createFunction( + { + conditionFn: () => + isOrganizationPrefixInputUserChanged && + isEmpty([organizationNameInput], { not: true }), + }, + () => { + const organizationPrefix = populateOrganizationPrefixInput(); + + if (!isHostNameInputUserChanged) { + populateHostNameInput({ organizationPrefix }); + } + }, + ); + const handlerHostNameSuggest = createFunction( + { + conditionFn: () => + isHostNameInputUserChanged && + isEmpty([organizationPrefixInput, hostNumberInput, domainNameInput], { + not: true, + }), + }, + populateHostNameInput, + ); + + useImperativeHandle( + ref, + () => ({ + organizationNameInput, + organizationPrefixInput, + domainNameInput, + hostNumberInput, + hostNameInput, + }), + [ + organizationNameInput, + organizationPrefixInput, + domainNameInput, + hostNumberInput, + hostNameInput, + ], + ); + + return ( + *': { + flexBasis: '50%', + }, + + '& > :not(:first-child)': { + marginLeft: { xs: 0, sm: '1em' }, + marginTop: { xs: '1em', sm: 0 }, + }, + }} + > + + + + ), + inputProps: { + maxLength: MAX_ORGANIZATION_PREFIX_LENGTH, + style: { width: '2.5em' }, + }, + onBlur: populateHostNameInputOnBlur, + sx: { + minWidth: 'min-content', + width: 'fit-content', + }, + }} + label="Prefix" + onChange={handleOrganizationPrefixInputOnChange} + value={organizationPrefixInput} + /> + + + + + , + inputProps: { + style: { + minWidth: '4em', + }, + }, + sx: { + minWidth: 'min-content', + }, + }} + label="Host name" + onChange={handleHostNameInputOnChange} + value={hostNameInput} + /> + + + ); + }); + +GeneralInitForm.displayName = 'GeneralInitForm'; + +export default GeneralInitForm; diff --git a/striker-ui/components/NetworkInitForm.tsx b/striker-ui/components/NetworkInitForm.tsx index ed662ffa..e910a236 100644 --- a/striker-ui/components/NetworkInitForm.tsx +++ b/striker-ui/components/NetworkInitForm.tsx @@ -1,4 +1,3 @@ -import { FC, useEffect, useState } from 'react'; import { Box as MUIBox, BoxProps as MUIBoxProps, @@ -16,6 +15,7 @@ import { DataGridProps as MUIDataGridProps, gridClasses as muiGridClasses, } from '@mui/x-data-grid'; +import { FC, useEffect, useState } from 'react'; import { v4 as uuidv4 } from 'uuid'; import API_BASE_URL from '../lib/consts/API_BASE_URL'; @@ -33,14 +33,7 @@ import sumstring from '../lib/sumstring'; import { BodyText, DataGridCellText } from './Text'; import IconButton from './IconButton'; -export type NetworkInterfaceInputMap = Record< - string, - { - isApplied?: boolean; - } ->; - -export type NetworkInput = { +type NetworkInput = { inputUUID: string; interfaces: (NetworkInterfaceOverviewMetadata | undefined)[]; ipAddress: string; @@ -49,6 +42,13 @@ export type NetworkInput = { type: string; }; +type NetworkInterfaceInputMap = Record< + string, + { + isApplied?: boolean; + } +>; + const MOCK_NICS: NetworkInterfaceOverviewMetadata[] = [ { networkInterfaceUUID: 'fe299134-c8fe-47bd-ab7a-3aa95eada1f6', @@ -617,4 +617,6 @@ const NetworkInitForm: FC = () => { ); }; +export type { NetworkInput, NetworkInterfaceInputMap }; + export default NetworkInitForm; diff --git a/striker-ui/components/StrikerInitForm.tsx b/striker-ui/components/StrikerInitForm.tsx index a19006a9..d8b1bf0e 100644 --- a/striker-ui/components/StrikerInitForm.tsx +++ b/striker-ui/components/StrikerInitForm.tsx @@ -1,356 +1,45 @@ -import { Dispatch, FC, SetStateAction, useState } from 'react'; -import { Box as MUIBox } from '@mui/material'; +import { FC, useRef, useState } from 'react'; -import ContainedButton, { ContainedButtonProps } from './ContainedButton'; +import ContainedButton from './ContainedButton'; import FlexBox from './FlexBox'; +import GeneralInitForm from './GeneralInitForm'; import NetworkInitForm from './NetworkInitForm'; -import { OutlinedInputProps } from './OutlinedInput'; -import OutlinedInputWithLabel from './OutlinedInputWithLabel'; -import pad from '../lib/pad'; import { Panel, PanelHeader } from './Panels'; -import { HeaderText } from './Text'; +import { BodyText, HeaderText } from './Text'; -const MAX_ORGANIZATION_PREFIX_LENGTH = 5; -const MIN_ORGANIZATION_PREFIX_LENGTH = 2; -const MAX_HOST_NUMBER_LENGTH = 2; +const StrikerInitForm: FC = () => { + const [requestBody, setRequestBody] = useState< + Record | undefined + >(); -const MAP_TO_ORGANIZATION_PREFIX_BUILDER: Record< - number, - (words: string[]) => string -> = { - 0: () => '', - 1: ([word]) => - word.substring(0, MIN_ORGANIZATION_PREFIX_LENGTH).toLocaleLowerCase(), - 2: (words) => - words.map((word) => word.substring(0, 1).toLocaleLowerCase()).join(''), -}; - -export type MapToType = { - number: number; - string: string; -}; - -export type MapToStateSetter = { - [TypeName in keyof MapToType]: Dispatch>; -}; - -export type MapToValueConverter = { - [TypeName in keyof MapToType]: (value: unknown) => MapToType[TypeName]; -}; - -export type MapToValueIsEmptyFunction = { - [TypeName in keyof MapToType]: (value: MapToType[TypeName]) => boolean; -}; - -export type InputOnChangeParameters = Parameters< - Exclude ->; - -const MAP_TO_VALUE_CONVERTER: MapToValueConverter = { - number: (value) => parseInt(String(value), 10) || 0, - string: (value) => String(value), -}; - -const MAP_TO_VALUE_IS_EMPTY_FUNCTION: MapToValueIsEmptyFunction = { - number: (value: number) => value === 0, - string: (value: string) => value.trim().length === 0, -}; - -const createInputOnChangeHandler = - ({ - postSet, - preSet, - set, - setType = 'string', - }: { - postSet?: (...args: InputOnChangeParameters) => void; - preSet?: (...args: InputOnChangeParameters) => void; - set?: MapToStateSetter[TypeName]; - setType?: TypeName | 'string'; - }): OutlinedInputProps['onChange'] => - (event) => { - const { - target: { value }, - } = event; - const postConvertValue = MAP_TO_VALUE_CONVERTER[setType]( - value, - ) as MapToType[TypeName]; - - preSet?.call(null, event); - set?.call(null, postConvertValue); - postSet?.call(null, event); - }; - -const isEmpty = ( - values: Array, - { not, fn = 'every' }: { not?: boolean; fn?: 'every' | 'some' }, -) => - values[fn]((value) => { - const type = typeof value as TypeName; - - let result = MAP_TO_VALUE_IS_EMPTY_FUNCTION[type](value); - - if (not) { - result = !result; - } - - return result; - }); - -const createFunction = ( - { - conditionFn = () => true, - str = '', - condition = conditionFn() && str.length === 0, - }: { - condition?: boolean; - conditionFn?: (...args: unknown[]) => boolean; - str?: string; - }, - fn: () => unknown, - ...fnArgs: Parameters -) => (condition ? fn.bind(null, ...fnArgs) : undefined); - -const buildOrganizationPrefix = (organizationName: string) => { - const words: string[] = organizationName - .split(/\s/) - .filter((word) => !/and|of/.test(word)) - .slice(0, MAX_ORGANIZATION_PREFIX_LENGTH); - - const builderKey: number = words.length > 1 ? 2 : words.length; - - return MAP_TO_ORGANIZATION_PREFIX_BUILDER[builderKey](words); -}; - -const buildHostName = ( - organizationPrefix: string, - hostNumber: number, - domainName: string, -) => - isEmpty([organizationPrefix, hostNumber, domainName], { not: true }) - ? `${organizationPrefix}-striker${pad(hostNumber)}.${domainName}` - : ''; - -const SuggestButton: FC = ({ onClick, ...restProps }) => - onClick ? ( - - Suggest - - ) : ( - <> - ); - -const StrikerInitGeneralForm: FC = () => { - const [organizationNameInput, setOrganizationNameInput] = - useState(''); - const [organizationPrefixInput, setOrganizationPrefixInput] = - useState(''); - const [ - isOrganizationPrefixInputUserChanged, - setIsOrganizationPrefixInputUserChanged, - ] = useState(false); - const [domainNameInput, setDomainNameInput] = useState(''); - const [hostNumberInput, setHostNumberInput] = useState(0); - const [hostNameInput, setHostNameInput] = useState(''); - const [isHostNameInputUserChanged, setIsHostNameInputUserChanged] = - useState(false); - - const handleOrganizationNameInputOnChange = createInputOnChangeHandler({ - set: setOrganizationNameInput, - }); - const handleOrganizationPrefixInputOnChange = createInputOnChangeHandler({ - postSet: () => { - setIsOrganizationPrefixInputUserChanged(true); - }, - set: setOrganizationPrefixInput, - }); - const handleDomainNameInputOnChange = createInputOnChangeHandler({ - set: setDomainNameInput, - }); - const handleHostNumberInputOnChange = createInputOnChangeHandler({ - set: setHostNumberInput, - setType: 'number', - }); - const handleHostNameInputOnChange = createInputOnChangeHandler({ - postSet: () => { - setIsHostNameInputUserChanged(true); - }, - set: setHostNameInput, - }); - const populateOrganizationPrefixInput = ({ - organizationName = organizationNameInput, - } = {}) => { - const organizationPrefix = buildOrganizationPrefix(organizationName); - - setOrganizationPrefixInput(organizationPrefix); - - return organizationPrefix; - }; - const populateHostNameInput = ({ - organizationPrefix = organizationPrefixInput, - hostNumber = hostNumberInput, - domainName = domainNameInput, - } = {}) => { - const hostName = buildHostName(organizationPrefix, hostNumber, domainName); - - setHostNameInput(hostName); - - return hostName; - }; - const populateOrganizationPrefixInputOnBlur = createFunction( - { condition: !isOrganizationPrefixInputUserChanged }, - populateOrganizationPrefixInput, - ); - const populateHostNameInputOnBlur = createFunction( - { condition: !isHostNameInputUserChanged }, - populateHostNameInput, - ); - const handleOrganizationPrefixSuggest = createFunction( - { - conditionFn: () => - isOrganizationPrefixInputUserChanged && - isEmpty([organizationNameInput], { not: true }), - }, - () => { - const organizationPrefix = populateOrganizationPrefixInput(); - - if (!isHostNameInputUserChanged) { - populateHostNameInput({ organizationPrefix }); - } - }, - ); - const handlerHostNameSuggest = createFunction( - { - conditionFn: () => - isHostNameInputUserChanged && - isEmpty([organizationPrefixInput, hostNumberInput, domainNameInput], { - not: true, - }), - }, - populateHostNameInput, - ); + const generalInitFormRef = useRef(); return ( - *': { - flexBasis: '50%', - }, - - '& > :not(:first-child)': { - marginLeft: { xs: 0, sm: '1em' }, - marginTop: { xs: '1em', sm: 0 }, - }, - }} - > + + + + - - - ), - inputProps: { - maxLength: MAX_ORGANIZATION_PREFIX_LENGTH, - style: { width: '2.5em' }, - }, - onBlur: populateHostNameInputOnBlur, - sx: { - minWidth: 'min-content', - width: 'fit-content', - }, - }} - label="Prefix" - onChange={handleOrganizationPrefixInputOnChange} - value={organizationPrefixInput} - /> + + + + { + setRequestBody(generalInitFormRef.current); + }} + > + Initialize + + + {requestBody && ( + + )} - - - - , - inputProps: { - style: { - minWidth: '4em', - }, - }, - sx: { - minWidth: 'min-content', - }, - }} - label="Host name" - onChange={handleHostNameInputOnChange} - value={hostNameInput} - /> - - + ); }; -const StrikerInitForm: FC = () => ( - - - - - - - - - -); - export default StrikerInitForm; diff --git a/striker-ui/components/SuggestButton.tsx b/striker-ui/components/SuggestButton.tsx new file mode 100644 index 00000000..e1fc1f63 --- /dev/null +++ b/striker-ui/components/SuggestButton.tsx @@ -0,0 +1,14 @@ +import { FC } from 'react'; + +import ContainedButton, { ContainedButtonProps } from './ContainedButton'; + +const SuggestButton: FC = ({ onClick, ...restProps }) => + onClick ? ( + + Suggest + + ) : ( + <> + ); + +export default SuggestButton;