fix(striker-ui): add input tests to GeneralInitForm

main
Tsu-ba-me 2 years ago
parent 807af8d7b0
commit 4175759b56
  1. 339
      striker-ui/components/GeneralInitForm.tsx

@ -4,28 +4,34 @@ import {
ReactNode, ReactNode,
useCallback, useCallback,
useImperativeHandle, useImperativeHandle,
useMemo,
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { v4 as uuidv4 } from 'uuid';
import INPUT_TYPES from '../lib/consts/INPUT_TYPES';
import FlexBox from './FlexBox'; import FlexBox from './FlexBox';
import InputWithRef, { InputForwardedRefContent } from './InputWithRef'; import InputWithRef, { InputForwardedRefContent } from './InputWithRef';
import isEmpty from '../lib/isEmpty'; import isEmpty from '../lib/isEmpty';
import MessageBox from './MessageBox'; import MessageBox, { Message } from './MessageBox';
import OutlinedInputWithLabel, { import OutlinedInputWithLabel, {
OutlinedInputWithLabelProps, OutlinedInputWithLabelProps,
} from './OutlinedInputWithLabel'; } from './OutlinedInputWithLabel';
import pad from '../lib/pad'; import pad from '../lib/pad';
import SuggestButton from './SuggestButton'; import SuggestButton from './SuggestButton';
import { testInput, testLength, testNotBlank } from '../lib/test_input';
import { InputTestBatches } from '../types/TestInputFunction';
type GeneralInitFormForwardRefContent = { type GeneralInitFormForwardRefContent = {
get?: () => { get?: () => {
adminPassword?: string; adminPassword?: string;
organizationName?: string;
organizationPrefix?: string;
domainName?: string; domainName?: string;
hostNumber?: number;
hostName?: string; hostName?: string;
hostNumber?: number;
organizationName?: string;
organizationPrefix?: string;
}; };
}; };
@ -35,8 +41,10 @@ type OutlinedInputWithLabelOnBlur = Exclude<
>['onBlur']; >['onBlur'];
const MAX_ORGANIZATION_PREFIX_LENGTH = 5; const MAX_ORGANIZATION_PREFIX_LENGTH = 5;
const MIN_ORGANIZATION_PREFIX_LENGTH = 2; const MIN_ORGANIZATION_PREFIX_LENGTH = 1;
const MAX_HOST_NUMBER_LENGTH = 2; const MAX_HOST_NUMBER_LENGTH = 2;
const INPUT_COUNT = 7;
const REP_DN_CHAR = /^[a-z0-9-.]+$/;
const MAP_TO_ORGANIZATION_PREFIX_BUILDER: Record< const MAP_TO_ORGANIZATION_PREFIX_BUILDER: Record<
number, number,
@ -49,6 +57,13 @@ const MAP_TO_ORGANIZATION_PREFIX_BUILDER: Record<
words.map((word) => word.substring(0, 1).toLocaleLowerCase()).join(''), words.map((word) => word.substring(0, 1).toLocaleLowerCase()).join(''),
}; };
const INPUT_TEST_MESSAGE_KEYS: string[] = Array.from(
{
length: INPUT_COUNT,
},
() => uuidv4(),
);
const buildOrganizationPrefix = (organizationName = '') => { const buildOrganizationPrefix = (organizationName = '') => {
const words: string[] = organizationName const words: string[] = organizationName
.split(/\s/) .split(/\s/)
@ -75,13 +90,18 @@ const buildHostName = ({
const GeneralInitForm = forwardRef<GeneralInitFormForwardRefContent>( const GeneralInitForm = forwardRef<GeneralInitFormForwardRefContent>(
(generalInitFormProps, ref) => { (generalInitFormProps, ref) => {
const [helpMessage, setHelpText] = useState<ReactNode | undefined>(); const [helpMessage, setHelpMessage] = useState<ReactNode | undefined>();
const [inputMessages, setInputMessages] = useState<
Array<Message | undefined>
>([]);
const [ const [
isShowOrganizationPrefixSuggest, isShowOrganizationPrefixSuggest,
setIsShowOrganizationPrefixSuggest, setIsShowOrganizationPrefixSuggest,
] = useState<boolean>(false); ] = useState<boolean>(false);
const [isShowHostNameSuggest, setIsShowHostNameSuggest] = const [isShowHostNameSuggest, setIsShowHostNameSuggest] =
useState<boolean>(false); useState<boolean>(false);
const [isConfirmAdminPassword, setIsConfirmAdminPassword] =
useState<boolean>(true);
const adminPasswordInputRef = useRef<InputForwardedRefContent<'string'>>( const adminPasswordInputRef = useRef<InputForwardedRefContent<'string'>>(
{}, {},
@ -99,6 +119,58 @@ const GeneralInitForm = forwardRef<GeneralInitFormForwardRefContent>(
const hostNumberInputRef = useRef<InputForwardedRefContent<'number'>>({}); const hostNumberInputRef = useRef<InputForwardedRefContent<'number'>>({});
const hostNameInputRef = useRef<InputForwardedRefContent<'string'>>({}); const hostNameInputRef = useRef<InputForwardedRefContent<'string'>>({});
const setInputMessage = useCallback((index: number, message?: Message) => {
setInputMessages((previous) => {
previous.splice(index, 1, message);
return [...previous];
});
}, []);
const setOrganizationPrefixInputMessage = useCallback(
(message?: Message) => setInputMessage(1, message),
[setInputMessage],
);
const setHostNumberInputMessage = useCallback(
(message?: Message) => setInputMessage(2, message),
[setInputMessage],
);
const setDomainNameInputMessage = useCallback(
(message?: Message) => setInputMessage(3, message),
[setInputMessage],
);
const setHostNameInputMessage = useCallback(
(message?: Message) => setInputMessage(4, message),
[setInputMessage],
);
const setAdminPasswordInputMessage = useCallback(
(message?: Message) => setInputMessage(5, message),
[setInputMessage],
);
const setConfirmAdminPasswordInputMessage = useCallback(
(message?: Message) => setInputMessage(6, message),
[setInputMessage],
);
const createInputTestMessages = useCallback(
() =>
inputMessages.map((message, index) => {
let messageElement;
if (message) {
const { children, type = 'warning' } = message;
messageElement = (
<MessageBox
key={`input-test-message-${INPUT_TEST_MESSAGE_KEYS[index]}`}
type={type}
>
{children}
</MessageBox>
);
}
return messageElement;
}),
[inputMessages],
);
const populateOrganizationPrefixInput = useCallback( const populateOrganizationPrefixInput = useCallback(
({ ({
organizationName = organizationNameInputRef.current.getValue?.call( organizationName = organizationNameInputRef.current.getValue?.call(
@ -191,6 +263,136 @@ const GeneralInitForm = forwardRef<GeneralInitFormForwardRefContent>(
[], [],
); );
const inputTests: InputTestBatches = useMemo(
() => ({
adminPassword: {
defaults: {
onSuccess: () => {
setAdminPasswordInputMessage(undefined);
},
},
tests: [
{
onFailure: () => {
setAdminPasswordInputMessage({
children:
'Admin password cannot contain single-quote, double-quote, slash, backslash, angle brackets, and curly brackets.',
});
},
test: ({ value }) => !/['"/\\><}{]/g.test(value as string),
},
],
},
confirmAdminPassword: {
defaults: {
onSuccess: () => {
setConfirmAdminPasswordInputMessage(undefined);
},
},
tests: [
{
onFailure: () => {
setConfirmAdminPasswordInputMessage({
children: 'Admin password confirmation failed.',
});
},
test: ({ value, compare }) => value === compare,
},
],
},
domainName: {
defaults: {
onSuccess: () => {
setDomainNameInputMessage(undefined);
},
},
tests: [
{
onFailure: () => {
setDomainNameInputMessage({
children:
'Domain name can only contain lowercase alphanumeric, hyphen, and decimal characters.',
});
},
test: ({ value }) => REP_DN_CHAR.test(value as string),
},
],
},
hostName: {
defaults: {
onSuccess: () => {
setHostNameInputMessage(undefined);
},
},
tests: [
{
onFailure: () => {
setHostNameInputMessage({
children:
'Host name can only contain lowercase alphanumeric, hyphen, and decimal characters.',
});
},
test: ({ value }) => REP_DN_CHAR.test(value as string),
},
],
},
hostNumber: {
defaults: {
onSuccess: () => {
setHostNumberInputMessage(undefined);
},
},
tests: [
{
onFailure: () => {
setHostNumberInputMessage({
children: 'Host number can only contain digits.',
});
},
test: ({ value }) => /^\d+$/.test(value as string),
},
],
},
organizationName: {
tests: [{ test: testNotBlank }],
},
organizationPrefix: {
defaults: {
onSuccess: () => {
setOrganizationPrefixInputMessage(undefined);
},
},
tests: [
{
onFailure: ({ max, min }) => {
setOrganizationPrefixInputMessage({
children: `Organization prefix must be ${min} to ${max} characters.`,
});
},
test: testLength,
},
{
onFailure: () => {
setOrganizationPrefixInputMessage({
children:
'Organization prefix can only contain lowercase alphanumeric characters.',
});
},
test: ({ value }) => /^[a-z0-9]+$/.test(value as string),
},
],
},
}),
[
setAdminPasswordInputMessage,
setConfirmAdminPasswordInputMessage,
setDomainNameInputMessage,
setHostNameInputMessage,
setHostNumberInputMessage,
setOrganizationPrefixInputMessage,
],
);
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
get: () => ({ get: () => ({
adminPassword: adminPasswordInputRef.current.getValue?.call(null), adminPassword: adminPasswordInputRef.current.getValue?.call(null),
@ -215,9 +417,16 @@ const GeneralInitForm = forwardRef<GeneralInitFormForwardRefContent>(
inputProps={{ inputProps={{
onBlur: populateOrganizationPrefixInputOnBlur, onBlur: populateOrganizationPrefixInputOnBlur,
}} }}
inputLabelProps={{ isNotifyRequired: true }}
label="Organization name" label="Organization name"
onChange={({ target: { value } }) => {
testInput({
inputs: { organizationName: { value } },
tests: inputTests,
});
}}
onHelp={() => { onHelp={() => {
setHelpText( setHelpMessage(
buildHelpMessage( buildHelpMessage(
'Name of the organization that maintains this Anvil! system. You can enter anything that makes sense to you.', 'Name of the organization that maintains this Anvil! system. You can enter anything that makes sense to you.',
), ),
@ -254,14 +463,26 @@ const GeneralInitForm = forwardRef<GeneralInitFormForwardRefContent>(
}, },
onBlur: populateHostNameInputOnBlur, onBlur: populateHostNameInputOnBlur,
}} }}
inputLabelProps={{ isNotifyRequired: true }}
label="Prefix" label="Prefix"
onChange={() => { onChange={({ target: { value } }) => {
testInput({
inputs: {
organizationPrefix: {
max: MAX_ORGANIZATION_PREFIX_LENGTH,
min: MIN_ORGANIZATION_PREFIX_LENGTH,
value,
},
},
tests: inputTests,
});
setIsShowOrganizationPrefixSuggest( setIsShowOrganizationPrefixSuggest(
isOrganizationPrefixPrereqFilled(), isOrganizationPrefixPrereqFilled(),
); );
}} }}
onHelp={() => { onHelp={() => {
setHelpText( setHelpMessage(
buildHelpMessage( buildHelpMessage(
"Alphanumberic short-form of the organization name. It's used as the prefix for host names.", "Alphanumberic short-form of the organization name. It's used as the prefix for host names.",
), ),
@ -284,9 +505,16 @@ const GeneralInitForm = forwardRef<GeneralInitFormForwardRefContent>(
}, },
onBlur: populateHostNameInputOnBlur, onBlur: populateHostNameInputOnBlur,
}} }}
label="Host #" inputLabelProps={{ isNotifyRequired: true }}
label="Striker #"
onChange={({ target: { value } }) => {
testInput({
inputs: { hostNumber: { value } },
tests: inputTests,
});
}}
onHelp={() => { onHelp={() => {
setHelpText( setHelpMessage(
buildHelpMessage( buildHelpMessage(
"Number or count of this striker; this should be '1' for the first striker, '2' for the second striker, and such.", "Number or count of this striker; this should be '1' for the first striker, '2' for the second striker, and such.",
), ),
@ -309,9 +537,16 @@ const GeneralInitForm = forwardRef<GeneralInitFormForwardRefContent>(
inputProps={{ inputProps={{
onBlur: populateHostNameInputOnBlur, onBlur: populateHostNameInputOnBlur,
}} }}
inputLabelProps={{ isNotifyRequired: true }}
label="Domain name" label="Domain name"
onChange={({ target: { value } }) => {
testInput({
inputs: { domainName: { value } },
tests: inputTests,
});
}}
onHelp={() => { onHelp={() => {
setHelpText( setHelpMessage(
buildHelpMessage( buildHelpMessage(
"Domain name for this striker. It's also the default domain used when creating new install manifests.", "Domain name for this striker. It's also the default domain used when creating new install manifests.",
), ),
@ -334,12 +569,18 @@ const GeneralInitForm = forwardRef<GeneralInitFormForwardRefContent>(
/> />
), ),
}} }}
inputLabelProps={{ isNotifyRequired: true }}
label="Host name" label="Host name"
onChange={() => { onChange={({ target: { value } }) => {
testInput({
inputs: { hostName: { value } },
tests: inputTests,
});
setIsShowHostNameSuggest(isHostNamePrereqFilled()); setIsShowHostNameSuggest(isHostNamePrereqFilled());
}} }}
onHelp={() => { onHelp={() => {
setHelpText( setHelpMessage(
buildHelpMessage( buildHelpMessage(
"Host name for this striker. It's usually a good idea to use the auto-generated value.", "Host name for this striker. It's usually a good idea to use the auto-generated value.",
), ),
@ -369,12 +610,24 @@ const GeneralInitForm = forwardRef<GeneralInitFormForwardRefContent>(
id="striker-init-general-admin-password" id="striker-init-general-admin-password"
inputProps={{ inputProps={{
inputProps: { inputProps: {
type: 'password', type: INPUT_TYPES.password,
},
onPasswordVisibilityAppend: (inputType) => {
setIsConfirmAdminPassword(
inputType === INPUT_TYPES.password,
);
}, },
}} }}
inputLabelProps={{ isNotifyRequired: true }}
label="Admin password" label="Admin password"
onChange={({ target: { value } }) => {
testInput({
inputs: { adminPassword: { value } },
tests: inputTests,
});
}}
onHelp={() => { onHelp={() => {
setHelpText( setHelpMessage(
buildHelpMessage( buildHelpMessage(
"Password use to login to this Striker and connect to its database. Don't provide an used password here because it'll be stored as plaintext.", "Password use to login to this Striker and connect to its database. Don't provide an used password here because it'll be stored as plaintext.",
), ),
@ -385,29 +638,49 @@ const GeneralInitForm = forwardRef<GeneralInitFormForwardRefContent>(
ref={adminPasswordInputRef} ref={adminPasswordInputRef}
/> />
</MUIGrid> </MUIGrid>
<MUIGrid item xs={1}> {isConfirmAdminPassword && (
<InputWithRef <MUIGrid item xs={1}>
input={ <InputWithRef
<OutlinedInputWithLabel input={
id="striker-init-general-confirm-admin-password" <OutlinedInputWithLabel
inputProps={{ id="striker-init-general-confirm-admin-password"
inputProps: { inputProps={{
type: 'password', inputProps: {
}, type: INPUT_TYPES.password,
}} },
label="Confirm password" }}
/> inputLabelProps={{
} isNotifyRequired: isConfirmAdminPassword,
ref={confirmAdminPasswordInputRef} }}
/> label="Confirm password"
</MUIGrid> onChange={({ target: { value } }) => {
testInput({
inputs: {
confirmAdminPassword: {
value,
compare:
adminPasswordInputRef.current.getValue?.call(
null,
),
},
},
tests: inputTests,
});
}}
/>
}
ref={confirmAdminPasswordInputRef}
/>
</MUIGrid>
)}
</MUIGrid> </MUIGrid>
</MUIGrid> </MUIGrid>
</MUIGrid> </MUIGrid>
{createInputTestMessages()}
{helpMessage && ( {helpMessage && (
<MessageBox <MessageBox
onClose={() => { onClose={() => {
setHelpText(undefined); setHelpMessage(undefined);
}} }}
> >
{helpMessage} {helpMessage}

Loading…
Cancel
Save