import { Box as MUIBox, BoxProps as MUIBoxProps, iconButtonClasses as muiIconButtonClasses, useMediaQuery, useTheme, } from '@mui/material'; import { Add as MUIAddIcon, Check as MUICheckIcon, Close as MUICloseIcon, DragHandle as MUIDragHandleIcon, } from '@mui/icons-material'; import { DataGrid as MUIDataGrid, DataGridProps as MUIDataGridProps, gridClasses as muiGridClasses, } from '@mui/x-data-grid'; import { Netmask } from 'netmask'; import { Dispatch, FC, forwardRef, MutableRefObject, SetStateAction, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react'; import { v4 as uuidv4 } from 'uuid'; import API_BASE_URL from '../lib/consts/API_BASE_URL'; import { BLUE, GREY } from '../lib/consts/DEFAULT_THEME'; import NETWORK_TYPES from '../lib/consts/NETWORK_TYPES'; import { REP_IPV4, REP_IPV4_CSV } from '../lib/consts/REG_EXP_PATTERNS'; import BriefNetworkInterface from './BriefNetworkInterface'; import Decorator from './Decorator'; import DropArea from './DropArea'; import FlexBox from './FlexBox'; import IconButton from './IconButton'; import InputWithRef, { InputForwardedRefContent } from './InputWithRef'; import { Message } from './MessageBox'; import MessageGroup, { MessageGroupForwardedRefContent } from './MessageGroup'; import OutlinedInputWithLabel from './OutlinedInputWithLabel'; import { InnerPanel, InnerPanelHeader } from './Panels'; import periodicFetch from '../lib/fetchers/periodicFetch'; import SelectWithLabel from './SelectWithLabel'; import setMapNetwork from '../lib/setMapNetwork'; import Spinner from './Spinner'; import { createTestInputFunction, testNotBlank } from '../lib/test_input'; import { BodyText, MonoText, SmallText } from './Text'; type NetworkInput = { inputUUID: string; interfaces: (NetworkInterfaceOverviewMetadata | undefined)[]; ipAddress: string; ipAddressInputRef?: MutableRefObject>; isRequired?: boolean; name?: string; subnetMask: string; subnetMaskInputRef?: MutableRefObject>; type: string; typeCount: number; }; type NetworkInterfaceInputMap = Record< string, { metadata: NetworkInterfaceOverviewMetadata; isApplied?: boolean; } >; type NetworkInitFormValues = { dns?: string; gateway?: string; gatewayInterface?: string; networks: Omit[]; }; type NetworkInitFormForwardedRefContent = MessageGroupForwardedRefContent & { get?: () => NetworkInitFormValues; }; type GetNetworkTypeCountFunction = ( targetType: string, options?: { inputs?: NetworkInput[] | undefined; lastIndex?: number | undefined; }, ) => number; type TestInputToToggleSubmitDisabled = ( options?: Pick< TestInputFunctionOptions, 'excludeTestIds' | 'excludeTestIdsRe' | 'inputs' | 'isContinueOnFailure' >, ) => void; const CLASS_PREFIX = 'NetworkInitForm'; const CLASSES = { ifaceNotApplied: `${CLASS_PREFIX}-network-interface-not-applied`, }; const INITIAL_IFACES = [undefined, undefined]; const MSG_ID_API = 'api'; const MAX_INTERFACES_PER_NETWORK = 2; const IT_IDS = { dnsCSV: 'dns', gateway: 'gateway', networkInterfaces: (prefix: string) => `${prefix}Interface`, networkIPAddress: (prefix: string) => `${prefix}IPAddress`, networkName: (prefix: string) => `${prefix}Name`, networkSubnetMask: (prefix: string) => `${prefix}SubnetMask`, networkSubnetConflict: (prefix: string) => `${prefix}NetworkSubnetConflict`, }; const NETWORK_INTERFACE_TEMPLATE = Array.from( { length: MAX_INTERFACES_PER_NETWORK }, (unused, index) => index + 1, ); const MAP_TO_NETWORK_TYPE_DEFAULTS: Record< string, { ip: (sequence: number | string, postfix?: string) => string; mask: string } > = { bcn: { ip: (sequence, postfix = '') => `10.20${sequence}.${postfix}`, mask: '255.255.0.0', }, ifn: { ip: () => '', mask: '' }, mn: { ip: () => '10.199.', mask: '255.255.0.0' }, sn: { ip: (sequence, postfix = '') => `10.10${sequence}.${postfix}`, mask: '255.255.0.0', }, }; 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, ...eventArgs: Parameters> ) => void, networkInterfaceInputMap: NetworkInterfaceInputMap, ): MUIDataGridProps['columns'] => [ { align: 'center', field: '', renderCell: ({ row }) => { const { isApplied } = networkInterfaceInputMap[row.networkInterfaceUUID] ?? false; let cursor = 'grab'; let handleMouseDown: MUIBoxProps['onMouseDown'] = (...eventArgs) => { handleDragMouseDown(row, ...eventArgs); }; let icon = ; if (isApplied) { cursor = 'auto'; handleMouseDown = undefined; icon = ; } return ( {icon} ); }, sortable: false, width: 1, }, { field: 'networkInterfaceName', flex: 1, headerName: 'Name', renderCell: ({ row: { networkInterfaceState } = {}, value }) => ( :not(:first-child)': { marginLeft: '.5em' }, }} > {value} ), }, { field: 'networkInterfaceMACAddress', flex: 1, headerName: 'MAC', renderCell: ({ value }) => , }, { field: 'networkInterfaceState', flex: 1, headerName: 'State', renderCell: ({ value }) => { const state = String(value); return ( ); }, }, { field: 'networkInterfaceSpeed', flex: 1, headerName: 'Speed', renderCell: ({ value }) => ( ), }, { field: 'networkInterfaceOrder', flex: 1, headerName: 'Order', }, ]; const NetworkForm: FC<{ allowMigrationNetwork?: boolean; createDropMouseUpHandler?: ( interfaces: (NetworkInterfaceOverviewMetadata | undefined)[], interfaceIndex: number, ) => MUIBoxProps['onMouseUp']; getNetworkTypeCount: GetNetworkTypeCountFunction; hostDetail?: Partial>; networkIndex: number; networkInput: NetworkInput; networkInterfaceCount: number; networkInterfaceInputMap: NetworkInterfaceInputMap; removeNetwork: (index: number) => void; setMessageRe: (re: RegExp, message?: Message) => void; setNetworkInputs: Dispatch>; setNetworkInterfaceInputMap: Dispatch< SetStateAction >; testInput: (options?: TestInputFunctionOptions) => boolean; testInputToToggleSubmitDisabled: TestInputToToggleSubmitDisabled; }> = ({ allowMigrationNetwork, createDropMouseUpHandler, getNetworkTypeCount, hostDetail: { hostType, sequence } = {}, networkIndex, networkInput, networkInterfaceCount, networkInterfaceInputMap, removeNetwork, setMessageRe, setNetworkInputs, setNetworkInterfaceInputMap, testInput, testInputToToggleSubmitDisabled, }) => { const theme = useTheme(); const breakpointMedium = useMediaQuery(theme.breakpoints.up('md')); const breakpointLarge = useMediaQuery(theme.breakpoints.up('lg')); const ipAddressInputRef = useRef>({}); const subnetMaskInputRef = useRef>({}); const { inputUUID, interfaces, ipAddress, isRequired, subnetMask, type, typeCount, } = networkInput; const inputTestPrefix = useMemo( () => createInputTestPrefix(inputUUID), [inputUUID], ); const interfacesInputTestId = useMemo( () => IT_IDS.networkInterfaces(inputTestPrefix), [inputTestPrefix], ); const ipAddressInputTestId = useMemo( () => IT_IDS.networkIPAddress(inputTestPrefix), [inputTestPrefix], ); const subnetMaskInputTestId = useMemo( () => IT_IDS.networkSubnetMask(inputTestPrefix), [inputTestPrefix], ); const subnetConflictInputMessageKeyPrefix = useMemo( () => IT_IDS.networkSubnetConflict(inputTestPrefix), [inputTestPrefix], ); const isNode = useMemo(() => hostType === 'node', [hostType]); const netIfTemplate = useMemo( () => !isNode && networkInterfaceCount <= 2 ? [1] : NETWORK_INTERFACE_TEMPLATE, [isNode, networkInterfaceCount], ); const netTypeList = useMemo(() => { const { bcn, ifn, mn, sn } = NETWORK_TYPES; return isNode && networkInterfaceCount >= 8 && (allowMigrationNetwork || type === 'mn') ? { bcn, ifn, mn, sn } : { bcn, ifn, sn }; }, [allowMigrationNetwork, isNode, networkInterfaceCount, type]); const setIpAndMask = useCallback( (nInput: NetworkInput, ip: string, mask: string) => { const { current: { getIsChangedByUser: getIpModded, setValue: setIp }, } = ipAddressInputRef; const { current: { getIsChangedByUser: getMaskModded, setValue: setMask }, } = subnetMaskInputRef; if (!getIpModded?.call(null)) { nInput.ipAddress = ip; setIp?.call(null, ip); } if (!getMaskModded?.call(null)) { nInput.subnetMask = mask; setMask?.call(null, mask); } }, [], ); 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; if (ipRef !== ipAddressInputRef || maskRef !== subnetMaskInputRef) { networkInput.ipAddressInputRef = ipAddressInputRef; networkInput.subnetMaskInputRef = subnetMaskInputRef; setNetworkInputs((previous) => [...previous]); } }, [networkInput, setNetworkInputs]); return ( { let count = getNetworkTypeCount(networkType, { lastIndex: networkIndex, }); if (networkType !== type) { count += 1; } const displayValue = `${networkTypeName} ${count}`; return { value: networkType, displayValue }; }, )} selectProps={{ onChange: ({ target: { value } }) => { const networkType = String(value); networkInput.type = networkType; const networkTypeCount = getNetworkTypeCount(networkType, { lastIndex: networkIndex, }); networkInput.typeCount = networkTypeCount; networkInput.name = `${NETWORK_TYPES[networkType]} ${networkTypeCount}`; const networkTypeDefaults = MAP_TO_NETWORK_TYPE_DEFAULTS[networkType]; if (networkTypeDefaults) { const { ip, mask } = networkTypeDefaults; let postfix: string | undefined; if (hostType === 'striker' && networkType === 'bcn') { postfix = '4.'; } setIpAndMask(networkInput, ip(networkTypeCount, postfix), mask); } setNetworkInputs((previous) => [...previous]); }, renderValue: breakpointLarge ? undefined : (value) => `${String(value).toUpperCase()} ${typeCount}`, value: type, }} /> {!isRequired && ( { removeNetwork(networkIndex); }} sx={{ padding: '.2em', position: 'absolute', right: '-9px', top: '-4px', }} > )} :not(:first-child)': { marginTop: '1em', }, }} > {netIfTemplate.map((linkNumber) => { const linkName = `Link ${linkNumber}`; const networkInterfaceIndex = linkNumber - 1; const networkInterface = interfaces[networkInterfaceIndex]; const { networkInterfaceUUID = '' } = networkInterface ?? {}; const emptyDropAreaContent = breakpointMedium ? ( ) : ( ); return ( :not(:first-child)': { marginLeft: '1em', }, '& > :last-child': { flexGrow: 1, }, }} > { createDropMouseUpHandler ?.call(null, interfaces, networkInterfaceIndex) ?.call(null, ...args); testInputToToggleSubmitDisabled({ inputs: { [interfacesInputTestId]: { isIgnoreOnCallbacks: false, }, }, isContinueOnFailure: true, }); }} > {networkInterface ? ( { interfaces[networkInterfaceIndex] = undefined; networkInterfaceInputMap[networkInterfaceUUID].isApplied = false; setNetworkInterfaceInputMap((previous) => ({ ...previous, })); testInputToToggleSubmitDisabled({ inputs: { [interfacesInputTestId]: { isIgnoreOnCallbacks: false, }, }, isContinueOnFailure: true, }); }} /> ) : ( emptyDropAreaContent )} ); })} { testInput({ inputs: { [ipAddressInputTestId]: { value } } }); }, }} inputLabelProps={{ isNotifyRequired: true }} label="IP address" onChange={({ target: { value } }) => { testInputToToggleSubmitDisabled({ inputs: { [ipAddressInputTestId]: { value } }, }); setMessageRe( RegExp( `(?:^(?:${ipAddressInputTestId}|${subnetConflictInputMessageKeyPrefix})|${inputUUID}$)`, ), ); }} value={ipAddress} /> } ref={ipAddressInputRef} /> { testInput({ inputs: { [subnetMaskInputTestId]: { value } } }); }, }} inputLabelProps={{ isNotifyRequired: true }} label="Subnet mask" onChange={({ target: { value } }) => { testInputToToggleSubmitDisabled({ inputs: { [subnetMaskInputTestId]: { value } }, }); setMessageRe( RegExp( `(?:^(?:${subnetMaskInputTestId}|${subnetConflictInputMessageKeyPrefix})|${inputUUID}$)`, ), ); }} value={subnetMask} /> } ref={subnetMaskInputRef} /> ); }; NetworkForm.defaultProps = { allowMigrationNetwork: true, createDropMouseUpHandler: undefined, hostDetail: undefined, }; const NetworkInitForm = forwardRef< NetworkInitFormForwardedRefContent, { expectHostDetail?: boolean; hostDetail?: APIHostDetail; hostSequence?: string; toggleSubmitDisabled?: (testResult: boolean) => void; } >( ( { 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 initRequiredNetworks: NetworkInput[] = useMemo(() => { const result: NetworkInput[] = []; if (hostType === 'striker') { const ipAddress = sequence ? `10.201.4.${sequence}` : '10.201.4.'; result.push( createNetworkInput({ ipAddress, isRequired: true, subnetMask: '255.255.0.0', type: 'bcn', typeCount: 1, }), createNetworkInput({ isRequired: true, type: 'ifn', typeCount: 1, }), ); return result; } 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; }, [hostType, sequence]); const requiredNetworks = useMemo>>( () => hostType === 'node' ? { bcn: 1, ifn: 1, sn: 1 } : { bcn: 1, ifn: 1 }, [hostType], ); 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; result[networkInterfaceUUID] = networkInterfaceInputMap[ networkInterfaceUUID ] ?? { metadata }; return result; }, {}, ); setNetworkInterfaceInputMap(map); }, }, ); 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], ); /** * Allow user to add migration network only if none exists. */ const allowMigrationNetwork: boolean = useMemo( () => networkInputs.every(({ type }) => type !== 'mn'), [networkInputs], ); 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; } const otherIP = ipAddressInputRef?.current.getValue?.call(null); const otherMask = subnetMaskInputRef?.current.getValue?.call(null); let isMatch = false; try { const otherSubnet = new Netmask(`${otherIP}/${otherMask}`); isMatch = match(otherSubnet, { b: subnet, bIP: ip }); // 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 isNegateMatch ? !isMatch : isMatch; }); }, [networkInputs], ); const handleSetMapNetworkError = useCallback( (msg: Message): void => { setMessage(MSG_ID_API, msg); }, [setMessage], ); 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; } setGatewayInterface(`${type}${typeCount}`); }, }); if (!isIPInOneNetwork) { setGatewayInputMessage({ children: "Gateway must be in one network's subnet.", }); } return isIPInOneNetwork && isDistinctIP; }, }, { test: testNotBlank }, ], }, }; 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.`, }); }, onMiss: ({ inputUUID: otherUUID }) => { setNetworkSubnetConflictInputMessage(inputUUID, otherUUID); }, skipUUID: inputUUID, }); tests[inputTestIDIfaces] = { defaults: { getCompare: () => interfaces.map((iface) => iface !== undefined), onSuccess: () => { setNetworkIfacesInputMessage(); }, }, 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(); }, }, 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, ); interfaces.forEach((iface) => { if (iface === undefined) { return; } const { networkInterfaceUUID } = iface; networkInterfaceInputMap[networkInterfaceUUID].isApplied = false; }); 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; } } return count; }, [networkInputs], ); const createDropMouseUpHandler: | (( interfaces: (NetworkInterfaceOverviewMetadata | undefined)[], interfaceIndex: number, ) => MUIBoxProps['onMouseUp']) | undefined = useMemo(() => { if (networkInterfaceHeld === undefined) { return undefined; } const { networkInterfaceUUID } = networkInterfaceHeld; 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 <>; } const { x, y } = dragMousePosition; 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], ); 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; if ( [pDns, pGateway, pGatewayInterface, pNetworks].some( (condition) => !condition, ) ) { return; } dnsCSVInputRef.current.setValue?.call(null, pDns); gatewayInputRef.current.setValue?.call(null, pGateway); const applied: string[] = []; const inputs = Object.values(pNetworks as APIHostNetworkList).reduce< NetworkInput[] >((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; }, []); setGatewayInterface(pGatewayInterface as string); setNetworkInterfaceInputMap((previous) => { const result = { ...previous }; applied.forEach((uuid) => { if (result[uuid]) { result[uuid].isApplied = true; } }); return result; }); setNetworkInputs(inputs); testInputToToggleSubmitDisabled(); } }, [ expectHostDetail, getNetworkTypeCount, hostDetail, networkInterfaceInputMap, requiredNetworks, testInputToToggleSubmitDisabled, ]); useEffect(() => { // Enable network mapping on component mount. setMapNetwork(1, handleSetMapNetworkError); 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, handleSetMapNetworkError); }; }, [handleSetMapNetworkError]); 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%'; return isLoadingNetworkInterfaces ? ( ) : ( { const { left, top } = currentTarget.getBoundingClientRect(); setDragMousePosition({ x: clientX - left, y: clientY - top, }); }} onMouseLeave={handleDragAreaMouseLeave} onMouseMove={handleDragAreaMouseMove} onMouseUp={handleDragAreaMouseUp} sx={{ position: 'relative', ...dragAreaDraggingSx }} > {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); } } }, }, }} disableColumnMenu disableSelectionOnClick getRowClassName={({ row: { networkInterfaceUUID } }) => { const { isApplied } = networkInterfaceInputMap[networkInterfaceUUID] ?? false; let className = ''; if (!isApplied) { className += ` ${CLASSES.ifaceNotApplied}`; } return className; }} getRowId={({ networkInterfaceUUID }) => networkInterfaceUUID} hideFooter initialState={{ sorting: { sortModel: [{ field: 'networkInterfaceName', sort: 'asc' }], }, }} rows={networkInterfaces} sx={{ color: GREY, [`& .${muiIconButtonClasses.root}`]: { color: 'inherit', }, [`& .${muiGridClasses.cell}:focus`]: { outline: 'none', }, [`& .${muiGridClasses.row}.${CLASSES.ifaceNotApplied}:hover`]: { cursor: 'grab', [`& .${muiGridClasses.cell} p`]: { cursor: 'auto', }, }, }} /> {!isLoadingHostDetail && ( :first-child': { alignSelf: 'start', marginTop: '.7em', }, '& > :last-child': { flexGrow: 1, }, }} > div': { marginBottom: '.8em', marginTop: '.4em', minWidth: networkInputMinWidth, width: networkInputWidth, }, '& > :not(:first-child)': { marginLeft: '1em', }, }} > {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'; export type { NetworkInitFormForwardedRefContent, NetworkInitFormValues, NetworkInput, NetworkInterfaceInputMap, }; export default NetworkInitForm;