fix(striker-ui): break striker init form

main
Tsu-ba-me 2 years ago
parent debb607212
commit 0c7c1591ad
  1. 279
      striker-ui/components/GeneralInitForm.tsx
  2. 20
      striker-ui/components/NetworkInitForm.tsx
  3. 365
      striker-ui/components/StrikerInitForm.tsx
  4. 14
      striker-ui/components/SuggestButton.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<RefAttributes<unknown>> =
forwardRef((generalInitFormProps, ref) => {
const [organizationNameInput, setOrganizationNameInput] =
useState<string>('');
const [organizationPrefixInput, setOrganizationPrefixInput] =
useState<string>('');
const [
isOrganizationPrefixInputUserChanged,
setIsOrganizationPrefixInputUserChanged,
] = useState<boolean>(false);
const [domainNameInput, setDomainNameInput] = useState<string>('');
const [hostNumberInput, setHostNumberInput] = useState<number>(0);
const [hostNameInput, setHostNameInput] = useState<string>('');
const [isHostNameInputUserChanged, setIsHostNameInputUserChanged] =
useState<boolean>(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 (
<MUIBox
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
'& > *': {
flexBasis: '50%',
},
'& > :not(:first-child)': {
marginLeft: { xs: 0, sm: '1em' },
marginTop: { xs: '1em', sm: 0 },
},
}}
>
<FlexBox>
<OutlinedInputWithLabel
helpMessageBoxProps={{
text: 'Name of the organization that maintains this Anvil! system. You can enter anything that makes sense to you.',
}}
id="striker-init-general-organization-name"
inputProps={{
onBlur: populateOrganizationPrefixInputOnBlur,
}}
label="Organization name"
onChange={handleOrganizationNameInputOnChange}
value={organizationNameInput}
/>
<OutlinedInputWithLabel
helpMessageBoxProps={{
text: "Alphanumberic short-form of the organization name. It's used as the prefix for host names.",
}}
id="striker-init-general-organization-prefix"
inputProps={{
endAdornment: (
<SuggestButton onClick={handleOrganizationPrefixSuggest} />
),
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}
/>
</FlexBox>
<FlexBox>
<OutlinedInputWithLabel
helpMessageBoxProps={{
text: "Domain name for this striker. It's also the default domain used when creating new install manifests.",
}}
id="striker-init-general-domain-name"
inputProps={{
onBlur: populateHostNameInputOnBlur,
sx: {
minWidth: { sm: '16em' },
width: { xs: '100%', sm: '50%' },
},
}}
label="Domain name"
onChange={handleDomainNameInputOnChange}
value={domainNameInput}
/>
<OutlinedInputWithLabel
helpMessageBoxProps={{
text: "Number or count of this striker; this should be '1' for the first striker, '2' for the second striker, and such.",
}}
id="striker-init-general-host-number"
inputProps={{
inputProps: { maxLength: MAX_HOST_NUMBER_LENGTH },
onBlur: populateHostNameInputOnBlur,
sx: {
width: '5em',
},
}}
label="Host #"
onChange={handleHostNumberInputOnChange}
value={hostNumberInput}
/>
<OutlinedInputWithLabel
helpMessageBoxProps={{
text: "Host name for this striker. It's usually a good idea to use the auto-generated value.",
}}
id="striker-init-general-host-name"
inputProps={{
endAdornment: <SuggestButton onClick={handlerHostNameSuggest} />,
inputProps: {
style: {
minWidth: '4em',
},
},
sx: {
minWidth: 'min-content',
},
}}
label="Host name"
onChange={handleHostNameInputOnChange}
value={hostNameInput}
/>
</FlexBox>
</MUIBox>
);
});
GeneralInitForm.displayName = 'GeneralInitForm';
export default GeneralInitForm;

@ -1,4 +1,3 @@
import { FC, useEffect, useState } from 'react';
import { import {
Box as MUIBox, Box as MUIBox,
BoxProps as MUIBoxProps, BoxProps as MUIBoxProps,
@ -16,6 +15,7 @@ import {
DataGridProps as MUIDataGridProps, DataGridProps as MUIDataGridProps,
gridClasses as muiGridClasses, gridClasses as muiGridClasses,
} from '@mui/x-data-grid'; } from '@mui/x-data-grid';
import { FC, useEffect, useState } from 'react';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import API_BASE_URL from '../lib/consts/API_BASE_URL'; 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 { BodyText, DataGridCellText } from './Text';
import IconButton from './IconButton'; import IconButton from './IconButton';
export type NetworkInterfaceInputMap = Record< type NetworkInput = {
string,
{
isApplied?: boolean;
}
>;
export type NetworkInput = {
inputUUID: string; inputUUID: string;
interfaces: (NetworkInterfaceOverviewMetadata | undefined)[]; interfaces: (NetworkInterfaceOverviewMetadata | undefined)[];
ipAddress: string; ipAddress: string;
@ -49,6 +42,13 @@ export type NetworkInput = {
type: string; type: string;
}; };
type NetworkInterfaceInputMap = Record<
string,
{
isApplied?: boolean;
}
>;
const MOCK_NICS: NetworkInterfaceOverviewMetadata[] = [ const MOCK_NICS: NetworkInterfaceOverviewMetadata[] = [
{ {
networkInterfaceUUID: 'fe299134-c8fe-47bd-ab7a-3aa95eada1f6', networkInterfaceUUID: 'fe299134-c8fe-47bd-ab7a-3aa95eada1f6',
@ -617,4 +617,6 @@ const NetworkInitForm: FC = () => {
); );
}; };
export type { NetworkInput, NetworkInterfaceInputMap };
export default NetworkInitForm; export default NetworkInitForm;

@ -1,356 +1,45 @@
import { Dispatch, FC, SetStateAction, useState } from 'react'; import { FC, useRef, useState } from 'react';
import { Box as MUIBox } from '@mui/material';
import ContainedButton, { ContainedButtonProps } from './ContainedButton'; import ContainedButton from './ContainedButton';
import FlexBox from './FlexBox'; import FlexBox from './FlexBox';
import GeneralInitForm from './GeneralInitForm';
import NetworkInitForm from './NetworkInitForm'; import NetworkInitForm from './NetworkInitForm';
import { OutlinedInputProps } from './OutlinedInput';
import OutlinedInputWithLabel from './OutlinedInputWithLabel';
import pad from '../lib/pad';
import { Panel, PanelHeader } from './Panels'; import { Panel, PanelHeader } from './Panels';
import { HeaderText } from './Text'; import { BodyText, HeaderText } from './Text';
const MAX_ORGANIZATION_PREFIX_LENGTH = 5; const StrikerInitForm: FC = () => {
const MIN_ORGANIZATION_PREFIX_LENGTH = 2; const [requestBody, setRequestBody] = useState<
const MAX_HOST_NUMBER_LENGTH = 2; Record<string, unknown> | undefined
>();
const MAP_TO_ORGANIZATION_PREFIX_BUILDER: Record< const generalInitFormRef = useRef();
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<SetStateAction<MapToType[TypeName]>>;
};
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<OutlinedInputProps['onChange'], undefined>
>;
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 =
<TypeName extends keyof MapToType>({
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 = <TypeName extends keyof MapToType>(
values: Array<MapToType[TypeName]>,
{ 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<typeof fn>
) => (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<ContainedButtonProps> = ({ onClick, ...restProps }) =>
onClick ? (
<ContainedButton {...{ onClick, tabIndex: -1, ...restProps }}>
Suggest
</ContainedButton>
) : (
<></>
);
const StrikerInitGeneralForm: FC = () => {
const [organizationNameInput, setOrganizationNameInput] =
useState<string>('');
const [organizationPrefixInput, setOrganizationPrefixInput] =
useState<string>('');
const [
isOrganizationPrefixInputUserChanged,
setIsOrganizationPrefixInputUserChanged,
] = useState<boolean>(false);
const [domainNameInput, setDomainNameInput] = useState<string>('');
const [hostNumberInput, setHostNumberInput] = useState<number>(0);
const [hostNameInput, setHostNameInput] = useState<string>('');
const [isHostNameInputUserChanged, setIsHostNameInputUserChanged] =
useState<boolean>(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,
);
return ( return (
<MUIBox
sx={{
display: 'flex',
flexDirection: { xs: 'column', sm: 'row' },
'& > *': {
flexBasis: '50%',
},
'& > :not(:first-child)': {
marginLeft: { xs: 0, sm: '1em' },
marginTop: { xs: '1em', sm: 0 },
},
}}
>
<FlexBox>
<OutlinedInputWithLabel
helpMessageBoxProps={{
text: 'Name of the organization that maintains this Anvil! system. You can enter anything that makes sense to you.',
}}
id="striker-init-general-organization-name"
inputProps={{
onBlur: populateOrganizationPrefixInputOnBlur,
}}
label="Organization name"
onChange={handleOrganizationNameInputOnChange}
value={organizationNameInput}
/>
<OutlinedInputWithLabel
helpMessageBoxProps={{
text: "Alphanumberic short-form of the organization name. It's used as the prefix for host names.",
}}
id="striker-init-general-organization-prefix"
inputProps={{
endAdornment: (
<SuggestButton onClick={handleOrganizationPrefixSuggest} />
),
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}
/>
</FlexBox>
<FlexBox>
<OutlinedInputWithLabel
helpMessageBoxProps={{
text: "Domain name for this striker. It's also the default domain used when creating new install manifests.",
}}
id="striker-init-general-domain-name"
inputProps={{
onBlur: populateHostNameInputOnBlur,
sx: {
minWidth: { sm: '16em' },
width: { xs: '100%', sm: '50%' },
},
}}
label="Domain name"
onChange={handleDomainNameInputOnChange}
value={domainNameInput}
/>
<OutlinedInputWithLabel
helpMessageBoxProps={{
text: "Number or count of this striker; this should be '1' for the first striker, '2' for the second striker, and such.",
}}
id="striker-init-general-host-number"
inputProps={{
inputProps: { maxLength: MAX_HOST_NUMBER_LENGTH },
onBlur: populateHostNameInputOnBlur,
sx: {
width: '5em',
},
}}
label="Host #"
onChange={handleHostNumberInputOnChange}
value={hostNumberInput}
/>
<OutlinedInputWithLabel
helpMessageBoxProps={{
text: "Host name for this striker. It's usually a good idea to use the auto-generated value.",
}}
id="striker-init-general-host-name"
inputProps={{
endAdornment: <SuggestButton onClick={handlerHostNameSuggest} />,
inputProps: {
style: {
minWidth: '4em',
},
},
sx: {
minWidth: 'min-content',
},
}}
label="Host name"
onChange={handleHostNameInputOnChange}
value={hostNameInput}
/>
</FlexBox>
</MUIBox>
);
};
const StrikerInitForm: FC = () => (
<Panel> <Panel>
<PanelHeader> <PanelHeader>
<HeaderText text="Initialize striker" /> <HeaderText text="Initialize striker" />
</PanelHeader> </PanelHeader>
<FlexBox> <FlexBox>
<StrikerInitGeneralForm /> <GeneralInitForm ref={generalInitFormRef} />
<NetworkInitForm /> <NetworkInitForm />
<FlexBox row sx={{ flexDirection: 'row-reverse' }}>
<ContainedButton
onClick={() => {
setRequestBody(generalInitFormRef.current);
}}
>
Initialize
</ContainedButton>
</FlexBox>
{requestBody && (
<BodyText
sx={{ fontSize: '.8em' }}
text={JSON.stringify(requestBody, null, 2)}
/>
)}
</FlexBox> </FlexBox>
</Panel> </Panel>
); );
};
export default StrikerInitForm; export default StrikerInitForm;

@ -0,0 +1,14 @@
import { FC } from 'react';
import ContainedButton, { ContainedButtonProps } from './ContainedButton';
const SuggestButton: FC<ContainedButtonProps> = ({ onClick, ...restProps }) =>
onClick ? (
<ContainedButton {...{ onClick, tabIndex: -1, ...restProps }}>
Suggest
</ContainedButton>
) : (
<></>
);
export default SuggestButton;
Loading…
Cancel
Save