import { Netmask } from 'netmask'; import { ReactElement, useCallback, useMemo } from 'react'; import { v4 as uuidv4 } from 'uuid'; import NETWORK_TYPES from '../../lib/consts/NETWORK_TYPES'; import AnNetworkInputGroup from './AnNetworkInputGroup'; import buildObjectStateSetterCallback from '../../lib/buildObjectStateSetterCallback'; import Grid from '../Grid'; import IconButton from '../IconButton'; import InputWithRef from '../InputWithRef'; import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; import { buildIpCsvTestBatch, buildNumberTestBatch, } from '../../lib/test_input'; const INPUT_ID_PREFIX_AN_NETWORK_CONFIG = 'an-network-config-input'; const INPUT_CELL_ID_PREFIX_ANC = `${INPUT_ID_PREFIX_AN_NETWORK_CONFIG}-cell`; const INPUT_ID_ANC_DNS = `${INPUT_ID_PREFIX_AN_NETWORK_CONFIG}-dns`; const INPUT_ID_ANC_MTU = `${INPUT_ID_PREFIX_AN_NETWORK_CONFIG}-mtu`; const INPUT_ID_ANC_NTP = `${INPUT_ID_PREFIX_AN_NETWORK_CONFIG}-ntp`; const INPUT_LABEL_ANC_DNS = 'DNS'; const INPUT_LABEL_ANC_MTU = 'MTU'; const INPUT_LABEL_ANC_NTP = 'NTP'; const DEFAULT_DNS_CSV = '8.8.8.8,8.8.4.4'; const NETWORK_TYPE_ENTRIES = Object.entries(NETWORK_TYPES); const MAP_TO_NETWORK_DEFAULTS: Record = { bcn: { base: '10.201.0.0', mask: '255.255.0.0' }, mn: { base: '10.199.0.0', mask: '255.255.0.0' }, sn: { base: '10.101.0.0', mask: '255.255.0.0' }, }; const assertIfn = (type: string) => type === 'ifn'; const assertMn = (type: string) => type === 'mn'; const guessNetworkMinIp = ({ entries, type, }: { entries: [string, ManifestNetwork][]; type: string; }): { base?: string; mask?: string } => { const last = entries .filter(([, { networkType }]) => networkType === type) .sort(([, { networkNumber: a }], [, { networkNumber: b }]) => a > b ? 1 : -1, ) .pop(); if (!last) { return MAP_TO_NETWORK_DEFAULTS[type] ?? {}; } const [, { networkMinIp, networkSubnetMask }] = last; try { const block = new Netmask(`${networkMinIp}/${networkSubnetMask}`); const { base, mask } = block.next(); return { base, mask }; } catch (error) { return {}; } }; const AnNetworkConfigInputGroup = < M extends MapToInputTestID & { [K in | typeof INPUT_ID_ANC_DNS | typeof INPUT_ID_ANC_MTU | typeof INPUT_ID_ANC_NTP]: string; }, >({ formUtils, networkListEntries, previous: { dnsCsv: previousDnsCsv = DEFAULT_DNS_CSV, mtu: previousMtu, ntpCsv: previousNtpCsv, } = {}, setNetworkList, }: AnNetworkConfigInputGroupProps): ReactElement => { const { buildFinishInputTestBatchFunction, buildInputFirstRenderFunction, setMessage, setMessageRe, } = formUtils; const getNetworkNumber = useCallback( ( type: string, { input = networkListEntries, end = networkListEntries.length, }: { input?: Array<[string, ManifestNetwork]>; end?: number; } = {}, ) => { const limit = end - 1; let netNum = 0; input.every(([, { networkType }], networkIndex) => { if (networkType === type) { netNum += 1; } return networkIndex < limit; }); return netNum; }, [networkListEntries], ); const networkTypeOptions = useMemo( () => NETWORK_TYPE_ENTRIES.map(([key, value]) => ({ displayValue: value, value: key, })), [], ); const buildNetwork = useCallback( ({ networkMinIp = '', networkSubnetMask = '', networkType = 'ifn', // Params that depend on others. networkGateway = assertIfn(networkType) ? '' : undefined, networkNumber = getNetworkNumber(networkType) + 1, }: Partial = {}): { network: ManifestNetwork; networkId: string; } => { const { base = networkMinIp, mask = networkSubnetMask } = guessNetworkMinIp({ entries: networkListEntries, type: networkType, }); return { network: { networkGateway, networkMinIp: base, networkNumber, networkSubnetMask: mask, networkType, }, networkId: uuidv4(), }; }, [getNetworkNumber, networkListEntries], ); const setNetwork = useCallback( (key: string, value?: ManifestNetwork) => setNetworkList(buildObjectStateSetterCallback(key, value)), [setNetworkList], ); const setNetworkProp = useCallback(

( nkey: string, pkey: P, value: ManifestNetwork[P], ) => setNetworkList((previous) => { const nyu = { ...previous }; const { [nkey]: nw } = nyu; if (nw) { nw[pkey] = value; } return nyu; }), [setNetworkList], ); const handleNetworkTypeChange = useCallback( ( { networkId: targetId, networkType: previousType }, { target: { value } }, ) => { const newType = String(value); let isIdMatch = false; let newTypeNumber = 0; const newList = networkListEntries.reduce( (previous, [networkId, networkValue]) => { const { networkNumber: initnn, networkType: initnt, networkMinIp: initbase, networkSubnetMask: initmask, ...restNetworkValue } = networkValue; let networkNumber = initnn; let networkType = initnt; if (networkId === targetId) { isIdMatch = true; networkType = newType; setMessageRe(RegExp(networkId)); } const isTypeMatch = networkType === newType; if (isTypeMatch) { newTypeNumber += 1; } if (isIdMatch) { if (isTypeMatch) { networkNumber = newTypeNumber; } else if (networkType === previousType) { networkNumber -= 1; } const { base: networkMinIp = initbase, mask: networkSubnetMask = initmask, } = guessNetworkMinIp({ entries: networkListEntries, type: networkType, }); previous[networkId] = { ...restNetworkValue, networkMinIp, networkSubnetMask, networkNumber, networkType, }; } else { previous[networkId] = networkValue; } return previous; }, {}, ); setNetworkList(newList); }, [networkListEntries, setMessageRe, setNetworkList], ); const handleNetworkRemove = useCallback( ({ networkId: rmId, networkType: rmType }) => { let isIdMatch = false; let networkNumber = 0; const newList = networkListEntries.reduce( (previous, [networkId, networkValue]) => { if (networkId === rmId) { isIdMatch = true; } else { const { networkType } = networkValue; if (networkType === rmType) { networkNumber += 1; } previous[networkId] = isIdMatch ? { ...networkValue, networkNumber } : networkValue; } return previous; }, {}, ); setNetworkList(newList); }, [networkListEntries, setNetworkList], ); const networksGridLayout = useMemo(() => { let result: GridLayout = {}; result = networkListEntries.reduce( ( previous, [ networkId, { networkGateway, networkMinIp, networkNumber, networkSubnetMask, networkType, }, ], ) => { const cellId = `${INPUT_CELL_ID_PREFIX_ANC}-${networkId}`; const isFirstNetwork = networkNumber === 1; const isIfn = assertIfn(networkType); const isMn = assertMn(networkType); const isOptional = isMn || !isFirstNetwork; previous[cellId] = { children: ( setNetworkProp(nid, 'networkMinIp', value)} onNetworkSubnetMaskChange={( { networkId: nid }, { target: { value } }, ) => setNetworkProp(nid, 'networkSubnetMask', value)} onNetworkTypeChange={handleNetworkTypeChange} previous={{ gateway: networkGateway, minIp: networkMinIp, subnetMask: networkSubnetMask, }} readonlyNetworkName={!isOptional} showCloseButton={isOptional} showGateway={isIfn} /> ), md: 3, sm: 2, }; return previous; }, result, ); return result; }, [ networkListEntries, formUtils, networkTypeOptions, handleNetworkRemove, handleNetworkTypeChange, setNetworkProp, ]); return ( { const { network: newNet, networkId: newNetId } = buildNetwork(); setNetwork(newNetId, newNet); }} /> ), display: 'flex', justifyContent: 'center', md: 3, sm: 2, }, 'an-network-config-input-cell-dns': { children: ( } inputTestBatch={buildIpCsvTestBatch( INPUT_LABEL_ANC_DNS, () => { setMessage(INPUT_ID_ANC_DNS); }, { onFinishBatch: buildFinishInputTestBatchFunction(INPUT_ID_ANC_DNS), }, (message) => { setMessage(INPUT_ID_ANC_DNS, { children: message }); }, )} onFirstRender={buildInputFirstRenderFunction(INPUT_ID_ANC_DNS)} required /> ), }, 'an-network-config-input-cell-ntp': { children: ( } inputTestBatch={buildIpCsvTestBatch( INPUT_LABEL_ANC_NTP, () => { setMessage(INPUT_ID_ANC_NTP); }, { onFinishBatch: buildFinishInputTestBatchFunction(INPUT_ID_ANC_NTP), }, (message) => { setMessage(INPUT_ID_ANC_NTP, { children: message }); }, )} onFirstRender={buildInputFirstRenderFunction(INPUT_ID_ANC_NTP)} /> ), }, 'an-network-config-input-cell-mtu': { children: ( } inputTestBatch={buildNumberTestBatch( INPUT_LABEL_ANC_MTU, () => { setMessage(INPUT_ID_ANC_MTU); }, { onFinishBatch: buildFinishInputTestBatchFunction(INPUT_ID_ANC_MTU), }, (message) => { setMessage(INPUT_ID_ANC_MTU, { children: message }); }, )} onFirstRender={buildInputFirstRenderFunction(INPUT_ID_ANC_MTU)} valueType="number" /> ), }, }} spacing="1em" /> ); }; export { INPUT_ID_ANC_DNS, INPUT_ID_ANC_MTU, INPUT_ID_ANC_NTP }; export default AnNetworkConfigInputGroup;