diff --git a/striker-ui/components/ProvisionServerDialog.tsx b/striker-ui/components/ProvisionServerDialog.tsx index 38b12869..47183fec 100644 --- a/striker-ui/components/ProvisionServerDialog.tsx +++ b/striker-ui/components/ProvisionServerDialog.tsx @@ -32,7 +32,17 @@ import OutlinedInputWithLabel, { import { Panel, PanelHeader } from './Panels'; import Select, { SelectProps } from './Select'; import Slider, { SliderProps } from './Slider'; +import { + testInput as baseTestInput, + testMax, + testNotBlank, + testRange, +} from '../lib/test_input'; import { BodyText, HeaderText } from './Text'; +import { + InputTestBatches, + TestInputFunction, +} from '../types/TestInputFunction'; type InputMessage = Partial>; @@ -172,26 +182,6 @@ type UpdateLimitsFunction = (options?: { virtualDisks?: VirtualDiskStates; }) => Partial>; -type TestArgs = { - max: bigint | number; - min: bigint | number; - value: bigint | number | string; -}; - -type InputTest = { - onFailure?: (args: TestArgs) => void; - onSuccess?: () => void; - test: (args: TestArgs) => boolean; -}; - -type TestInputFunction = (options?: { - inputs?: { - [id: string]: Partial; - }; - isContinueOnFailure?: boolean; - isIgnoreOnCallbacks?: boolean; -}) => boolean; - const MOCK_DATA = { anvils: [ { @@ -1102,6 +1092,11 @@ const ProvisionServerDialog = ({ SelectItem[] >([]); + const [inputServerNameValue, setInputServerNameValue] = useState(''); + const [inputServerNameMessage, setInputServerNameMessage] = useState< + InputMessage | undefined + >(); + const [inputCPUCoresValue, setInputCPUCoresValue] = useState(1); const [inputCPUCoresMax, setInputCPUCoresMax] = useState(0); const [inputCPUCoresMessage, setInputCPUCoresMessage] = useState< @@ -1141,6 +1136,184 @@ const ProvisionServerDialog = ({ string[] >([]); + const inputTests: InputTestBatches = { + serverName: { + defaults: { + max: 0, + min: 0, + onSuccess: () => { + setInputServerNameMessage(undefined); + }, + value: inputServerNameValue, + }, + tests: [ + { + onFailure: () => { + setInputServerNameMessage({ + text: 'The server name length must be 1 to 16 characters.', + type: 'error', + }); + }, + test: ({ value }) => { + const { length } = value as string; + + return length >= 1 && length <= 16; + }, + }, + { + onFailure: () => { + setInputServerNameMessage({ + text: 'The server name is expected to only contain alphanumeric, hyphen, or underscore characters.', + type: 'error', + }); + }, + test: ({ value }) => /^[a-zA-Z0-9_-]+$/.test(value as string), + }, + ], + }, + cpuCores: { + defaults: { + max: inputCPUCoresMax, + min: inputCPUCoresMin, + onSuccess: () => { + setInputCPUCoresMessage(undefined); + }, + value: inputCPUCoresValue, + }, + tests: [ + { + onFailure: () => { + setInputCPUCoresMessage({ + text: 'Non available.', + type: 'error', + }); + }, + test: testMax, + }, + { + onFailure: ({ max, min }) => { + setInputCPUCoresMessage({ + text: `The number of CPU cores is expected to be between ${min} and ${max}.`, + type: 'error', + }); + }, + test: testRange, + }, + ], + }, + memory: { + defaults: { + max: memoryMax, + min: 1, + onSuccess: () => { + setInputMemoryMessage(undefined); + }, + value: memory, + }, + tests: [ + { + onFailure: () => { + setInputMemoryMessage({ text: 'Non available.', type: 'error' }); + }, + test: testMax, + }, + { + onFailure: ({ max, min }) => { + setInputMemoryMessage({ + text: `Memory is expected to be between ${min} B and ${max} B.`, + type: 'error', + }); + }, + test: testRange, + }, + ], + }, + installISO: { + defaults: { + max: 0, + min: 0, + onSuccess: () => { + setInputInstallISOMessage(undefined); + }, + value: inputInstallISOFileUUID, + }, + tests: [ + { + test: testNotBlank, + }, + ], + }, + anvil: { + defaults: { + max: 0, + min: 0, + onSuccess: () => { + setInputAnvilMessage(undefined); + }, + value: inputAnvilValue, + }, + tests: [ + { + test: testNotBlank, + }, + ], + }, + }; + virtualDisks.inputSizeMessages.forEach((message, vdIndex) => { + inputTests[`vd${vdIndex}Size`] = { + defaults: { + max: virtualDisks.maxes[vdIndex], + min: 1, + onSuccess: () => { + virtualDisks.inputSizeMessages[vdIndex] = undefined; + }, + value: virtualDisks.sizes[vdIndex], + }, + onFinishBatch: () => { + setVirtualDisks({ ...virtualDisks }); + }, + tests: [ + { + onFailure: () => { + virtualDisks.inputSizeMessages[vdIndex] = { + text: 'Non available.', + type: 'error', + }; + }, + test: testMax, + }, + { + onFailure: ({ max, min }) => { + virtualDisks.inputSizeMessages[vdIndex] = { + text: `Virtual disk ${vdIndex} size is expected to be between ${min} B and ${max} B.`, + type: 'error', + }; + }, + test: testRange, + }, + ], + }; + + inputTests[`vd${vdIndex}StorageGroup`] = { + defaults: { + max: 0, + min: 0, + onSuccess: () => { + virtualDisks.inputStorageGroupUUIDMessages[vdIndex] = undefined; + }, + value: virtualDisks.inputStorageGroupUUIDs[vdIndex], + }, + onFinishBatch: () => { + setVirtualDisks({ ...virtualDisks }); + }, + tests: [ + { + test: testNotBlank, + }, + ], + }; + }); + const updateLimits: UpdateLimitsFunction = ({ allAnvils: ulAllAnvils = allAnvils, cpuCores: ulCPUCores = inputCPUCoresValue, @@ -1215,246 +1388,9 @@ const ProvisionServerDialog = ({ // eslint-disable-next-line react-hooks/exhaustive-deps const initLimits = useCallback(updateLimits, []); - const testInput: TestInputFunction = ({ - inputs, - isContinueOnFailure, - isIgnoreOnCallbacks, - } = {}): boolean => { - const testNotBlank = ({ value }: Pick) => value !== ''; - const testMax = ({ max, min }: Pick) => max >= min; - const testRange = ({ max, min, value }: TestArgs) => - value >= min && value <= max; - - const tests: { - [id: string]: { - defaults: TestArgs & { - onSuccess: () => void; - }; - onFinishBatch?: () => void; - optionalTests?: Array; - tests: Array; - }; - } = { - cpuCores: { - defaults: { - max: inputCPUCoresMax, - min: inputCPUCoresMin, - onSuccess: () => { - setInputCPUCoresMessage(undefined); - }, - value: inputCPUCoresValue, - }, - tests: [ - { - onFailure: () => { - setInputCPUCoresMessage({ - text: 'Non available.', - type: 'error', - }); - }, - test: testMax, - }, - { - onFailure: ({ max, min }) => { - setInputCPUCoresMessage({ - text: `The number of CPU cores is expected to be between ${min} and ${max}.`, - type: 'error', - }); - }, - test: testRange, - }, - ], - }, - memory: { - defaults: { - max: memoryMax, - min: 1, - onSuccess: () => { - setInputMemoryMessage(undefined); - }, - value: memory, - }, - tests: [ - { - onFailure: () => { - setInputMemoryMessage({ text: 'Non available.', type: 'error' }); - }, - test: testMax, - }, - { - onFailure: ({ max, min }) => { - setInputMemoryMessage({ - text: `Memory is expected to be between ${min} B and ${max} B.`, - type: 'error', - }); - }, - test: testRange, - }, - ], - }, - installISO: { - defaults: { - max: 0, - min: 0, - onSuccess: () => { - setInputInstallISOMessage(undefined); - }, - value: inputInstallISOFileUUID, - }, - tests: [ - { - test: testNotBlank, - }, - ], - }, - anvil: { - defaults: { - max: 0, - min: 0, - onSuccess: () => { - setInputAnvilMessage(undefined); - }, - value: inputAnvilValue, - }, - tests: [ - { - test: testNotBlank, - }, - ], - }, - }; - - virtualDisks.inputSizeMessages.forEach((error, vdIndex) => { - tests[`vd${vdIndex}Size`] = { - defaults: { - max: virtualDisks.maxes[vdIndex], - min: 1, - onSuccess: () => { - virtualDisks.inputSizeMessages[vdIndex] = undefined; - }, - value: virtualDisks.sizes[vdIndex], - }, - onFinishBatch: () => { - setVirtualDisks({ ...virtualDisks }); - }, - tests: [ - { - onFailure: () => { - virtualDisks.inputSizeMessages[vdIndex] = { - text: 'Non available.', - type: 'error', - }; - }, - test: testMax, - }, - { - onFailure: ({ max, min }) => { - virtualDisks.inputSizeMessages[vdIndex] = { - text: `Virtual disk ${vdIndex} size is expected to be between ${min} B and ${max} B.`, - type: 'error', - }; - }, - test: testRange, - }, - ], - }; - - tests[`vd${vdIndex}StorageGroup`] = { - defaults: { - max: 0, - min: 0, - onSuccess: () => { - virtualDisks.inputStorageGroupUUIDMessages[vdIndex] = undefined; - }, - value: virtualDisks.inputStorageGroupUUIDs[vdIndex], - }, - onFinishBatch: () => { - setVirtualDisks({ ...virtualDisks }); - }, - tests: [ - { - test: testNotBlank, - }, - ], - }; - }); - - const testsToRun = - inputs ?? - Object.keys(tests).reduce< - Exclude< - Exclude[0], undefined>['inputs'], - undefined - > - >((reduceContainer, id: string) => { - reduceContainer[id] = {}; - return reduceContainer; - }, {}); - - let allResult = true; - - Object.keys(testsToRun).every((id: string) => { - const { - defaults: { - max: dMax, - min: dMin, - onSuccess: dOnSuccess, - value: dValue, - }, - onFinishBatch: dOnFinishBatch, - optionalTests, - tests: requiredTests, - } = tests[id]; - const { max = dMax, min = dMin, value = dValue } = testsToRun[id]; - - let cbFinishBatch; - let setOnResult: (test?: Partial) => { - cbFailure: InputTest['onFailure']; - cbSuccess: InputTest['onSuccess']; - } = () => ({ cbFailure: undefined, cbSuccess: undefined }); - - if (!isIgnoreOnCallbacks) { - cbFinishBatch = dOnFinishBatch; - - setOnResult = ({ onFailure, onSuccess }: Partial = {}) => ({ - cbFailure: onFailure, - cbSuccess: onSuccess, - }); - } - - const runTest: (test: InputTest) => boolean = ({ - onFailure, - onSuccess = dOnSuccess, - test, - }) => { - const args = { max, min, value }; - const singleResult: boolean = test(args); - - const { cbFailure, cbSuccess } = setOnResult({ onFailure, onSuccess }); - - if (singleResult) { - cbSuccess?.call(null); - } else { - allResult = singleResult; - - cbFailure?.call(null, args); - } - - return singleResult; - }; - - // Don't need to pass optional tests for input to be valid. - optionalTests?.forEach(runTest); - - const requiredTestsResult = requiredTests.every(runTest); - - cbFinishBatch?.call(null); - - return requiredTestsResult || isContinueOnFailure; - }); - - return allResult; - }; + const testInput = ( + ...[options, ...restArgs]: Parameters + ) => baseTestInput({ tests: inputTests, ...options }, ...restArgs); const changeMemory = ({ cmValue = BIGINT_ZERO, @@ -1581,7 +1517,21 @@ const ProvisionServerDialog = ({ }, }} > - + + { + setInputServerNameValue(value); + + testInput({ inputs: { serverName: { value } } }); + }, + value: inputServerNameValue, + }} + messageBoxProps={inputServerNameMessage} + /> + {createOutlinedSlider('ps-cpu-cores', 'CPU cores', inputCPUCoresValue, { messageBoxProps: inputCPUCoresMessage, sliderProps: { @@ -1677,7 +1627,7 @@ const ProvisionServerDialog = ({ )} {createOutlinedSelect( 'ps-driver-image', - 'Driver ISO', + 'Driver ISO (optional)', fileSelectItems, { disableItem: (value) => value === inputInstallISOFileUUID, diff --git a/striker-ui/lib/test_input/index.ts b/striker-ui/lib/test_input/index.ts new file mode 100644 index 00000000..092a1d01 --- /dev/null +++ b/striker-ui/lib/test_input/index.ts @@ -0,0 +1,6 @@ +import testInput from './testInput'; +import testMax from './testMax'; +import testNotBlank from './testNotBlank'; +import testRange from './testRange'; + +export { testInput, testMax, testNotBlank, testRange }; diff --git a/striker-ui/lib/test_input/testInput.ts b/striker-ui/lib/test_input/testInput.ts new file mode 100644 index 00000000..1d201f13 --- /dev/null +++ b/striker-ui/lib/test_input/testInput.ts @@ -0,0 +1,97 @@ +import { + InputTest, + TestInputFunction, + TestInputFunctionOptions, +} from '../../types/TestInputFunction'; + +const testInput: TestInputFunction = ({ + inputs, + isContinueOnFailure, + isIgnoreOnCallbacks, + tests = {}, +} = {}): boolean => { + const testsToRun = + inputs ?? + Object.keys(tests).reduce< + Exclude + >((reduceContainer, id: string) => { + reduceContainer[id] = {}; + return reduceContainer; + }, {}); + + let allResult = true; + + let setBatchCallback: ( + batch?: Partial< + Exclude[string] + >, + ) => { + cbFinishBatch: Exclude< + TestInputFunctionOptions['tests'], + undefined + >[string]['onFinishBatch']; + } = () => ({ cbFinishBatch: undefined }); + let setSingleCallback: (test?: Partial) => { + cbFailure: InputTest['onFailure']; + cbSuccess: InputTest['onSuccess']; + } = () => ({ cbFailure: undefined, cbSuccess: undefined }); + + if (!isIgnoreOnCallbacks) { + setBatchCallback = ({ onFinishBatch } = {}) => ({ + cbFinishBatch: onFinishBatch, + }); + setSingleCallback = ({ onFailure, onSuccess } = {}) => ({ + cbFailure: onFailure, + cbSuccess: onSuccess, + }); + } + + Object.keys(testsToRun).every((id: string) => { + const { + defaults: { max: dMax, min: dMin, onSuccess: dOnSuccess, value: dValue }, + onFinishBatch, + optionalTests, + tests: requiredTests, + } = tests[id]; + const { max = dMax, min = dMin, value = dValue } = testsToRun[id]; + + const { cbFinishBatch } = setBatchCallback({ onFinishBatch }); + + const runTest: (test: InputTest) => boolean = ({ + onFailure, + onSuccess = dOnSuccess, + test, + }) => { + const args = { max, min, value }; + const singleResult: boolean = test(args); + + const { cbFailure, cbSuccess } = setSingleCallback({ + onFailure, + onSuccess, + }); + + if (singleResult) { + cbSuccess?.call(null); + } else { + allResult = singleResult; + + cbFailure?.call(null, args); + } + + return singleResult; + }; + + // Don't need to pass optional tests for input to be valid. + optionalTests?.forEach(runTest); + + const requiredTestsResult = requiredTests.every(runTest); + + cbFinishBatch?.call(null); + + return requiredTestsResult || isContinueOnFailure; + }); + + return allResult; +}; + +export default testInput; diff --git a/striker-ui/lib/test_input/testMax.ts b/striker-ui/lib/test_input/testMax.ts new file mode 100644 index 00000000..e86ee2f7 --- /dev/null +++ b/striker-ui/lib/test_input/testMax.ts @@ -0,0 +1,5 @@ +import { InputTestArgs } from '../../types/TestInputFunction'; + +const testMax: (args: InputTestArgs) => boolean = ({ max, min }) => max >= min; + +export default testMax; diff --git a/striker-ui/lib/test_input/testNotBlank.ts b/striker-ui/lib/test_input/testNotBlank.ts new file mode 100644 index 00000000..b7c62026 --- /dev/null +++ b/striker-ui/lib/test_input/testNotBlank.ts @@ -0,0 +1,6 @@ +import { InputTestArgs } from '../../types/TestInputFunction'; + +const testNotBlank: (args: InputTestArgs) => boolean = ({ value }) => + value !== ''; + +export default testNotBlank; diff --git a/striker-ui/lib/test_input/testRange.ts b/striker-ui/lib/test_input/testRange.ts new file mode 100644 index 00000000..9326a318 --- /dev/null +++ b/striker-ui/lib/test_input/testRange.ts @@ -0,0 +1,6 @@ +import { InputTestArgs } from '../../types/TestInputFunction'; + +const testRange: (args: InputTestArgs) => boolean = ({ max, min, value }) => + value >= min && value <= max; + +export default testRange; diff --git a/striker-ui/types/TestInputFunction.ts b/striker-ui/types/TestInputFunction.ts new file mode 100644 index 00000000..5fa15c7a --- /dev/null +++ b/striker-ui/types/TestInputFunction.ts @@ -0,0 +1,33 @@ +export type InputTestArgs = { + max: bigint | number; + min: bigint | number; + value: bigint | number | string; +}; + +export type InputTest = { + onFailure?: (args: InputTestArgs) => void; + onSuccess?: () => void; + test: (args: InputTestArgs) => boolean; +}; + +export type InputTestBatches = { + [id: string]: { + defaults: InputTestArgs & { + onSuccess: () => void; + }; + onFinishBatch?: () => void; + optionalTests?: Array; + tests: Array; + }; +}; + +export type TestInputFunctionOptions = { + inputs?: { + [id: string]: Partial; + }; + isContinueOnFailure?: boolean; + isIgnoreOnCallbacks?: boolean; + tests?: InputTestBatches; +}; + +export type TestInputFunction = (options?: TestInputFunctionOptions) => boolean;