fix(striker-ui): hoist testInput() and add server name validations

main
Tsu-ba-me 3 years ago
parent d5967c1361
commit 599cd59d2f
  1. 312
      striker-ui/components/ProvisionServerDialog.tsx
  2. 6
      striker-ui/lib/test_input/index.ts
  3. 97
      striker-ui/lib/test_input/testInput.ts
  4. 5
      striker-ui/lib/test_input/testMax.ts
  5. 6
      striker-ui/lib/test_input/testNotBlank.ts
  6. 6
      striker-ui/lib/test_input/testRange.ts
  7. 33
      striker-ui/types/TestInputFunction.ts

@ -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<Pick<MessageBoxProps, 'type' | 'text'>>;
@ -172,26 +182,6 @@ type UpdateLimitsFunction = (options?: {
virtualDisks?: VirtualDiskStates;
}) => Partial<ReturnType<FilterAnvilsFunction>>;
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<TestArgs>;
};
isContinueOnFailure?: boolean;
isIgnoreOnCallbacks?: boolean;
}) => boolean;
const MOCK_DATA = {
anvils: [
{
@ -1102,6 +1092,11 @@ const ProvisionServerDialog = ({
SelectItem[]
>([]);
const [inputServerNameValue, setInputServerNameValue] = useState<string>('');
const [inputServerNameMessage, setInputServerNameMessage] = useState<
InputMessage | undefined
>();
const [inputCPUCoresValue, setInputCPUCoresValue] = useState<number>(1);
const [inputCPUCoresMax, setInputCPUCoresMax] = useState<number>(0);
const [inputCPUCoresMessage, setInputCPUCoresMessage] = useState<
@ -1141,100 +1136,41 @@ const ProvisionServerDialog = ({
string[]
>([]);
const updateLimits: UpdateLimitsFunction = ({
allAnvils: ulAllAnvils = allAnvils,
cpuCores: ulCPUCores = inputCPUCoresValue,
fileUUIDs: ulFileUUIDs = [inputInstallISOFileUUID, inputDriverISOFileUUID],
includeAnvilUUIDs: ulIncludeAnvilUUIDs = filterBlanks([inputAnvilValue]),
includeFileUUIDs: ulIncludeFileUUIDs,
includeStorageGroupUUIDs: ulIncludeStorageGroupUUIDs,
inputMemoryUnit: ulInputMemoryUnit = inputMemoryUnit,
memory: ulMemory = memory,
storageGroupUUIDMapToFree:
ulStorageGroupUUIDMapToFree = storageGroupUUIDMapToFree,
virtualDisks: ulVirtualDisks = virtualDisks,
} = {}) => {
const {
anvilUUIDs,
fileUUIDs,
maxCPUCores,
maxMemory,
maxVirtualDiskSizes,
storageGroupUUIDs,
} = filterAnvils(
ulAllAnvils,
ulStorageGroupUUIDMapToFree,
ulCPUCores,
ulMemory,
ulVirtualDisks.sizes,
ulVirtualDisks.inputStorageGroupUUIDs,
ulFileUUIDs,
const inputTests: InputTestBatches = {
serverName: {
defaults: {
max: 0,
min: 0,
onSuccess: () => {
setInputServerNameMessage(undefined);
},
value: inputServerNameValue,
},
tests: [
{
includeAnvilUUIDs: ulIncludeAnvilUUIDs,
includeFileUUIDs: ulIncludeFileUUIDs,
includeStorageGroupUUIDs: ulIncludeStorageGroupUUIDs,
onFailure: () => {
setInputServerNameMessage({
text: 'The server name length must be 1 to 16 characters.',
type: 'error',
});
},
);
test: ({ value }) => {
const { length } = value as string;
setInputCPUCoresMax(maxCPUCores);
setMemoryMax(maxMemory);
ulVirtualDisks.maxes = maxVirtualDiskSizes;
ulVirtualDisks.maxes.forEach((vdMaxSize, vdIndex) => {
dSize(vdMaxSize, {
fromUnit: 'B',
onSuccess: {
string: (value) => {
ulVirtualDisks.inputMaxes[vdIndex] = value;
return length >= 1 && length <= 16;
},
},
toUnit: ulVirtualDisks.inputUnits[vdIndex],
});
{
onFailure: () => {
setInputServerNameMessage({
text: 'The server name is expected to only contain alphanumeric, hyphen, or underscore characters.',
type: 'error',
});
setVirtualDisks({ ...ulVirtualDisks });
setIncludeAnvilUUIDs(anvilUUIDs);
setIncludeFileUUIDs(fileUUIDs);
setIncludeStorageGroupUUIDs(storageGroupUUIDs);
dSize(maxMemory, {
fromUnit: 'B',
onSuccess: {
string: (value) => setInputMemoryMax(value),
},
toUnit: ulInputMemoryUnit,
});
return {
maxCPUCores,
maxMemory,
maxVirtualDiskSizes,
};
};
// The memorized version of updateLimits() should only be called during first render.
// eslint-disable-next-line react-hooks/exhaustive-deps
const initLimits = useCallback(updateLimits, []);
const testInput: TestInputFunction = ({
inputs,
isContinueOnFailure,
isIgnoreOnCallbacks,
} = {}): boolean => {
const testNotBlank = ({ value }: Pick<TestArgs, 'value'>) => value !== '';
const testMax = ({ max, min }: Pick<TestArgs, 'max' | 'min'>) => max >= min;
const testRange = ({ max, min, value }: TestArgs) =>
value >= min && value <= max;
const tests: {
[id: string]: {
defaults: TestArgs & {
onSuccess: () => void;
};
onFinishBatch?: () => void;
optionalTests?: Array<InputTest>;
tests: Array<InputTest>;
};
} = {
test: ({ value }) => /^[a-zA-Z0-9_-]+$/.test(value as string),
},
],
},
cpuCores: {
defaults: {
max: inputCPUCoresMax,
@ -1323,9 +1259,8 @@ const ProvisionServerDialog = ({
],
},
};
virtualDisks.inputSizeMessages.forEach((error, vdIndex) => {
tests[`vd${vdIndex}Size`] = {
virtualDisks.inputSizeMessages.forEach((message, vdIndex) => {
inputTests[`vd${vdIndex}Size`] = {
defaults: {
max: virtualDisks.maxes[vdIndex],
min: 1,
@ -1359,7 +1294,7 @@ const ProvisionServerDialog = ({
],
};
tests[`vd${vdIndex}StorageGroup`] = {
inputTests[`vd${vdIndex}StorageGroup`] = {
defaults: {
max: 0,
min: 0,
@ -1379,82 +1314,83 @@ const ProvisionServerDialog = ({
};
});
const testsToRun =
inputs ??
Object.keys(tests).reduce<
Exclude<
Exclude<Parameters<TestInputFunction>[0], undefined>['inputs'],
undefined
>
>((reduceContainer, id: string) => {
reduceContainer[id] = {};
return reduceContainer;
}, {});
let allResult = true;
Object.keys(testsToRun).every((id: string) => {
const updateLimits: UpdateLimitsFunction = ({
allAnvils: ulAllAnvils = allAnvils,
cpuCores: ulCPUCores = inputCPUCoresValue,
fileUUIDs: ulFileUUIDs = [inputInstallISOFileUUID, inputDriverISOFileUUID],
includeAnvilUUIDs: ulIncludeAnvilUUIDs = filterBlanks([inputAnvilValue]),
includeFileUUIDs: ulIncludeFileUUIDs,
includeStorageGroupUUIDs: ulIncludeStorageGroupUUIDs,
inputMemoryUnit: ulInputMemoryUnit = inputMemoryUnit,
memory: ulMemory = memory,
storageGroupUUIDMapToFree:
ulStorageGroupUUIDMapToFree = storageGroupUUIDMapToFree,
virtualDisks: ulVirtualDisks = virtualDisks,
} = {}) => {
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<InputTest>) => {
cbFailure: InputTest['onFailure'];
cbSuccess: InputTest['onSuccess'];
} = () => ({ cbFailure: undefined, cbSuccess: undefined });
if (!isIgnoreOnCallbacks) {
cbFinishBatch = dOnFinishBatch;
setOnResult = ({ onFailure, onSuccess }: Partial<InputTest> = {}) => ({
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;
};
anvilUUIDs,
fileUUIDs,
maxCPUCores,
maxMemory,
maxVirtualDiskSizes,
storageGroupUUIDs,
} = filterAnvils(
ulAllAnvils,
ulStorageGroupUUIDMapToFree,
ulCPUCores,
ulMemory,
ulVirtualDisks.sizes,
ulVirtualDisks.inputStorageGroupUUIDs,
ulFileUUIDs,
{
includeAnvilUUIDs: ulIncludeAnvilUUIDs,
includeFileUUIDs: ulIncludeFileUUIDs,
includeStorageGroupUUIDs: ulIncludeStorageGroupUUIDs,
},
);
// Don't need to pass optional tests for input to be valid.
optionalTests?.forEach(runTest);
setInputCPUCoresMax(maxCPUCores);
setMemoryMax(maxMemory);
const requiredTestsResult = requiredTests.every(runTest);
ulVirtualDisks.maxes = maxVirtualDiskSizes;
ulVirtualDisks.maxes.forEach((vdMaxSize, vdIndex) => {
dSize(vdMaxSize, {
fromUnit: 'B',
onSuccess: {
string: (value) => {
ulVirtualDisks.inputMaxes[vdIndex] = value;
},
},
toUnit: ulVirtualDisks.inputUnits[vdIndex],
});
});
setVirtualDisks({ ...ulVirtualDisks });
cbFinishBatch?.call(null);
setIncludeAnvilUUIDs(anvilUUIDs);
setIncludeFileUUIDs(fileUUIDs);
setIncludeStorageGroupUUIDs(storageGroupUUIDs);
return requiredTestsResult || isContinueOnFailure;
dSize(maxMemory, {
fromUnit: 'B',
onSuccess: {
string: (value) => setInputMemoryMax(value),
},
toUnit: ulInputMemoryUnit,
});
return allResult;
return {
maxCPUCores,
maxMemory,
maxVirtualDiskSizes,
};
};
// The memorized version of updateLimits() should only be called during first render.
// eslint-disable-next-line react-hooks/exhaustive-deps
const initLimits = useCallback(updateLimits, []);
const testInput = (
...[options, ...restArgs]: Parameters<TestInputFunction>
) => baseTestInput({ tests: inputTests, ...options }, ...restArgs);
const changeMemory = ({
cmValue = BIGINT_ZERO,
@ -1581,7 +1517,21 @@ const ProvisionServerDialog = ({
},
}}
>
<OutlinedInputWithLabel id="ps-server-name" label="Server name" />
<Box sx={{ display: 'flex', flexDirection: 'column' }}>
<OutlinedInputWithLabel
id="ps-server-name"
label="Server name"
inputProps={{
onChange: ({ target: { value } }) => {
setInputServerNameValue(value);
testInput({ inputs: { serverName: { value } } });
},
value: inputServerNameValue,
}}
messageBoxProps={inputServerNameMessage}
/>
</Box>
{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,

@ -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 };

@ -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<TestInputFunctionOptions['inputs'], undefined>
>((reduceContainer, id: string) => {
reduceContainer[id] = {};
return reduceContainer;
}, {});
let allResult = true;
let setBatchCallback: (
batch?: Partial<
Exclude<TestInputFunctionOptions['tests'], undefined>[string]
>,
) => {
cbFinishBatch: Exclude<
TestInputFunctionOptions['tests'],
undefined
>[string]['onFinishBatch'];
} = () => ({ cbFinishBatch: undefined });
let setSingleCallback: (test?: Partial<InputTest>) => {
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;

@ -0,0 +1,5 @@
import { InputTestArgs } from '../../types/TestInputFunction';
const testMax: (args: InputTestArgs) => boolean = ({ max, min }) => max >= min;
export default testMax;

@ -0,0 +1,6 @@
import { InputTestArgs } from '../../types/TestInputFunction';
const testNotBlank: (args: InputTestArgs) => boolean = ({ value }) =>
value !== '';
export default testNotBlank;

@ -0,0 +1,6 @@
import { InputTestArgs } from '../../types/TestInputFunction';
const testRange: (args: InputTestArgs) => boolean = ({ max, min, value }) =>
value >= min && value <= max;
export default testRange;

@ -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<InputTest>;
tests: Array<InputTest>;
};
};
export type TestInputFunctionOptions = {
inputs?: {
[id: string]: Partial<InputTestArgs>;
};
isContinueOnFailure?: boolean;
isIgnoreOnCallbacks?: boolean;
tests?: InputTestBatches;
};
export type TestInputFunction = (options?: TestInputFunctionOptions) => boolean;
Loading…
Cancel
Save