From 86ecd3537ceebc95d8235fb19c14407a8038a49f Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Fri, 4 Aug 2023 19:40:34 -0400 Subject: [PATCH] fix(striker-ui): auto-suggest last octet of not IFN on striker based on host sequence --- striker-ui/components/GeneralInitForm.tsx | 1345 +++++++-------- striker-ui/components/NetworkInitForm.tsx | 1813 +++++++++++---------- striker-ui/components/StrikerInitForm.tsx | 5 + 3 files changed, 1634 insertions(+), 1529 deletions(-) diff --git a/striker-ui/components/GeneralInitForm.tsx b/striker-ui/components/GeneralInitForm.tsx index cb41b5f6..21fce477 100644 --- a/striker-ui/components/GeneralInitForm.tsx +++ b/striker-ui/components/GeneralInitForm.tsx @@ -96,782 +96,817 @@ const GeneralInitForm = forwardRef< { expectHostDetail?: boolean; hostDetail?: APIHostDetail; + onHostNumberBlurAppend?: OutlinedInputWithLabelProps['onBlur']; toggleSubmitDisabled?: ToggleSubmitDisabledFunction; } ->(({ expectHostDetail = false, hostDetail, toggleSubmitDisabled }, ref) => { - const [helpMessage, setHelpMessage] = useState(); - const [isShowOrganizationPrefixSuggest, setIsShowOrganizationPrefixSuggest] = - useState(false); - const [isShowHostNameSuggest, setIsShowHostNameSuggest] = - useState(false); - const [isConfirmAdminPassword, setIsConfirmAdminPassword] = - useState(true); - const [isValidateDomain, setIsValidateDomain] = useState(true); - - const readHostDetailRef = useRef(true); - - const adminPasswordInputRef = useRef>({}); - const confirmAdminPasswordInputRef = useRef< - InputForwardedRefContent<'string'> - >({}); - const organizationNameInputRef = useRef>( - {}, - ); - const organizationPrefixInputRef = useRef>( - {}, - ); - const domainNameInputRef = useRef>({}); - const hostNumberInputRef = useRef>({}); - const hostNameInputRef = useRef>({}); - const messageGroupRef = useRef({}); - - const setOrganizationPrefixInputMessage = useCallback( - (message?: Message) => - messageGroupRef.current.setMessage?.call( - null, - IT_IDS.organizationPrefix, - message, - ), - [], - ); - const setHostNumberInputMessage = useCallback( - (message?: Message) => - messageGroupRef.current.setMessage?.call( - null, - IT_IDS.hostNumber, - message, - ), - [], - ); - const setDomainNameInputMessage = useCallback( - (message?: Message) => - messageGroupRef.current.setMessage?.call( - null, - IT_IDS.domainName, - message, - ), - [], - ); - const setHostNameInputMessage = useCallback( - (message?: Message) => - messageGroupRef.current.setMessage?.call(null, IT_IDS.hostName, message), - [], - ); - const setAdminPasswordInputMessage = useCallback( - (message?: Message) => - messageGroupRef.current.setMessage?.call( - null, - IT_IDS.adminPassword, - message, - ), - [], - ); - const setConfirmAdminPasswordInputMessage = useCallback( - (message?: Message) => - messageGroupRef.current.setMessage?.call( - null, - IT_IDS.confirmAdminPassword, - message, - ), - [], - ); - - const inputTests: InputTestBatches = useMemo( - () => ({ - [IT_IDS.adminPassword]: { - defaults: { - getValue: () => adminPasswordInputRef.current.getValue?.call(null), - onSuccess: () => { - setAdminPasswordInputMessage(undefined); +>( + ( + { + expectHostDetail = false, + hostDetail, + onHostNumberBlurAppend, + toggleSubmitDisabled, + }, + ref, + ) => { + const [helpMessage, setHelpMessage] = useState(); + const [ + isShowOrganizationPrefixSuggest, + setIsShowOrganizationPrefixSuggest, + ] = useState(false); + const [isShowHostNameSuggest, setIsShowHostNameSuggest] = + useState(false); + const [isConfirmAdminPassword, setIsConfirmAdminPassword] = + useState(true); + const [isValidateDomain, setIsValidateDomain] = useState(true); + + const readHostDetailRef = useRef(true); + + const adminPasswordInputRef = useRef>( + {}, + ); + const confirmAdminPasswordInputRef = useRef< + InputForwardedRefContent<'string'> + >({}); + const organizationNameInputRef = useRef>( + {}, + ); + const organizationPrefixInputRef = useRef< + InputForwardedRefContent<'string'> + >({}); + const domainNameInputRef = useRef>({}); + const hostNumberInputRef = useRef>({}); + const hostNameInputRef = useRef>({}); + const messageGroupRef = useRef({}); + + const setOrganizationPrefixInputMessage = useCallback( + (message?: Message) => + messageGroupRef.current.setMessage?.call( + null, + IT_IDS.organizationPrefix, + message, + ), + [], + ); + const setHostNumberInputMessage = useCallback( + (message?: Message) => + messageGroupRef.current.setMessage?.call( + null, + IT_IDS.hostNumber, + message, + ), + [], + ); + const setDomainNameInputMessage = useCallback( + (message?: Message) => + messageGroupRef.current.setMessage?.call( + null, + IT_IDS.domainName, + message, + ), + [], + ); + const setHostNameInputMessage = useCallback( + (message?: Message) => + messageGroupRef.current.setMessage?.call( + null, + IT_IDS.hostName, + message, + ), + [], + ); + const setAdminPasswordInputMessage = useCallback( + (message?: Message) => + messageGroupRef.current.setMessage?.call( + null, + IT_IDS.adminPassword, + message, + ), + [], + ); + const setConfirmAdminPasswordInputMessage = useCallback( + (message?: Message) => + messageGroupRef.current.setMessage?.call( + null, + IT_IDS.confirmAdminPassword, + message, + ), + [], + ); + + const inputTests: InputTestBatches = useMemo( + () => ({ + [IT_IDS.adminPassword]: { + defaults: { + getValue: () => adminPasswordInputRef.current.getValue?.call(null), + onSuccess: () => { + setAdminPasswordInputMessage(undefined); + }, }, + tests: [ + { + onFailure: () => { + setAdminPasswordInputMessage({ + children: ( + <> + Admin password cannot contain single-quote ( + + ), double-quote ( + ), slash ( + ), backslash ( + ), angle brackets ( + ), curly brackets ( + ). + + ), + }); + }, + test: ({ value }) => !/['"/\\><}{]/g.test(value as string), + }, + { test: testNotBlank }, + ], }, - tests: [ - { - onFailure: () => { - setAdminPasswordInputMessage({ - children: ( - <> - Admin password cannot contain single-quote ( - - ), double-quote ( - ), slash ( - ), backslash ( - ), angle brackets ( - ), curly brackets ( - ). - - ), - }); + [IT_IDS.confirmAdminPassword]: { + defaults: { + getValue: () => + confirmAdminPasswordInputRef.current?.getValue?.call(null), + onSuccess: () => { + setConfirmAdminPasswordInputMessage(undefined); }, - test: ({ value }) => !/['"/\\><}{]/g.test(value as string), - }, - { test: testNotBlank }, - ], - }, - [IT_IDS.confirmAdminPassword]: { - defaults: { - getValue: () => - confirmAdminPasswordInputRef.current?.getValue?.call(null), - onSuccess: () => { - setConfirmAdminPasswordInputMessage(undefined); }, + tests: [ + { + onFailure: () => { + setConfirmAdminPasswordInputMessage({ + children: "Confirmation doesn't match admin password.", + }); + }, + test: ({ value }) => + value === adminPasswordInputRef.current.getValue?.call(null), + }, + { test: testNotBlank }, + ], }, - tests: [ - { - onFailure: () => { - setConfirmAdminPasswordInputMessage({ - children: "Confirmation doesn't match admin password.", - }); + [IT_IDS.domainName]: { + defaults: { + compare: [!isValidateDomain], + getValue: () => domainNameInputRef.current.getValue?.call(null), + onSuccess: () => { + setDomainNameInputMessage(undefined); }, - test: ({ value }) => - value === adminPasswordInputRef.current.getValue?.call(null), - }, - { test: testNotBlank }, - ], - }, - [IT_IDS.domainName]: { - defaults: { - compare: [!isValidateDomain], - getValue: () => domainNameInputRef.current.getValue?.call(null), - onSuccess: () => { - setDomainNameInputMessage(undefined); }, + tests: [ + { + onFailure: () => { + setDomainNameInputMessage({ + children: ( + <> + Domain name can only contain lowercase alphanumeric, + hyphen ( + ), and dot () characters. + + ), + }); + }, + test: ({ compare, value }) => + (compare[0] as boolean) || REP_DOMAIN.test(value as string), + }, + { test: testNotBlank }, + ], }, - tests: [ - { - onFailure: () => { - setDomainNameInputMessage({ - children: ( - <> - Domain name can only contain lowercase alphanumeric, hyphen - ( - ), and dot () characters. - - ), - }); + [IT_IDS.hostName]: { + defaults: { + compare: [!isValidateDomain], + getValue: () => hostNameInputRef.current.getValue?.call(null), + onSuccess: () => { + setHostNameInputMessage(undefined); }, - test: ({ compare, value }) => - (compare[0] as boolean) || REP_DOMAIN.test(value as string), - }, - { test: testNotBlank }, - ], - }, - [IT_IDS.hostName]: { - defaults: { - compare: [!isValidateDomain], - getValue: () => hostNameInputRef.current.getValue?.call(null), - onSuccess: () => { - setHostNameInputMessage(undefined); }, + tests: [ + { + onFailure: () => { + setHostNameInputMessage({ + children: ( + <> + Host name can only contain lowercase alphanumeric, hyphen + ( + + ), and dot () characters. + + ), + }); + }, + test: ({ compare, value }) => + (compare[0] as boolean) || REP_DOMAIN.test(value as string), + }, + { test: testNotBlank }, + ], }, - tests: [ - { - onFailure: () => { - setHostNameInputMessage({ - children: ( - <> - Host name can only contain lowercase alphanumeric, hyphen ( - - ), and dot () characters. - - ), - }); + [IT_IDS.hostNumber]: { + defaults: { + getValue: () => hostNumberInputRef.current.getValue?.call(null), + onSuccess: () => { + setHostNumberInputMessage(undefined); }, - test: ({ compare, value }) => - (compare[0] as boolean) || REP_DOMAIN.test(value as string), }, - { test: testNotBlank }, - ], - }, - [IT_IDS.hostNumber]: { - defaults: { - getValue: () => hostNumberInputRef.current.getValue?.call(null), - onSuccess: () => { - setHostNumberInputMessage(undefined); + tests: [ + { + onFailure: () => { + setHostNumberInputMessage({ + children: 'Striker number can only contain digits.', + }); + }, + test: ({ value }) => /^\d+$/.test(value as string), + }, + { test: testNotBlank }, + ], + }, + [IT_IDS.organizationName]: { + defaults: { + getValue: () => + organizationNameInputRef.current.getValue?.call(null), }, + tests: [{ test: testNotBlank }], }, - tests: [ - { - onFailure: () => { - setHostNumberInputMessage({ - children: 'Striker number can only contain digits.', - }); + [IT_IDS.organizationPrefix]: { + defaults: { + getValue: () => + organizationPrefixInputRef.current.getValue?.call(null), + max: MAX_ORGANIZATION_PREFIX_LENGTH, + min: MIN_ORGANIZATION_PREFIX_LENGTH, + onSuccess: () => { + setOrganizationPrefixInputMessage(undefined); }, - test: ({ value }) => /^\d+$/.test(value as string), }, - { test: testNotBlank }, - ], - }, - [IT_IDS.organizationName]: { - defaults: { - getValue: () => organizationNameInputRef.current.getValue?.call(null), + tests: [ + { + onFailure: ({ max, min }) => { + setOrganizationPrefixInputMessage({ + children: `Organization prefix must be ${min} to ${max} lowercase alphanumeric characters.`, + }); + }, + test: ({ max, min, value }) => + RegExp(`^[a-z0-9]{${min},${max}}$`).test(value as string), + }, + ], }, - tests: [{ test: testNotBlank }], + }), + [ + isValidateDomain, + setAdminPasswordInputMessage, + setConfirmAdminPasswordInputMessage, + setDomainNameInputMessage, + setHostNameInputMessage, + setHostNumberInputMessage, + setOrganizationPrefixInputMessage, + ], + ); + const testInput = useMemo( + () => createTestInputFunction(inputTests), + [inputTests], + ); + + const testInputToToggleSubmitDisabled = useCallback( + ({ + excludeTestIds = [], + inputs, + isContinueOnFailure, + isExcludeConfirmAdminPassword = !isConfirmAdminPassword, + }: Pick< + TestInputFunctionOptions, + 'inputs' | 'excludeTestIds' | 'isContinueOnFailure' + > & { + isExcludeConfirmAdminPassword?: boolean; + } = {}) => { + if (isExcludeConfirmAdminPassword) { + excludeTestIds.push(IT_IDS.confirmAdminPassword); + } + + toggleSubmitDisabled?.call( + null, + testInput({ + excludeTestIds, + inputs, + isContinueOnFailure, + isIgnoreOnCallbacks: true, + isTestAll: true, + }), + ); }, - [IT_IDS.organizationPrefix]: { - defaults: { - getValue: () => - organizationPrefixInputRef.current.getValue?.call(null), - max: MAX_ORGANIZATION_PREFIX_LENGTH, - min: MIN_ORGANIZATION_PREFIX_LENGTH, - onSuccess: () => { - setOrganizationPrefixInputMessage(undefined); - }, - }, - tests: [ - { - onFailure: ({ max, min }) => { - setOrganizationPrefixInputMessage({ - children: `Organization prefix must be ${min} to ${max} lowercase alphanumeric characters.`, - }); + [isConfirmAdminPassword, testInput, toggleSubmitDisabled], + ); + const populateOrganizationPrefixInput = useCallback( + ({ + organizationName = organizationNameInputRef.current.getValue?.call( + null, + ), + } = {}) => { + const organizationPrefix = buildOrganizationPrefix(organizationName); + + organizationPrefixInputRef.current.setValue?.call( + null, + organizationPrefix, + ); + + testInputToToggleSubmitDisabled({ + inputs: { + [IT_IDS.organizationPrefix]: { + isIgnoreOnCallbacks: false, + value: organizationPrefix, }, - test: ({ max, min, value }) => - RegExp(`^[a-z0-9]{${min},${max}}$`).test(value as string), }, - ], - }, - }), - [ - isValidateDomain, - setAdminPasswordInputMessage, - setConfirmAdminPasswordInputMessage, - setDomainNameInputMessage, - setHostNameInputMessage, - setHostNumberInputMessage, - setOrganizationPrefixInputMessage, - ], - ); - const testInput = useMemo( - () => createTestInputFunction(inputTests), - [inputTests], - ); - - const testInputToToggleSubmitDisabled = useCallback( - ({ - excludeTestIds = [], - inputs, - isContinueOnFailure, - isExcludeConfirmAdminPassword = !isConfirmAdminPassword, - }: Pick< - TestInputFunctionOptions, - 'inputs' | 'excludeTestIds' | 'isContinueOnFailure' - > & { - isExcludeConfirmAdminPassword?: boolean; - } = {}) => { - if (isExcludeConfirmAdminPassword) { - excludeTestIds.push(IT_IDS.confirmAdminPassword); - } + isContinueOnFailure: true, + }); - toggleSubmitDisabled?.call( - null, - testInput({ - excludeTestIds, - inputs, - isContinueOnFailure, - isIgnoreOnCallbacks: true, - isTestAll: true, - }), - ); - }, - [isConfirmAdminPassword, testInput, toggleSubmitDisabled], - ); - const populateOrganizationPrefixInput = useCallback( - ({ - organizationName = organizationNameInputRef.current.getValue?.call(null), - } = {}) => { - const organizationPrefix = buildOrganizationPrefix(organizationName); - - organizationPrefixInputRef.current.setValue?.call( - null, - organizationPrefix, - ); - - testInputToToggleSubmitDisabled({ - inputs: { - [IT_IDS.organizationPrefix]: { - isIgnoreOnCallbacks: false, - value: organizationPrefix, + return organizationPrefix; + }, + [testInputToToggleSubmitDisabled], + ); + const populateHostNameInput = useCallback( + ({ + organizationPrefix = organizationPrefixInputRef.current.getValue?.call( + null, + ), + hostNumber = hostNumberInputRef.current.getValue?.call(null), + domainName = domainNameInputRef.current.getValue?.call(null), + } = {}) => { + const hostName = buildHostName({ + organizationPrefix, + hostNumber, + domainName, + }); + + hostNameInputRef.current.setValue?.call(null, hostName); + + testInputToToggleSubmitDisabled({ + inputs: { + [IT_IDS.hostName]: { isIgnoreOnCallbacks: false, value: hostName }, }, - }, - isContinueOnFailure: true, - }); + isContinueOnFailure: true, + }); - return organizationPrefix; - }, - [testInputToToggleSubmitDisabled], - ); - const populateHostNameInput = useCallback( - ({ - organizationPrefix = organizationPrefixInputRef.current.getValue?.call( - null, - ), - hostNumber = hostNumberInputRef.current.getValue?.call(null), - domainName = domainNameInputRef.current.getValue?.call(null), - } = {}) => { - const hostName = buildHostName({ - organizationPrefix, - hostNumber, - domainName, - }); - - hostNameInputRef.current.setValue?.call(null, hostName); - - testInputToToggleSubmitDisabled({ - inputs: { - [IT_IDS.hostName]: { isIgnoreOnCallbacks: false, value: hostName }, - }, - isContinueOnFailure: true, - }); - - return hostName; - }, - [testInputToToggleSubmitDisabled], - ); - const isOrganizationPrefixPrereqFilled = useCallback( - () => Boolean(organizationNameInputRef.current.getValue?.call(null)), - [], - ); - const isHostNamePrereqFilled = useCallback( - () => - [ - organizationPrefixInputRef.current.getValue?.call(null), - hostNumberInputRef.current.getValue?.call(null), - domainNameInputRef.current.getValue?.call(null), - ].every((value) => Boolean(value)), - [], - ); - const populateOrganizationPrefixInputOnBlur: OutlinedInputWithLabelOnBlur = - useCallback(() => { - if (organizationPrefixInputRef.current.getIsChangedByUser?.call(null)) { - setIsShowOrganizationPrefixSuggest(isOrganizationPrefixPrereqFilled()); - } else { - populateOrganizationPrefixInput(); - } - }, [isOrganizationPrefixPrereqFilled, populateOrganizationPrefixInput]); - const populateHostNameInputOnBlur: OutlinedInputWithLabelOnBlur = - useCallback(() => { - if (hostNameInputRef.current.getIsChangedByUser?.call(null)) { - setIsShowHostNameSuggest(isHostNamePrereqFilled()); - } else { - populateHostNameInput(); + return hostName; + }, + [testInputToToggleSubmitDisabled], + ); + const isOrganizationPrefixPrereqFilled = useCallback( + () => Boolean(organizationNameInputRef.current.getValue?.call(null)), + [], + ); + const isHostNamePrereqFilled = useCallback( + () => + [ + organizationPrefixInputRef.current.getValue?.call(null), + hostNumberInputRef.current.getValue?.call(null), + domainNameInputRef.current.getValue?.call(null), + ].every((value) => Boolean(value)), + [], + ); + const populateOrganizationPrefixInputOnBlur: OutlinedInputWithLabelOnBlur = + useCallback(() => { + if (organizationPrefixInputRef.current.getIsChangedByUser?.call(null)) { + setIsShowOrganizationPrefixSuggest( + isOrganizationPrefixPrereqFilled(), + ); + } else { + populateOrganizationPrefixInput(); + } + }, [isOrganizationPrefixPrereqFilled, populateOrganizationPrefixInput]); + const populateHostNameInputOnBlur: OutlinedInputWithLabelOnBlur = + useCallback(() => { + if (hostNameInputRef.current.getIsChangedByUser?.call(null)) { + setIsShowHostNameSuggest(isHostNamePrereqFilled()); + } else { + populateHostNameInput(); + } + }, [isHostNamePrereqFilled, populateHostNameInput]); + const handleOrganizationPrefixSuggest = useCallback(() => { + const organizationPrefix = populateOrganizationPrefixInput(); + + if (!hostNameInputRef.current.getIsChangedByUser?.call(null)) { + populateHostNameInput({ organizationPrefix }); } - }, [isHostNamePrereqFilled, populateHostNameInput]); - const handleOrganizationPrefixSuggest = useCallback(() => { - const organizationPrefix = populateOrganizationPrefixInput(); - - if (!hostNameInputRef.current.getIsChangedByUser?.call(null)) { - populateHostNameInput({ organizationPrefix }); - } - }, [populateHostNameInput, populateOrganizationPrefixInput]); - const handlerHostNameSuggest = useCallback(() => { - populateHostNameInput(); - }, [populateHostNameInput]); - const buildHelpMessage = useCallback( - (text: string) => (previous?: string) => - previous === text ? undefined : text, - [], - ); - - const validateDomainCheckbox = useMemo( - () => ( - { - setIsValidateDomain(checked); - testInputToToggleSubmitDisabled({ - inputs: { - [IT_IDS.domainName]: { - compare: [!checked], - isIgnoreOnCallbacks: false, - }, - [IT_IDS.hostName]: { - compare: [!checked], - isIgnoreOnCallbacks: false, - }, - }, - isContinueOnFailure: true, - }); - }} - sx={{ padding: '.2em' }} - /> - ), - [isValidateDomain, testInputToToggleSubmitDisabled], - ); - - useEffect(() => { - if ( - [ - expectHostDetail, - hostDetail, - readHostDetailRef.current, - domainNameInputRef.current, - hostNameInputRef.current, - hostNumberInputRef.current, - organizationNameInputRef.current, - organizationPrefixInputRef.current, - ].every((condition) => Boolean(condition)) - ) { - readHostDetailRef.current = false; - - const { - domain: pDomain, - hostName: pHostName, - organization: pOrganization, - prefix: pPrefix, - sequence: pSequence, - } = hostDetail as APIHostDetail; - - domainNameInputRef.current.setValue?.call(null, pDomain); - hostNameInputRef.current.setValue?.call(null, pHostName); - hostNumberInputRef.current.setValue?.call(null, pSequence); - organizationNameInputRef.current.setValue?.call(null, pOrganization); - organizationPrefixInputRef.current.setValue?.call(null, pPrefix); - - testInputToToggleSubmitDisabled(); - } - }, [expectHostDetail, hostDetail, testInputToToggleSubmitDisabled]); - - useImperativeHandle(ref, () => ({ - get: () => ({ - adminPassword: adminPasswordInputRef.current.getValue?.call(null), - organizationName: organizationNameInputRef.current.getValue?.call(null), - organizationPrefix: - organizationPrefixInputRef.current.getValue?.call(null), - domainName: domainNameInputRef.current.getValue?.call(null), - hostNumber: hostNumberInputRef.current.getValue?.call(null), - hostName: hostNameInputRef.current.getValue?.call(null), - }), - })); - - return ( - - - - - { - testInputToToggleSubmitDisabled(); - }} - onHelp={() => { - setHelpMessage( - buildHelpMessage( - 'Name of the organization that maintains this Anvil! system. You can enter anything that makes sense to you.', - ), - ); - }} - /> - } - ref={organizationNameInputRef} - /> - :first-child': { - flexGrow: 1, + }, [populateHostNameInput, populateOrganizationPrefixInput]); + const handlerHostNameSuggest = useCallback(() => { + populateHostNameInput(); + }, [populateHostNameInput]); + const buildHelpMessage = useCallback( + (text: string) => (previous?: string) => + previous === text ? undefined : text, + [], + ); + + const validateDomainCheckbox = useMemo( + () => ( + { + setIsValidateDomain(checked); + testInputToToggleSubmitDisabled({ + inputs: { + [IT_IDS.domainName]: { + compare: [!checked], + isIgnoreOnCallbacks: false, }, - }} - > + [IT_IDS.hostName]: { + compare: [!checked], + isIgnoreOnCallbacks: false, + }, + }, + isContinueOnFailure: true, + }); + }} + sx={{ padding: '.2em' }} + /> + ), + [isValidateDomain, testInputToToggleSubmitDisabled], + ); + + useEffect(() => { + if ( + [ + expectHostDetail, + hostDetail, + readHostDetailRef.current, + domainNameInputRef.current, + hostNameInputRef.current, + hostNumberInputRef.current, + organizationNameInputRef.current, + organizationPrefixInputRef.current, + ].every((condition) => Boolean(condition)) + ) { + readHostDetailRef.current = false; + + const { + domain: pDomain, + hostName: pHostName, + organization: pOrganization, + prefix: pPrefix, + sequence: pSequence, + } = hostDetail as APIHostDetail; + + domainNameInputRef.current.setValue?.call(null, pDomain); + hostNameInputRef.current.setValue?.call(null, pHostName); + hostNumberInputRef.current.setValue?.call(null, pSequence); + organizationNameInputRef.current.setValue?.call(null, pOrganization); + organizationPrefixInputRef.current.setValue?.call(null, pPrefix); + + testInputToToggleSubmitDisabled(); + } + }, [expectHostDetail, hostDetail, testInputToToggleSubmitDisabled]); + + useImperativeHandle(ref, () => ({ + get: () => ({ + adminPassword: adminPasswordInputRef.current.getValue?.call(null), + organizationName: organizationNameInputRef.current.getValue?.call(null), + organizationPrefix: + organizationPrefixInputRef.current.getValue?.call(null), + domainName: domainNameInputRef.current.getValue?.call(null), + hostNumber: hostNumberInputRef.current.getValue?.call(null), + hostName: hostNameInputRef.current.getValue?.call(null), + }), + })); + + return ( + + + + - ), - inputProps: { - maxLength: MAX_ORGANIZATION_PREFIX_LENGTH, - sx: { - minWidth: '2.5em', - }, - }, - onBlur: (event, ...resetArgs) => { - const { - target: { value }, - } = event; - - testInput({ - inputs: { [IT_IDS.organizationPrefix]: { value } }, - }); - populateHostNameInputOnBlur(event, ...resetArgs); - }, + onBlur: populateOrganizationPrefixInputOnBlur, }} inputLabelProps={{ isNotifyRequired: true }} - label="Prefix" - onChange={({ target: { value } }) => { - testInputToToggleSubmitDisabled({ - inputs: { [IT_IDS.organizationPrefix]: { value } }, - }); - setOrganizationPrefixInputMessage(); - setIsShowOrganizationPrefixSuggest( - isOrganizationPrefixPrereqFilled(), - ); + label="Organization name" + onChange={() => { + testInputToToggleSubmitDisabled(); }} onHelp={() => { setHelpMessage( buildHelpMessage( - "Alphanumberic short-form of the organization name. It's used as the prefix for host names.", + 'Name of the organization that maintains this Anvil! system. You can enter anything that makes sense to you.', ), ); }} /> } - ref={organizationPrefixInputRef} + ref={organizationNameInputRef} /> + :first-child': { + flexGrow: 1, + }, + }} + > + + ), + inputProps: { + maxLength: MAX_ORGANIZATION_PREFIX_LENGTH, + sx: { + minWidth: '2.5em', + }, + }, + onBlur: (event, ...resetArgs) => { + const { + target: { value }, + } = event; + + testInput({ + inputs: { [IT_IDS.organizationPrefix]: { value } }, + }); + populateHostNameInputOnBlur(event, ...resetArgs); + }, + }} + inputLabelProps={{ isNotifyRequired: true }} + label="Prefix" + onChange={({ target: { value } }) => { + testInputToToggleSubmitDisabled({ + inputs: { [IT_IDS.organizationPrefix]: { value } }, + }); + setOrganizationPrefixInputMessage(); + setIsShowOrganizationPrefixSuggest( + isOrganizationPrefixPrereqFilled(), + ); + }} + onHelp={() => { + setHelpMessage( + buildHelpMessage( + "Alphanumberic short-form of the organization name. It's used as the prefix for host names.", + ), + ); + }} + /> + } + ref={organizationPrefixInputRef} + /> + { + const [event] = args; + const { + target: { value }, + } = event; + + testInput({ + inputs: { [IT_IDS.hostNumber]: { value } }, + }); + populateHostNameInputOnBlur(...args); + + onHostNumberBlurAppend?.call(null, ...args); + }, + }} + inputLabelProps={{ isNotifyRequired: true }} + label="Striker #" + onChange={({ target: { value } }) => { + testInputToToggleSubmitDisabled({ + inputs: { [IT_IDS.hostNumber]: { value } }, + }); + setHostNumberInputMessage(); + }} + onHelp={() => { + setHelpMessage( + buildHelpMessage( + "Number or count of this striker; this should be '1' for the first striker, '2' for the second striker, and such.", + ), + ); + }} + /> + } + ref={hostNumberInputRef} + valueType="number" + /> + + + + + { const { target: { value }, } = event; testInput({ - inputs: { [IT_IDS.hostNumber]: { value } }, + inputs: { [IT_IDS.domainName]: { value } }, }); populateHostNameInputOnBlur(event, ...restArgs); }, }} inputLabelProps={{ isNotifyRequired: true }} - label="Striker #" + label="Domain name" onChange={({ target: { value } }) => { testInputToToggleSubmitDisabled({ - inputs: { [IT_IDS.hostNumber]: { value } }, + inputs: { [IT_IDS.domainName]: { value } }, }); - setHostNumberInputMessage(); + setDomainNameInputMessage(); }} onHelp={() => { setHelpMessage( buildHelpMessage( - "Number or count of this striker; this should be '1' for the first striker, '2' for the second striker, and such.", + "Domain name for this striker. It's also the default domain used when creating new install manifests.", ), ); }} /> } - ref={hostNumberInputRef} - valueType="number" + ref={domainNameInputRef} /> - - - - - - { - const { - target: { value }, - } = event; - - testInput({ inputs: { [IT_IDS.domainName]: { value } } }); - populateHostNameInputOnBlur(event, ...restArgs); - }, - }} - inputLabelProps={{ isNotifyRequired: true }} - label="Domain name" - onChange={({ target: { value } }) => { - testInputToToggleSubmitDisabled({ - inputs: { [IT_IDS.domainName]: { value } }, - }); - setDomainNameInputMessage(); - }} - onHelp={() => { - setHelpMessage( - buildHelpMessage( - "Domain name for this striker. It's also the default domain used when creating new install manifests.", - ), - ); - }} - /> - } - ref={domainNameInputRef} - /> - - - ), - onBlur: ({ target: { value } }) => { - testInput({ inputs: { [IT_IDS.hostName]: { value } } }); - }, - }} - inputLabelProps={{ isNotifyRequired: true }} - label="Host name" - onChange={({ target: { value } }) => { - testInputToToggleSubmitDisabled({ - inputs: { [IT_IDS.hostName]: { value } }, - }); - setHostNameInputMessage(); - setIsShowHostNameSuggest(isHostNamePrereqFilled()); - }} - onHelp={() => { - setHelpMessage( - buildHelpMessage( - "Host name for this striker. It's usually a good idea to use the auto-generated value.", - ), - ); - }} - /> - } - ref={hostNameInputRef} - /> - - - - * > *': { - width: '100%', - }, - }} - > - + + ), onBlur: ({ target: { value } }) => { - testInput({ - inputs: { [IT_IDS.adminPassword]: { value } }, - }); - }, - onPasswordVisibilityAppend: (inputType) => { - const localIsConfirmAdminPassword = - inputType === INPUT_TYPES.password; - - testInputToToggleSubmitDisabled({ - isExcludeConfirmAdminPassword: - !localIsConfirmAdminPassword, - }); - setIsConfirmAdminPassword(localIsConfirmAdminPassword); - setConfirmAdminPasswordInputMessage(); + testInput({ inputs: { [IT_IDS.hostName]: { value } } }); }, }} inputLabelProps={{ isNotifyRequired: true }} - label="Admin password" + label="Host name" onChange={({ target: { value } }) => { testInputToToggleSubmitDisabled({ - inputs: { [IT_IDS.adminPassword]: { value } }, + inputs: { [IT_IDS.hostName]: { value } }, }); - setAdminPasswordInputMessage(); + setHostNameInputMessage(); + setIsShowHostNameSuggest(isHostNamePrereqFilled()); }} onHelp={() => { setHelpMessage( 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.", + "Host name for this striker. It's usually a good idea to use the auto-generated value.", ), ); }} /> } - ref={adminPasswordInputRef} + ref={hostNameInputRef} /> - - {isConfirmAdminPassword && ( + + + + * > *': { + width: '100%', + }, + }} + > { testInput({ - inputs: { - [IT_IDS.confirmAdminPassword]: { value }, - }, + inputs: { [IT_IDS.adminPassword]: { value } }, }); }, + onPasswordVisibilityAppend: (inputType) => { + const localIsConfirmAdminPassword = + inputType === INPUT_TYPES.password; + + testInputToToggleSubmitDisabled({ + isExcludeConfirmAdminPassword: + !localIsConfirmAdminPassword, + }); + setIsConfirmAdminPassword( + localIsConfirmAdminPassword, + ); + setConfirmAdminPasswordInputMessage(); + }, }} - inputLabelProps={{ - isNotifyRequired: isConfirmAdminPassword, - }} - label="Confirm password" + inputLabelProps={{ isNotifyRequired: true }} + label="Admin password" onChange={({ target: { value } }) => { testInputToToggleSubmitDisabled({ - inputs: { [IT_IDS.confirmAdminPassword]: { value } }, + inputs: { [IT_IDS.adminPassword]: { value } }, }); - setConfirmAdminPasswordInputMessage(); + setAdminPasswordInputMessage(); + }} + onHelp={() => { + setHelpMessage( + 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.", + ), + ); }} /> } - ref={confirmAdminPasswordInputRef} + ref={adminPasswordInputRef} /> - )} + {isConfirmAdminPassword && ( + + { + testInput({ + inputs: { + [IT_IDS.confirmAdminPassword]: { value }, + }, + }); + }, + }} + inputLabelProps={{ + isNotifyRequired: isConfirmAdminPassword, + }} + label="Confirm password" + onChange={({ target: { value } }) => { + testInputToToggleSubmitDisabled({ + inputs: { + [IT_IDS.confirmAdminPassword]: { value }, + }, + }); + setConfirmAdminPasswordInputMessage(); + }} + /> + } + ref={confirmAdminPasswordInputRef} + /> + + )} + - - - - :last-child': { flexGrow: 1 } }}> - {validateDomainCheckbox} - - {isValidateDomain - ? 'Uncheck to skip domain and host name pattern validation.' - : 'Check to re-enable domain and host name pattern validation.'} - - - - {helpMessage && ( - { - setHelpMessage(undefined); - }} - > - {helpMessage} + + + :last-child': { flexGrow: 1 } }}> + {validateDomainCheckbox} + + {isValidateDomain + ? 'Uncheck to skip domain and host name pattern validation.' + : 'Check to re-enable domain and host name pattern validation.'} + + - )} - - ); -}); + {helpMessage && ( + { + setHelpMessage(undefined); + }} + > + {helpMessage} + + )} + + ); + }, +); GeneralInitForm.defaultProps = { expectHostDetail: false, hostDetail: undefined, + onHostNumberBlurAppend: undefined, toggleSubmitDisabled: undefined, }; diff --git a/striker-ui/components/NetworkInitForm.tsx b/striker-ui/components/NetworkInitForm.tsx index 9ee49c6c..0df05820 100644 --- a/striker-ui/components/NetworkInitForm.tsx +++ b/striker-ui/components/NetworkInitForm.tsx @@ -110,42 +110,6 @@ const CLASSES = { const INITIAL_IFACES = [undefined, undefined]; const MSG_ID_API = 'api'; -const STRIKER_REQUIRED_NETWORKS: NetworkInput[] = [ - { - inputUUID: '30dd2ac5-8024-4a7e-83a1-6a3df7218972', - interfaces: [...INITIAL_IFACES], - ipAddress: '10.200.1.1', - isRequired: true, - name: `${NETWORK_TYPES.bcn} 1`, - subnetMask: '255.255.0.0', - type: 'bcn', - typeCount: 1, - }, - { - inputUUID: 'e7ef3af5-5602-440c-87f8-69c242e3d7f3', - interfaces: [...INITIAL_IFACES], - ipAddress: '10.201.1.1', - isRequired: true, - name: `${NETWORK_TYPES.ifn} 1`, - subnetMask: '255.255.0.0', - type: 'ifn', - typeCount: 1, - }, -]; -const NODE_REQUIRED_NETWORKS: NetworkInput[] = [ - ...STRIKER_REQUIRED_NETWORKS, - { - inputUUID: '525e4847-f929-44a7-83b2-28eb289ffb57', - interfaces: [...INITIAL_IFACES], - ipAddress: '10.202.1.1', - isRequired: true, - name: `${NETWORK_TYPES.sn} 1`, - subnetMask: '255.255.0.0', - type: 'sn', - typeCount: 1, - }, -]; - const MAX_INTERFACES_PER_NETWORK = 2; const IT_IDS = { dnsCSV: 'dns', @@ -164,6 +128,38 @@ const NETWORK_INTERFACE_TEMPLATE = Array.from( const createInputTestPrefix = (uuid: string) => `network${uuid}`; +const createNetworkInput = ({ + inputUUID = uuidv4(), + interfaces = [...INITIAL_IFACES], + ipAddress = '', + name: initName, + subnetMask = '', + type = '', + typeCount = 0, + ...rest +}: Partial = {}): NetworkInput => { + let name = initName; + + if (!initName) { + if (NETWORK_TYPES[type] && typeCount > 0) { + name = `${NETWORK_TYPES[type]} ${typeCount}`; + } else { + name = 'Unknown Network'; + } + } + + return { + inputUUID, + interfaces, + ipAddress, + name, + subnetMask, + type, + typeCount, + ...rest, + }; +}; + const createNetworkInterfaceTableColumns = ( handleDragMouseDown: ( row: NetworkInterfaceOverviewMetadata, @@ -270,7 +266,7 @@ const NetworkForm: FC<{ interfaceIndex: number, ) => MUIBoxProps['onMouseUp']; getNetworkTypeCount: GetNetworkTypeCountFunction; - hostDetail?: APIHostDetail; + hostDetail?: Partial>; networkIndex: number; networkInput: NetworkInput; networkInterfaceCount: number; @@ -286,7 +282,7 @@ const NetworkForm: FC<{ }> = ({ createDropMouseUpHandler, getNetworkTypeCount, - hostDetail: { hostType } = {}, + hostDetail: { hostType, sequence } = {}, networkIndex, networkInput, networkInterfaceCount, @@ -350,6 +346,20 @@ const NetworkForm: FC<{ : { bcn, ifn, sn }; }, [isNode, networkInterfaceCount]); + useEffect((): void => { + if (hostType !== 'striker' || type === 'ifn') return; + + const changedByUser = + ipAddressInputRef.current.getIsChangedByUser?.call(null); + + if (changedByUser || !Number(sequence)) return; + + ipAddressInputRef.current.setValue?.call( + null, + ipAddress.replace(/^((?:\d+\.){3})\d*$/, `$1${sequence}`), + ); + }, [hostType, ipAddress, sequence, type]); + useEffect(() => { const { ipAddressInputRef: ipRef, subnetMaskInputRef: maskRef } = networkInput; @@ -579,967 +589,1022 @@ const NetworkInitForm = forwardRef< { expectHostDetail?: boolean; hostDetail?: APIHostDetail; + hostSequence?: string; toggleSubmitDisabled?: (testResult: boolean) => void; } ->(({ expectHostDetail = false, hostDetail, toggleSubmitDisabled }, ref) => { - const { hostType, hostUUID = 'local' }: APIHostDetail = - hostDetail ?? ({} as APIHostDetail); +>( + ( + { + expectHostDetail = false, + hostDetail, + hostSequence, + toggleSubmitDisabled, + }, + ref, + ) => { + let hostType: string | undefined; + let hostUUID = 'local'; + let sequence = hostSequence; + + if (!expectHostDetail) { + hostType = 'striker'; + } else if (hostDetail) { + ({ hostType, hostUUID, sequence } = hostDetail); + } - const uninitRequiredNetworks: NetworkInput[] = useMemo( - () => - hostType === 'node' ? NODE_REQUIRED_NETWORKS : STRIKER_REQUIRED_NETWORKS, - [hostType], - ); + const initRequiredNetworks: NetworkInput[] = useMemo(() => { + const result: NetworkInput[] = []; - const requiredNetworks = useMemo>>( - () => - hostType === 'node' ? { bcn: 1, ifn: 1, sn: 1 } : { bcn: 1, ifn: 1 }, - [hostType], - ); + if (hostType === 'striker') { + const ipAddress = sequence ? `10.201.4.${sequence}` : '10.201.4.'; - const [dragMousePosition, setDragMousePosition] = useState<{ - x: number; - y: number; - }>({ x: 0, y: 0 }); - const [networkInterfaceInputMap, setNetworkInterfaceInputMap] = - useState({}); - const [networkInputs, setNetworkInputs] = useState( - uninitRequiredNetworks, - ); - const [networkInterfaceHeld, setNetworkInterfaceHeld] = useState< - NetworkInterfaceOverviewMetadata | undefined - >(); - const [gatewayInterface, setGatewayInterface] = useState(''); - - const dnsCSVInputRef = useRef>({}); - const gatewayInputRef = useRef>({}); - /** Avoid state here to prevent triggering multiple renders when reading - * host detail. */ - const readHostDetailRef = useRef(true); - const messageGroupRef = useRef({}); + result.push( + createNetworkInput({ + ipAddress, + isRequired: true, + subnetMask: '255.255.0.0', + type: 'bcn', + typeCount: 1, + }), + createNetworkInput({ + isRequired: true, + type: 'ifn', + typeCount: 1, + }), + ); - const { - data: networkInterfaces = [], - isLoading: isLoadingNetworkInterfaces, - } = periodicFetch( - `${API_BASE_URL}/init/network-interface/${hostUUID}`, - { - refreshInterval: 2000, - onSuccess: (data) => { - const map = data.reduce( - (result, metadata) => { - const { networkInterfaceUUID } = metadata; + return result; + } - result[networkInterfaceUUID] = networkInterfaceInputMap[ - networkInterfaceUUID - ] ?? { metadata }; + result.push( + createNetworkInput({ + ipAddress: '10.201.', + isRequired: true, + subnetMask: '255.255.0.0', + type: 'bcn', + typeCount: 1, + }), + createNetworkInput({ + isRequired: true, + type: 'ifn', + typeCount: 1, + }), + createNetworkInput({ + ipAddress: '10.101.', + isRequired: true, + subnetMask: '255.255.0.0', + type: 'sn', + typeCount: 1, + }), + ); - return result; - }, - {}, - ); + return result; + }, [hostType, sequence]); - setNetworkInterfaceInputMap(map); - }, - }, - ); + const requiredNetworks = useMemo>>( + () => + hostType === 'node' ? { bcn: 1, ifn: 1, sn: 1 } : { bcn: 1, ifn: 1 }, + [hostType], + ); - const isDisableAddNetworkButton: boolean = useMemo( - () => - networkInputs.length >= networkInterfaces.length || - Object.values(networkInterfaceInputMap).every( - ({ isApplied }) => isApplied, - ) || - (hostType === 'node' && networkInterfaces.length <= 6), - [hostType, networkInputs, networkInterfaces, networkInterfaceInputMap], - ); - const isLoadingHostDetail: boolean = useMemo( - () => expectHostDetail && !hostDetail, - [expectHostDetail, hostDetail], - ); + const [dragMousePosition, setDragMousePosition] = useState<{ + x: number; + y: number; + }>({ x: 0, y: 0 }); + const [networkInterfaceInputMap, setNetworkInterfaceInputMap] = + useState({}); + const [networkInputs, setNetworkInputs] = + useState(initRequiredNetworks); + const [networkInterfaceHeld, setNetworkInterfaceHeld] = useState< + NetworkInterfaceOverviewMetadata | undefined + >(); + const [gatewayInterface, setGatewayInterface] = useState(''); + + const dnsCSVInputRef = useRef>({}); + const gatewayInputRef = useRef>({}); + /** Avoid state here to prevent triggering multiple renders when reading + * host detail. */ + const readHostDetailRef = useRef(true); + const messageGroupRef = useRef({}); + + const { + data: networkInterfaces = [], + isLoading: isLoadingNetworkInterfaces, + } = periodicFetch( + `${API_BASE_URL}/init/network-interface/${hostUUID}`, + { + refreshInterval: 2000, + onSuccess: (data) => { + const map = data.reduce( + (result, metadata) => { + const { networkInterfaceUUID } = metadata; - const setMessage = useCallback( - (key: string, message?: Message) => - messageGroupRef.current.setMessage?.call(null, key, message), - [], - ); - const setMessageRe = useCallback( - (re: RegExp, message?: Message) => - messageGroupRef.current.setMessageRe?.call(null, re, message), - [], - ); - const setDnsInputMessage = useCallback( - (message?: Message) => setMessage(IT_IDS.dnsCSV, message), - [setMessage], - ); - const setGatewayInputMessage = useCallback( - (message?: Message) => setMessage(IT_IDS.gateway, message), - [setMessage], - ); - const subnetContains = useCallback( - ({ - fn = 'every', - ip = '', - mask = '', - isNegateMatch = fn === 'every', - onMatch, - onMiss, - skipUUID, - }: { - fn?: Extract, 'every' | 'some'>; - ip?: string; - isNegateMatch?: boolean; - mask?: string; - onMatch?: (otherInput: NetworkInput) => void; - onMiss?: (otherInput: NetworkInput) => void; - skipUUID?: string; - }) => { - const skipReturn = fn === 'every'; - const match = ( - a: Netmask, - { b, bIP = '' }: { aIP?: string; b?: Netmask; bIP?: string }, - ) => a.contains(b ?? bIP) || (b !== undefined && b.contains(a)); - - let subnet: Netmask | undefined; - - try { - subnet = new Netmask(`${ip}/${mask}`); - // TODO: find a way to express the netmask creation error - // eslint-disable-next-line no-empty - } catch (netmaskError) {} - - return networkInputs[fn]((networkInput) => { - const { inputUUID, ipAddressInputRef, subnetMaskInputRef } = - networkInput; - - if (inputUUID === skipUUID) { - return skipReturn; - } + result[networkInterfaceUUID] = networkInterfaceInputMap[ + networkInterfaceUUID + ] ?? { metadata }; - const otherIP = ipAddressInputRef?.current.getValue?.call(null); - const otherMask = subnetMaskInputRef?.current.getValue?.call(null); + return result; + }, + {}, + ); - let isMatch = false; + setNetworkInterfaceInputMap(map); + }, + }, + ); - try { - const otherSubnet = new Netmask(`${otherIP}/${otherMask}`); + const isDisableAddNetworkButton: boolean = useMemo( + () => + networkInputs.length >= networkInterfaces.length || + Object.values(networkInterfaceInputMap).every( + ({ isApplied }) => isApplied, + ) || + (hostType === 'node' && networkInterfaces.length <= 6), + [hostType, networkInputs, networkInterfaces, networkInterfaceInputMap], + ); + const isLoadingHostDetail: boolean = useMemo( + () => expectHostDetail && !hostDetail, + [expectHostDetail, hostDetail], + ); - isMatch = match(otherSubnet, { b: subnet, bIP: ip }); + const setMessage = useCallback( + (key: string, message?: Message) => + messageGroupRef.current.setMessage?.call(null, key, message), + [], + ); + const setMessageRe = useCallback( + (re: RegExp, message?: Message) => + messageGroupRef.current.setMessageRe?.call(null, re, message), + [], + ); + const setDnsInputMessage = useCallback( + (message?: Message) => setMessage(IT_IDS.dnsCSV, message), + [setMessage], + ); + const setGatewayInputMessage = useCallback( + (message?: Message) => setMessage(IT_IDS.gateway, message), + [setMessage], + ); + const subnetContains = useCallback( + ({ + fn = 'every', + ip = '', + mask = '', + isNegateMatch = fn === 'every', + onMatch, + onMiss, + skipUUID, + }: { + fn?: Extract, 'every' | 'some'>; + ip?: string; + isNegateMatch?: boolean; + mask?: string; + onMatch?: (otherInput: NetworkInput) => void; + onMiss?: (otherInput: NetworkInput) => void; + skipUUID?: string; + }) => { + const skipReturn = fn === 'every'; + const match = ( + a: Netmask, + { b, bIP = '' }: { aIP?: string; b?: Netmask; bIP?: string }, + ) => a.contains(b ?? bIP) || (b !== undefined && b.contains(a)); + let subnet: Netmask | undefined; + + try { + subnet = new Netmask(`${ip}/${mask}`); // TODO: find a way to express the netmask creation error // eslint-disable-next-line no-empty } catch (netmaskError) {} - if (isMatch) { - onMatch?.call(null, networkInput); - } else { - onMiss?.call(null, networkInput); - } + return networkInputs[fn]((networkInput) => { + const { inputUUID, ipAddressInputRef, subnetMaskInputRef } = + networkInput; - return isNegateMatch ? !isMatch : isMatch; - }); - }, - [networkInputs], - ); + if (inputUUID === skipUUID) { + return skipReturn; + } - const setMapNetwork = useCallback( - (value: 0 | 1) => { - api.put('/init/set-map-network', { value }).catch((error) => { - const emsg = handleAPIError(error); + const otherIP = ipAddressInputRef?.current.getValue?.call(null); + const otherMask = subnetMaskInputRef?.current.getValue?.call(null); - emsg.children = ( - <> - Failed to {value ? 'enable' : 'disable'} network mapping.{' '} - {emsg.children} - - ); + let isMatch = false; - setMessage(MSG_ID_API, emsg); - }); - }, - [setMessage], - ); + try { + const otherSubnet = new Netmask(`${otherIP}/${otherMask}`); - const inputTests: InputTestBatches = useMemo(() => { - const tests: InputTestBatches = { - [IT_IDS.dnsCSV]: { - defaults: { - getValue: () => dnsCSVInputRef.current.getValue?.call(null), - onSuccess: () => { - setDnsInputMessage(); - }, - }, - tests: [ - { - onFailure: () => { - setDnsInputMessage({ - children: - 'Domain name servers should be a comma-separated list of IPv4 addresses without trailing comma(s).', - }); - }, - test: ({ value }) => REP_IPV4_CSV.test(value as string), - }, - { test: testNotBlank }, - ], - }, - [IT_IDS.gateway]: { - defaults: { - getValue: () => gatewayInputRef.current.getValue?.call(null), - onSuccess: () => { - setGatewayInputMessage(); - }, - }, - tests: [ - { - onFailure: () => { - setGatewayInputMessage({ - children: 'Gateway should be a valid IPv4 address.', - }); - }, - test: ({ value }) => REP_IPV4.test(value as string), - }, - { - test: ({ value }) => { - let isDistinctIP = true; - - const isIPInOneNetwork = subnetContains({ - fn: 'some', - ip: value as string, - onMatch: ({ ipAddress, name, type, typeCount }) => { - if (value === ipAddress) { - isDistinctIP = false; - - setGatewayInputMessage({ - children: `Gateway cannot be the same as IP address in ${name}.`, - }); - - return; - } + isMatch = match(otherSubnet, { b: subnet, bIP: ip }); - setGatewayInterface(`${type}${typeCount}`); - }, - }); + // TODO: find a way to express the netmask creation error + // eslint-disable-next-line no-empty + } catch (netmaskError) {} - if (!isIPInOneNetwork) { - setGatewayInputMessage({ - children: "Gateway must be in one network's subnet.", - }); - } + if (isMatch) { + onMatch?.call(null, networkInput); + } else { + onMiss?.call(null, networkInput); + } - return isIPInOneNetwork && isDistinctIP; - }, - }, - { test: testNotBlank }, - ], + return isNegateMatch ? !isMatch : isMatch; + }); }, - }; + [networkInputs], + ); - networkInputs.forEach( - ({ - inputUUID, - interfaces, - ipAddressInputRef, - name, - subnetMaskInputRef, - }) => { - const inputTestPrefix = createInputTestPrefix(inputUUID); - const inputTestIDIfaces = IT_IDS.networkInterfaces(inputTestPrefix); - const inputTestIDIPAddress = IT_IDS.networkIPAddress(inputTestPrefix); - const inputTestIDSubnetMask = IT_IDS.networkSubnetMask(inputTestPrefix); - - const setNetworkIfacesInputMessage = (message?: Message) => - setMessage(inputTestIDIfaces, message); - const setNetworkIPAddressInputMessage = (message?: Message) => - setMessage(inputTestIDIPAddress, message); - const setNetworkSubnetMaskInputMessage = (message?: Message) => - setMessage(inputTestIDSubnetMask, message); - const setNetworkSubnetConflictInputMessage = ( - uuid: string, - otherUUID: string, - message?: Message, - ) => { - const id = `${IT_IDS.networkSubnetConflict( - inputTestPrefix, - )}-${otherUUID}`; - const reverseID = `${IT_IDS.networkSubnetConflict( - createInputTestPrefix(otherUUID), - )}-${uuid}`; - - setMessage( - messageGroupRef.current.exists?.call(null, reverseID) - ? reverseID - : id, - message, + const setMapNetwork = useCallback( + (value: 0 | 1) => { + api.put('/init/set-map-network', { value }).catch((error) => { + const emsg = handleAPIError(error); + + emsg.children = ( + <> + Failed to {value ? 'enable' : 'disable'} network mapping.{' '} + {emsg.children} + ); - }; - const testNetworkSubnetConflictWithDefaults = ({ - ip = ipAddressInputRef?.current.getValue?.call(null), - mask = subnetMaskInputRef?.current.getValue?.call(null), - }: { - ip?: string; - mask?: string; - }) => - subnetContains({ - ip, - mask, - onMatch: ({ inputUUID: otherUUID, name: otherName }) => { - setNetworkSubnetConflictInputMessage(inputUUID, otherUUID, { - children: `"${name}" and "${otherName}" cannot be in the same subnet.`, - }); - }, - onMiss: ({ inputUUID: otherUUID }) => { - setNetworkSubnetConflictInputMessage(inputUUID, otherUUID); - }, - skipUUID: inputUUID, - }); - tests[inputTestIDIfaces] = { + setMessage(MSG_ID_API, emsg); + }); + }, + [setMessage], + ); + + const inputTests: InputTestBatches = useMemo(() => { + const tests: InputTestBatches = { + [IT_IDS.dnsCSV]: { defaults: { - getCompare: () => interfaces.map((iface) => iface !== undefined), + getValue: () => dnsCSVInputRef.current.getValue?.call(null), onSuccess: () => { - setNetworkIfacesInputMessage(); + setDnsInputMessage(); }, }, tests: [ { onFailure: () => { - setNetworkIfacesInputMessage({ - children: `${name} must have at least 1 interface.`, - }); - }, - test: ({ compare }) => - (compare as boolean[]).some((ifaceSet) => ifaceSet), - }, - { - onFailure: () => { - setNetworkIfacesInputMessage({ - children: `${name} must have a Link 1 interface.`, + setDnsInputMessage({ + children: + 'Domain name servers should be a comma-separated list of IPv4 addresses without trailing comma(s).', }); }, - test: ({ compare: [iface1Exists, iface2Exists] }) => - !(iface2Exists && !iface1Exists), + test: ({ value }) => REP_IPV4_CSV.test(value as string), }, + { test: testNotBlank }, ], - }; - tests[inputTestIDIPAddress] = { + }, + [IT_IDS.gateway]: { defaults: { - getValue: () => ipAddressInputRef?.current.getValue?.call(null), + getValue: () => gatewayInputRef.current.getValue?.call(null), onSuccess: () => { - setNetworkIPAddressInputMessage(); + setGatewayInputMessage(); }, }, tests: [ { onFailure: () => { - setNetworkIPAddressInputMessage({ - children: `IP address in ${name} must be a valid IPv4 address.`, + setGatewayInputMessage({ + children: 'Gateway should be a valid IPv4 address.', }); }, test: ({ value }) => REP_IPV4.test(value as string), }, { - test: ({ value }) => - testNetworkSubnetConflictWithDefaults({ + test: ({ value }) => { + let isDistinctIP = true; + + const isIPInOneNetwork = subnetContains({ + fn: 'some', ip: value as string, - }), + onMatch: ({ ipAddress, name, type, typeCount }) => { + if (value === ipAddress) { + isDistinctIP = false; + + setGatewayInputMessage({ + children: `Gateway cannot be the same as IP address in ${name}.`, + }); + + return; + } + + setGatewayInterface(`${type}${typeCount}`); + }, + }); + + if (!isIPInOneNetwork) { + setGatewayInputMessage({ + children: "Gateway must be in one network's subnet.", + }); + } + + return isIPInOneNetwork && isDistinctIP; + }, }, { test: testNotBlank }, ], - }; - tests[IT_IDS.networkName(inputTestPrefix)] = { - defaults: { value: name }, - tests: [{ test: testNotBlank }], - }; - tests[inputTestIDSubnetMask] = { - defaults: { - getValue: () => subnetMaskInputRef?.current.getValue?.call(null), - onSuccess: () => { - setNetworkSubnetMaskInputMessage(); - }, - }, - tests: [ - { - onFailure: () => { - setNetworkSubnetMaskInputMessage({ - children: `Subnet mask in ${name} must be a valid IPv4 address.`, + }, + }; + + networkInputs.forEach( + ({ + inputUUID, + interfaces, + ipAddressInputRef, + name, + subnetMaskInputRef, + }) => { + const inputTestPrefix = createInputTestPrefix(inputUUID); + const inputTestIDIfaces = IT_IDS.networkInterfaces(inputTestPrefix); + const inputTestIDIPAddress = IT_IDS.networkIPAddress(inputTestPrefix); + const inputTestIDSubnetMask = + IT_IDS.networkSubnetMask(inputTestPrefix); + + const setNetworkIfacesInputMessage = (message?: Message) => + setMessage(inputTestIDIfaces, message); + const setNetworkIPAddressInputMessage = (message?: Message) => + setMessage(inputTestIDIPAddress, message); + const setNetworkSubnetMaskInputMessage = (message?: Message) => + setMessage(inputTestIDSubnetMask, message); + const setNetworkSubnetConflictInputMessage = ( + uuid: string, + otherUUID: string, + message?: Message, + ) => { + const id = `${IT_IDS.networkSubnetConflict( + inputTestPrefix, + )}-${otherUUID}`; + const reverseID = `${IT_IDS.networkSubnetConflict( + createInputTestPrefix(otherUUID), + )}-${uuid}`; + + setMessage( + messageGroupRef.current.exists?.call(null, reverseID) + ? reverseID + : id, + message, + ); + }; + const testNetworkSubnetConflictWithDefaults = ({ + ip = ipAddressInputRef?.current.getValue?.call(null), + mask = subnetMaskInputRef?.current.getValue?.call(null), + }: { + ip?: string; + mask?: string; + }) => + subnetContains({ + ip, + mask, + onMatch: ({ inputUUID: otherUUID, name: otherName }) => { + setNetworkSubnetConflictInputMessage(inputUUID, otherUUID, { + children: `"${name}" and "${otherName}" cannot be in the same subnet.`, }); }, - test: ({ value }) => REP_IPV4.test(value as string), + onMiss: ({ inputUUID: otherUUID }) => { + setNetworkSubnetConflictInputMessage(inputUUID, otherUUID); + }, + skipUUID: inputUUID, + }); + + tests[inputTestIDIfaces] = { + defaults: { + getCompare: () => interfaces.map((iface) => iface !== undefined), + onSuccess: () => { + setNetworkIfacesInputMessage(); + }, }, - { - test: ({ value }) => - testNetworkSubnetConflictWithDefaults({ - mask: value as string, - }), + tests: [ + { + onFailure: () => { + setNetworkIfacesInputMessage({ + children: `${name} must have at least 1 interface.`, + }); + }, + test: ({ compare }) => + (compare as boolean[]).some((ifaceSet) => ifaceSet), + }, + { + onFailure: () => { + setNetworkIfacesInputMessage({ + children: `${name} must have a Link 1 interface.`, + }); + }, + test: ({ compare: [iface1Exists, iface2Exists] }) => + !(iface2Exists && !iface1Exists), + }, + ], + }; + tests[inputTestIDIPAddress] = { + defaults: { + getValue: () => ipAddressInputRef?.current.getValue?.call(null), + onSuccess: () => { + setNetworkIPAddressInputMessage(); + }, }, - { test: testNotBlank }, - ], - }; + tests: [ + { + onFailure: () => { + setNetworkIPAddressInputMessage({ + children: `IP address in ${name} must be a valid IPv4 address.`, + }); + }, + test: ({ value }) => REP_IPV4.test(value as string), + }, + { + test: ({ value }) => + testNetworkSubnetConflictWithDefaults({ + ip: value as string, + }), + }, + { test: testNotBlank }, + ], + }; + tests[IT_IDS.networkName(inputTestPrefix)] = { + defaults: { value: name }, + tests: [{ test: testNotBlank }], + }; + tests[inputTestIDSubnetMask] = { + defaults: { + getValue: () => subnetMaskInputRef?.current.getValue?.call(null), + onSuccess: () => { + setNetworkSubnetMaskInputMessage(); + }, + }, + tests: [ + { + onFailure: () => { + setNetworkSubnetMaskInputMessage({ + children: `Subnet mask in ${name} must be a valid IPv4 address.`, + }); + }, + test: ({ value }) => REP_IPV4.test(value as string), + }, + { + test: ({ value }) => + testNetworkSubnetConflictWithDefaults({ + mask: value as string, + }), + }, + { test: testNotBlank }, + ], + }; + }, + ); + + return tests; + }, [ + networkInputs, + setDnsInputMessage, + setGatewayInputMessage, + setMessage, + subnetContains, + ]); + const testInput = useMemo( + () => createTestInputFunction(inputTests), + [inputTests], + ); + + const testInputToToggleSubmitDisabled: TestInputToToggleSubmitDisabled = + useCallback( + (options) => { + toggleSubmitDisabled?.call( + null, + testInput({ + isIgnoreOnCallbacks: true, + isTestAll: true, + + ...options, + }), + ); + }, + [testInput, toggleSubmitDisabled], + ); + const clearNetworkInterfaceHeld = useCallback(() => { + setNetworkInterfaceHeld(undefined); + }, []); + const createNetwork = useCallback( + (args: Partial = {}) => { + networkInputs.unshift(createNetworkInput(args)); + + toggleSubmitDisabled?.call(null, false); + setNetworkInputs([...networkInputs]); }, + [networkInputs, toggleSubmitDisabled], ); + const removeNetwork = useCallback( + (networkIndex: number) => { + const [{ inputUUID, interfaces }] = networkInputs.splice( + networkIndex, + 1, + ); - return tests; - }, [ - networkInputs, - setDnsInputMessage, - setGatewayInputMessage, - setMessage, - subnetContains, - ]); - const testInput = useMemo( - () => createTestInputFunction(inputTests), - [inputTests], - ); + interfaces.forEach((iface) => { + if (iface === undefined) { + return; + } - const testInputToToggleSubmitDisabled: TestInputToToggleSubmitDisabled = - useCallback( - (options) => { - toggleSubmitDisabled?.call( - null, - testInput({ - isIgnoreOnCallbacks: true, - isTestAll: true, + const { networkInterfaceUUID } = iface; - ...options, - }), - ); + networkInterfaceInputMap[networkInterfaceUUID].isApplied = false; + }); + + testInputToToggleSubmitDisabled({ + excludeTestIdsRe: RegExp(inputUUID), + }); + setNetworkInputs([...networkInputs]); + setNetworkInterfaceInputMap((previous) => ({ + ...previous, + })); }, - [testInput, toggleSubmitDisabled], + [ + networkInputs, + networkInterfaceInputMap, + testInputToToggleSubmitDisabled, + ], ); - const clearNetworkInterfaceHeld = useCallback(() => { - setNetworkInterfaceHeld(undefined); - }, []); - const createNetwork = useCallback( - ({ - inputUUID = uuidv4(), - interfaces = [...INITIAL_IFACES], - ipAddress = '', - name = 'Unknown Network', - subnetMask = '', - type = '', - typeCount = 0, - }: Partial = {}) => { - networkInputs.unshift({ - inputUUID, - interfaces, - ipAddress, - name, - subnetMask, - type, - typeCount, - }); - - toggleSubmitDisabled?.call(null, false); - setNetworkInputs([...networkInputs]); - }, - [networkInputs, toggleSubmitDisabled], - ); - const removeNetwork = useCallback( - (networkIndex: number) => { - const [{ inputUUID, interfaces }] = networkInputs.splice(networkIndex, 1); - - interfaces.forEach((iface) => { - if (iface === undefined) { - return; + const getNetworkTypeCount: GetNetworkTypeCountFunction = useCallback( + ( + targetType: string, + { + inputs = networkInputs, + lastIndex = 0, + }: { + inputs?: NetworkInput[]; + lastIndex?: number; + } = {}, + ) => { + let count = 0; + + for (let index = inputs.length - 1; index >= lastIndex; index -= 1) { + if (inputs[index].type === targetType) { + count += 1; + } } - const { networkInterfaceUUID } = iface; - - networkInterfaceInputMap[networkInterfaceUUID].isApplied = false; - }); + return count; + }, + [networkInputs], + ); - testInputToToggleSubmitDisabled({ - excludeTestIdsRe: RegExp(inputUUID), - }); - setNetworkInputs([...networkInputs]); - setNetworkInterfaceInputMap((previous) => ({ - ...previous, - })); - }, - [networkInputs, networkInterfaceInputMap, testInputToToggleSubmitDisabled], - ); - const getNetworkTypeCount: GetNetworkTypeCountFunction = useCallback( - ( - targetType: string, - { - inputs = networkInputs, - lastIndex = 0, - }: { - inputs?: NetworkInput[]; - lastIndex?: number; - } = {}, - ) => { - let count = 0; - - for (let index = inputs.length - 1; index >= lastIndex; index -= 1) { - if (inputs[index].type === targetType) { - count += 1; - } + const createDropMouseUpHandler: + | (( + interfaces: (NetworkInterfaceOverviewMetadata | undefined)[], + interfaceIndex: number, + ) => MUIBoxProps['onMouseUp']) + | undefined = useMemo(() => { + if (networkInterfaceHeld === undefined) { + return undefined; } - return count; - }, - [networkInputs], - ); - - const createDropMouseUpHandler: - | (( - interfaces: (NetworkInterfaceOverviewMetadata | undefined)[], - interfaceIndex: number, - ) => MUIBoxProps['onMouseUp']) - | undefined = useMemo(() => { - if (networkInterfaceHeld === undefined) { - return undefined; - } + const { networkInterfaceUUID } = networkInterfaceHeld; - const { networkInterfaceUUID } = networkInterfaceHeld; - - return ( - interfaces: (NetworkInterfaceOverviewMetadata | undefined)[], - interfaceIndex: number, - ) => - () => { - const { networkInterfaceUUID: previousNetworkInterfaceUUID } = - interfaces[interfaceIndex] ?? {}; - - if ( - previousNetworkInterfaceUUID && - previousNetworkInterfaceUUID !== networkInterfaceUUID - ) { - networkInterfaceInputMap[previousNetworkInterfaceUUID].isApplied = - false; - } + return ( + interfaces: (NetworkInterfaceOverviewMetadata | undefined)[], + interfaceIndex: number, + ) => + () => { + const { networkInterfaceUUID: previousNetworkInterfaceUUID } = + interfaces[interfaceIndex] ?? {}; + + if ( + previousNetworkInterfaceUUID && + previousNetworkInterfaceUUID !== networkInterfaceUUID + ) { + networkInterfaceInputMap[previousNetworkInterfaceUUID].isApplied = + false; + } - interfaces[interfaceIndex] = networkInterfaceHeld; - networkInterfaceInputMap[networkInterfaceUUID].isApplied = true; - }; - }, [networkInterfaceHeld, networkInterfaceInputMap]); - const dragAreaDraggingSx: MUIBoxProps['sx'] = useMemo( - () => - networkInterfaceHeld ? { cursor: 'grabbing', userSelect: 'none' } : {}, - [networkInterfaceHeld], - ); - const floatingNetworkInterface: JSX.Element = useMemo(() => { - if (networkInterfaceHeld === undefined) { - return <>; - } + interfaces[interfaceIndex] = networkInterfaceHeld; + networkInterfaceInputMap[networkInterfaceUUID].isApplied = true; + }; + }, [networkInterfaceHeld, networkInterfaceInputMap]); + const dragAreaDraggingSx: MUIBoxProps['sx'] = useMemo( + () => + networkInterfaceHeld ? { cursor: 'grabbing', userSelect: 'none' } : {}, + [networkInterfaceHeld], + ); + const floatingNetworkInterface: JSX.Element = useMemo(() => { + if (networkInterfaceHeld === undefined) { + return <>; + } - const { x, y } = dragMousePosition; + const { x, y } = dragMousePosition; - return ( - + return ( + + ); + }, [dragMousePosition, networkInterfaceHeld]); + const handleDragAreaMouseLeave: MUIBoxProps['onMouseLeave'] = useMemo( + () => + networkInterfaceHeld + ? () => { + clearNetworkInterfaceHeld(); + } + : undefined, + [clearNetworkInterfaceHeld, networkInterfaceHeld], + ); + const handleDragAreaMouseMove: MUIBoxProps['onMouseMove'] = useMemo( + () => + networkInterfaceHeld + ? ({ currentTarget, nativeEvent: { clientX, clientY } }) => { + const { left, top } = currentTarget.getBoundingClientRect(); + + setDragMousePosition({ + x: clientX - left, + y: clientY - top, + }); + } + : undefined, + [networkInterfaceHeld], + ); + const handleDragAreaMouseUp: MUIBoxProps['onMouseUp'] = useMemo( + () => + networkInterfaceHeld + ? () => { + clearNetworkInterfaceHeld(); + } + : undefined, + [clearNetworkInterfaceHeld, networkInterfaceHeld], ); - }, [dragMousePosition, networkInterfaceHeld]); - const handleDragAreaMouseLeave: MUIBoxProps['onMouseLeave'] = useMemo( - () => - networkInterfaceHeld - ? () => { - clearNetworkInterfaceHeld(); - } - : undefined, - [clearNetworkInterfaceHeld, networkInterfaceHeld], - ); - const handleDragAreaMouseMove: MUIBoxProps['onMouseMove'] = useMemo( - () => - networkInterfaceHeld - ? ({ currentTarget, nativeEvent: { clientX, clientY } }) => { - const { left, top } = currentTarget.getBoundingClientRect(); - setDragMousePosition({ - x: clientX - left, - y: clientY - top, + useEffect(() => { + if ( + [ + Object.keys(networkInterfaceInputMap).length > 0, + expectHostDetail, + hostDetail, + readHostDetailRef.current, + dnsCSVInputRef.current, + gatewayInputRef.current, + ].every((condition) => Boolean(condition)) + ) { + readHostDetailRef.current = false; + + const { + dns: pDns, + gateway: pGateway, + gatewayInterface: pGatewayInterface, + networks: pNetworks, + } = hostDetail as APIHostDetail; + + dnsCSVInputRef.current.setValue?.call(null, pDns); + gatewayInputRef.current.setValue?.call(null, pGateway); + + const applied: string[] = []; + const inputs = Object.values(pNetworks).reduce( + (previous, { ip, link1Uuid, link2Uuid = '', subnetMask, type }) => { + const typeCount = + getNetworkTypeCount(type, { inputs: previous }) + 1; + const isRequired = requiredNetworks[type] === typeCount; + + const name = `${NETWORK_TYPES[type]} ${typeCount}`; + + applied.push(link1Uuid, link2Uuid); + + previous.push({ + inputUUID: uuidv4(), + interfaces: [ + networkInterfaceInputMap[link1Uuid]?.metadata, + networkInterfaceInputMap[link2Uuid]?.metadata, + ], + ipAddress: ip, + isRequired, + name, + subnetMask, + type, + typeCount, }); - } - : undefined, - [networkInterfaceHeld], - ); - const handleDragAreaMouseUp: MUIBoxProps['onMouseUp'] = useMemo( - () => - networkInterfaceHeld - ? () => { - clearNetworkInterfaceHeld(); - } - : undefined, - [clearNetworkInterfaceHeld, networkInterfaceHeld], - ); - useEffect(() => { - if ( - [ - Object.keys(networkInterfaceInputMap).length > 0, - expectHostDetail, - hostDetail, - readHostDetailRef.current, - dnsCSVInputRef.current, - gatewayInputRef.current, - ].every((condition) => Boolean(condition)) - ) { - readHostDetailRef.current = false; - - const { - dns: pDns, - gateway: pGateway, - gatewayInterface: pGatewayInterface, - networks: pNetworks, - } = hostDetail as APIHostDetail; - - dnsCSVInputRef.current.setValue?.call(null, pDns); - gatewayInputRef.current.setValue?.call(null, pGateway); - - const applied: string[] = []; - const inputs = Object.values(pNetworks).reduce( - (previous, { ip, link1Uuid, link2Uuid = '', subnetMask, type }) => { - const typeCount = getNetworkTypeCount(type, { inputs: previous }) + 1; - const isRequired = requiredNetworks[type] === typeCount; - - const name = `${NETWORK_TYPES[type]} ${typeCount}`; - - applied.push(link1Uuid, link2Uuid); - - previous.push({ - inputUUID: uuidv4(), - interfaces: [ - networkInterfaceInputMap[link1Uuid]?.metadata, - networkInterfaceInputMap[link2Uuid]?.metadata, - ], - ipAddress: ip, - isRequired, - name, - subnetMask, - type, - typeCount, - }); + return previous; + }, + [], + ); - return previous; - }, - [], - ); + setGatewayInterface(pGatewayInterface); - setGatewayInterface(pGatewayInterface); + setNetworkInterfaceInputMap((previous) => { + const result = { ...previous }; - setNetworkInterfaceInputMap((previous) => { - const result = { ...previous }; + applied.forEach((uuid) => { + if (result[uuid]) { + result[uuid].isApplied = true; + } + }); - applied.forEach((uuid) => { - if (result[uuid]) { - result[uuid].isApplied = true; - } + return result; }); - return result; - }); - - setNetworkInputs(inputs); - - testInputToToggleSubmitDisabled(); - } - }, [ - createNetwork, - expectHostDetail, - getNetworkTypeCount, - hostDetail, - networkInputs, - networkInterfaceInputMap, - requiredNetworks, - testInputToToggleSubmitDisabled, - ]); + setNetworkInputs(inputs); - useEffect(() => { - // Enable network mapping on component mount. - setMapNetwork(1); - - if (window) { - window.addEventListener( - 'beforeunload', - () => { - // Cannot use async request (i.e., axios) because they won't be guaranteed to complete. - const request = new XMLHttpRequest(); - - request.open('PUT', `${API_BASE_URL}/init/set-map-network`, false); - request.send(null); - }, - { once: true }, - ); - } - - return () => { - // Disable network mapping on component unmount. - setMapNetwork(0); - }; - }, [setMapNetwork]); + testInputToToggleSubmitDisabled(); + } + }, [ + createNetwork, + expectHostDetail, + getNetworkTypeCount, + hostDetail, + networkInputs, + networkInterfaceInputMap, + requiredNetworks, + testInputToToggleSubmitDisabled, + ]); + + useEffect(() => { + // Enable network mapping on component mount. + setMapNetwork(1); + + if (window) { + window.addEventListener( + 'beforeunload', + () => { + // Cannot use async request (i.e., axios) because they won't be guaranteed to complete. + const request = new XMLHttpRequest(); + + request.open('PUT', `${API_BASE_URL}/init/set-map-network`, false); + request.send(null); + }, + { once: true }, + ); + } - useImperativeHandle( - ref, - () => ({ - ...messageGroupRef.current, - get: () => ({ - dns: dnsCSVInputRef.current.getValue?.call(null), - gateway: gatewayInputRef.current.getValue?.call(null), - gatewayInterface, - networks: networkInputs.map( - ({ - inputUUID, - interfaces, - ipAddressInputRef, - name, - subnetMaskInputRef, - type, - typeCount, - }) => ({ - inputUUID, - interfaces, - ipAddress: ipAddressInputRef?.current.getValue?.call(null) ?? '', - name, - subnetMask: subnetMaskInputRef?.current.getValue?.call(null) ?? '', - type, - typeCount, - }), - ), + return () => { + // Disable network mapping on component unmount. + setMapNetwork(0); + }; + }, [setMapNetwork]); + + useImperativeHandle( + ref, + () => ({ + ...messageGroupRef.current, + get: () => ({ + dns: dnsCSVInputRef.current.getValue?.call(null), + gateway: gatewayInputRef.current.getValue?.call(null), + gatewayInterface, + networks: networkInputs.map( + ({ + inputUUID, + interfaces, + ipAddressInputRef, + name, + subnetMaskInputRef, + type, + typeCount, + }) => ({ + inputUUID, + interfaces, + ipAddress: ipAddressInputRef?.current.getValue?.call(null) ?? '', + name, + subnetMask: + subnetMaskInputRef?.current.getValue?.call(null) ?? '', + type, + typeCount, + }), + ), + }), }), - }), - [gatewayInterface, networkInputs], - ); - - const networkInputMinWidth = '13em'; - const networkInputWidth = '25%'; + [gatewayInterface, networkInputs], + ); - return isLoadingNetworkInterfaces ? ( - - ) : ( - { - const { left, top } = currentTarget.getBoundingClientRect(); + const networkInputMinWidth = '13em'; + const networkInputWidth = '25%'; - setDragMousePosition({ - x: clientX - left, - y: clientY - top, - }); - }} - onMouseLeave={handleDragAreaMouseLeave} - onMouseMove={handleDragAreaMouseMove} - onMouseUp={handleDragAreaMouseUp} - sx={{ position: 'relative', ...dragAreaDraggingSx }} - > - {floatingNetworkInterface} + return isLoadingNetworkInterfaces ? ( + + ) : ( { + const { left, top } = currentTarget.getBoundingClientRect(); - '& > :not(:first-child, :nth-child(3))': { - marginTop: '1em', - }, + setDragMousePosition({ + x: clientX - left, + y: clientY - top, + }); }} + onMouseLeave={handleDragAreaMouseLeave} + onMouseMove={handleDragAreaMouseMove} + onMouseUp={handleDragAreaMouseUp} + sx={{ position: 'relative', ...dragAreaDraggingSx }} > - { - setNetworkInterfaceHeld(row); - }, networkInterfaceInputMap)} - componentsProps={{ - row: { - onMouseDown: ({ - target: { - parentElement: { - dataset: { id: networkInterfaceUUID = undefined } = {}, + {floatingNetworkInterface} + :not(:first-child, :nth-child(3))': { + marginTop: '1em', + }, + }} + > + { + setNetworkInterfaceHeld(row); + }, networkInterfaceInputMap)} + componentsProps={{ + row: { + onMouseDown: ({ + target: { + parentElement: { + dataset: { id: networkInterfaceUUID = undefined } = {}, + } = {}, } = {}, - } = {}, - }: { - target?: { parentElement?: { dataset?: { id?: string } } }; - }) => { - if (networkInterfaceUUID) { - const { isApplied, metadata } = - networkInterfaceInputMap[networkInterfaceUUID]; - - if (!isApplied) { - setNetworkInterfaceHeld(metadata); + }: { + target?: { parentElement?: { dataset?: { id?: string } } }; + }) => { + if (networkInterfaceUUID) { + const { isApplied, metadata } = + networkInterfaceInputMap[networkInterfaceUUID]; + + if (!isApplied) { + setNetworkInterfaceHeld(metadata); + } } - } + }, }, - }, - }} - disableColumnMenu - disableSelectionOnClick - getRowClassName={({ row: { networkInterfaceUUID } }) => { - const { isApplied } = - networkInterfaceInputMap[networkInterfaceUUID] ?? false; - - let className = ''; - - if (!isApplied) { - className += ` ${CLASSES.ifaceNotApplied}`; - } - - return className; - }} - getRowId={({ networkInterfaceUUID }) => networkInterfaceUUID} - hideFooter - rows={networkInterfaces} - sx={{ - color: GREY, + }} + disableColumnMenu + disableSelectionOnClick + getRowClassName={({ row: { networkInterfaceUUID } }) => { + const { isApplied } = + networkInterfaceInputMap[networkInterfaceUUID] ?? false; - [`& .${muiIconButtonClasses.root}`]: { - color: 'inherit', - }, + let className = ''; - [`& .${muiGridClasses.cell}:focus`]: { - outline: 'none', - }, + if (!isApplied) { + className += ` ${CLASSES.ifaceNotApplied}`; + } - [`& .${muiGridClasses.row}.${CLASSES.ifaceNotApplied}:hover`]: { - cursor: 'grab', + return className; + }} + getRowId={({ networkInterfaceUUID }) => networkInterfaceUUID} + hideFooter + rows={networkInterfaces} + sx={{ + color: GREY, - [`& .${muiGridClasses.cell} p`]: { - cursor: 'auto', + [`& .${muiIconButtonClasses.root}`]: { + color: 'inherit', }, - }, - }} - /> - {!isLoadingHostDetail && ( - :first-child': { - alignSelf: 'start', - marginTop: '.7em', + + [`& .${muiGridClasses.cell}:focus`]: { + outline: 'none', }, - '& > :last-child': { - flexGrow: 1, + [`& .${muiGridClasses.row}.${CLASSES.ifaceNotApplied}:hover`]: { + cursor: 'grab', + + [`& .${muiGridClasses.cell} p`]: { + cursor: 'auto', + }, }, }} - > - + {!isLoadingHostDetail && ( + div': { - marginBottom: '.8em', - marginTop: '.4em', - minWidth: networkInputMinWidth, - width: networkInputWidth, + '& > :first-child': { + alignSelf: 'start', + marginTop: '.7em', }, - '& > :not(:first-child)': { - marginLeft: '1em', + '& > :last-child': { + flexGrow: 1, }, }} > - {networkInputs.map((networkInput, networkIndex) => { - const { inputUUID } = networkInput; - - return ( - - ); - })} - - - )} - :not(button)': { - minWidth: networkInputMinWidth, - width: { sm: networkInputWidth }, - }, - }} - > - { - createNetwork(); - }} - > - - - { - testInput({ inputs: { [IT_IDS.gateway]: { value } } }); + div': { + marginBottom: '.8em', + marginTop: '.4em', + minWidth: networkInputMinWidth, + width: networkInputWidth, }, - }} - inputLabelProps={{ isNotifyRequired: true }} - onChange={({ target: { value } }) => { - testInputToToggleSubmitDisabled({ - inputs: { [IT_IDS.gateway]: { value } }, - }); - setGatewayInputMessage(); - }} - label="Gateway" - /> - } - ref={gatewayInputRef} - /> - { - testInput({ inputs: { [IT_IDS.dnsCSV]: { value } } }); + + '& > :not(:first-child)': { + marginLeft: '1em', }, }} - inputLabelProps={{ isNotifyRequired: true }} - onChange={({ target: { value } }) => { - testInputToToggleSubmitDisabled({ - inputs: { [IT_IDS.dnsCSV]: { value } }, - }); - setDnsInputMessage(); - }} - label="Domain name server(s)" - /> - } - ref={dnsCSVInputRef} + > + {networkInputs.map((networkInput, networkIndex) => { + const { inputUUID } = networkInput; + + return ( + + ); + })} + + + )} + :not(button)': { + minWidth: networkInputMinWidth, + width: { sm: networkInputWidth }, + }, + }} + > + { + createNetwork(); + }} + > + + + { + testInput({ inputs: { [IT_IDS.gateway]: { value } } }); + }, + }} + inputLabelProps={{ isNotifyRequired: true }} + onChange={({ target: { value } }) => { + testInputToToggleSubmitDisabled({ + inputs: { [IT_IDS.gateway]: { value } }, + }); + setGatewayInputMessage(); + }} + label="Gateway" + /> + } + ref={gatewayInputRef} + /> + { + testInput({ inputs: { [IT_IDS.dnsCSV]: { value } } }); + }, + }} + inputLabelProps={{ isNotifyRequired: true }} + onChange={({ target: { value } }) => { + testInputToToggleSubmitDisabled({ + inputs: { [IT_IDS.dnsCSV]: { value } }, + }); + setDnsInputMessage(); + }} + label="Domain name server(s)" + /> + } + ref={dnsCSVInputRef} + /> + + - - + - - ); -}); + ); + }, +); NetworkInitForm.defaultProps = { expectHostDetail: false, hostDetail: undefined, + hostSequence: undefined, toggleSubmitDisabled: undefined, }; NetworkInitForm.displayName = 'NetworkInitForm'; diff --git a/striker-ui/components/StrikerInitForm.tsx b/striker-ui/components/StrikerInitForm.tsx index 33e88150..83fdfb81 100644 --- a/striker-ui/components/StrikerInitForm.tsx +++ b/striker-ui/components/StrikerInitForm.tsx @@ -48,6 +48,7 @@ const StrikerInitForm: FC = () => { const [isNetworkInitFormValid, setIsNetworkInitFormValid] = useState(false); const [isSubmittingForm, setIsSubmittingForm] = useState(false); + const [hostNumber, setHostNumber] = useState(); const [hostDetail, setHostDetail] = useProtectedState< APIHostDetail | undefined @@ -147,6 +148,9 @@ const StrikerInitForm: FC = () => { { + setHostNumber(value); + }} ref={generalInitFormRef} toggleSubmitDisabled={(testResult) => { if (testResult !== isGeneralInitFormValid) { @@ -158,6 +162,7 @@ const StrikerInitForm: FC = () => { { if (testResult !== isNetworkInitFormValid) {