commit
bdbba0d59d
68 changed files with 4689 additions and 218 deletions
@ -0,0 +1,20 @@ |
||||
export const getEntityName = (id: string) => id.replace(/\d*$/, ''); |
||||
|
||||
export const getEntityNumber = (id: string) => |
||||
Number.parseInt(id.replace(/^[^\d]*/, '')); |
||||
|
||||
export const getEntityParts = (id: string) => { |
||||
let name = ''; |
||||
let number = NaN; |
||||
|
||||
const matchResult = id.match(/^([^\d]*)(\d*)$/); |
||||
|
||||
if (matchResult) { |
||||
const parts = matchResult; |
||||
|
||||
name = parts[1]; |
||||
number = Number.parseInt(parts[2]); |
||||
} |
||||
|
||||
return { name, number }; |
||||
}; |
@ -0,0 +1,8 @@ |
||||
export const getHostNameDomain = (hostName: string) => |
||||
hostName.replace(/^.*?[.]/, ''); |
||||
|
||||
export const getHostNamePrefix = (hostName: string) => |
||||
hostName.replace(/-.*$/, ''); |
||||
|
||||
export const getShortHostName = (hostName: string) => |
||||
hostName.replace(/[.].*$/, ''); |
@ -1,2 +0,0 @@ |
||||
export const getShortHostName = (hostName: string) => |
||||
hostName.replace(/[.].*$/, ''); |
@ -1,4 +1,5 @@ |
||||
export * from './getHostSSH'; |
||||
export * from './poweroffHost'; |
||||
export * from './rebootHost'; |
||||
export * from './runManifest'; |
||||
export * from './updateSystem'; |
||||
|
@ -0,0 +1,196 @@ |
||||
import assert from 'assert'; |
||||
import { RequestHandler } from 'express'; |
||||
|
||||
import { REP_PEACEFUL_STRING, REP_UUID } from '../../consts/REG_EXP_PATTERNS'; |
||||
import SERVER_PATHS from '../../consts/SERVER_PATHS'; |
||||
|
||||
import { getAnvilData, job, sub } from '../../accessModule'; |
||||
import { sanitize } from '../../sanitize'; |
||||
import { stderr } from '../../shell'; |
||||
|
||||
export const runManifest: RequestHandler< |
||||
{ manifestUuid: string }, |
||||
undefined, |
||||
RunManifestRequestBody |
||||
> = (request, response) => { |
||||
const { |
||||
params: { manifestUuid }, |
||||
body: { |
||||
debug = 2, |
||||
description: rawDescription, |
||||
hosts: rawHostList = {}, |
||||
password: rawPassword, |
||||
} = {}, |
||||
} = request; |
||||
|
||||
const description = sanitize(rawDescription, 'string'); |
||||
const password = sanitize(rawPassword, 'string'); |
||||
|
||||
const hostList: ManifestExecutionHostList = {}; |
||||
|
||||
const handleAssertError = (assertError: unknown) => { |
||||
stderr( |
||||
`Failed to assert value when trying to run manifest ${manifestUuid}; CAUSE: ${assertError}`, |
||||
); |
||||
|
||||
response.status(400).send(); |
||||
}; |
||||
|
||||
try { |
||||
assert( |
||||
REP_PEACEFUL_STRING.test(description), |
||||
`Description must be a peaceful string; got: [${description}]`, |
||||
); |
||||
|
||||
assert( |
||||
REP_PEACEFUL_STRING.test(password), |
||||
`Password must be a peaceful string; got [${password}]`, |
||||
); |
||||
|
||||
const uniqueList: Record<string, boolean | undefined> = {}; |
||||
const isHostListUnique = !Object.values(rawHostList).some( |
||||
({ hostNumber, hostType, hostUuid }) => { |
||||
const hostId = `${hostType}${hostNumber}`; |
||||
assert( |
||||
/^node[12]$/.test(hostId), |
||||
`Host ID must be "node" followed by 1 or 2; got [${hostId}]`, |
||||
); |
||||
|
||||
assert( |
||||
REP_UUID.test(hostUuid), |
||||
`Host UUID assigned to ${hostId} must be a UUIDv4; got [${hostUuid}]`, |
||||
); |
||||
|
||||
const isIdDuplicate = Boolean(uniqueList[hostId]); |
||||
const isUuidDuplicate = Boolean(uniqueList[hostUuid]); |
||||
|
||||
uniqueList[hostId] = true; |
||||
uniqueList[hostUuid] = true; |
||||
|
||||
hostList[hostId] = { hostNumber, hostType, hostUuid, hostId }; |
||||
|
||||
return isIdDuplicate || isUuidDuplicate; |
||||
}, |
||||
); |
||||
|
||||
assert(isHostListUnique, `Each entry in hosts must be unique`); |
||||
} catch (assertError) { |
||||
handleAssertError(assertError); |
||||
|
||||
return; |
||||
} |
||||
|
||||
let rawHostListData: AnvilDataHostListHash | undefined; |
||||
let rawManifestListData: AnvilDataManifestListHash | undefined; |
||||
let rawSysData: AnvilDataSysHash | undefined; |
||||
|
||||
try { |
||||
({ |
||||
hosts: rawHostListData, |
||||
manifests: rawManifestListData, |
||||
sys: rawSysData, |
||||
} = getAnvilData<{ |
||||
hosts?: AnvilDataHostListHash; |
||||
manifests?: AnvilDataManifestListHash; |
||||
sys?: AnvilDataSysHash; |
||||
}>( |
||||
{ hosts: true, manifests: true, sys: true }, |
||||
{ |
||||
predata: [ |
||||
['Database->get_hosts'], |
||||
[ |
||||
'Striker->load_manifest', |
||||
{ |
||||
debug, |
||||
manifest_uuid: manifestUuid, |
||||
}, |
||||
], |
||||
], |
||||
}, |
||||
)); |
||||
} catch (subError) { |
||||
stderr( |
||||
`Failed to get install manifest ${manifestUuid}; CAUSE: ${subError}`, |
||||
); |
||||
|
||||
response.status(500).send(); |
||||
|
||||
return; |
||||
} |
||||
|
||||
if (!rawHostListData || !rawManifestListData || !rawSysData) { |
||||
response.status(404).send(); |
||||
|
||||
return; |
||||
} |
||||
|
||||
const { host_uuid: hostUuidMapToData } = rawHostListData; |
||||
const { |
||||
manifest_uuid: { |
||||
[manifestUuid]: { |
||||
parsed: { name: manifestName }, |
||||
}, |
||||
}, |
||||
} = rawManifestListData; |
||||
const { hosts: { by_uuid: mapToHostNameData = {} } = {} } = rawSysData; |
||||
|
||||
const joinAnJobs: DBJobParams[] = []; |
||||
|
||||
let anParams: Record<string, string> | undefined; |
||||
|
||||
try { |
||||
anParams = Object.values(hostList).reduce<Record<string, string>>( |
||||
(previous, { hostId = '', hostUuid }) => { |
||||
const hostName = mapToHostNameData[hostUuid]; |
||||
const { anvil_name: anName } = hostUuidMapToData[hostUuid]; |
||||
|
||||
assert( |
||||
anName && anName !== manifestName, |
||||
`Host ${hostName} cannot be used for ${manifestName} because it belongs to ${anName}`, |
||||
); |
||||
|
||||
joinAnJobs.push({ |
||||
debug, |
||||
file: __filename, |
||||
job_command: SERVER_PATHS.usr.sbin['anvil-join-anvil'].self, |
||||
job_data: `as_machine=${hostId},manifest_uuid=${manifestUuid}`, |
||||
job_description: 'job_0073', |
||||
job_host_uuid: hostUuid, |
||||
job_name: `join_anvil::${hostId}`, |
||||
job_title: 'job_0072', |
||||
}); |
||||
|
||||
previous[`anvil_${hostId}_host_uuid`] = hostUuid; |
||||
|
||||
return previous; |
||||
}, |
||||
{ |
||||
anvil_description: description, |
||||
anvil_name: manifestName, |
||||
anvil_password: password, |
||||
}, |
||||
); |
||||
} catch (assertError) { |
||||
handleAssertError(assertError); |
||||
|
||||
return; |
||||
} |
||||
|
||||
try { |
||||
const [newAnUuid] = sub('insert_or_update_anvils', { subParams: anParams }) |
||||
.stdout as [string]; |
||||
|
||||
joinAnJobs.forEach((jobParams) => { |
||||
jobParams.job_data += `,anvil_uuid=${newAnUuid}`; |
||||
job(jobParams); |
||||
}); |
||||
} catch (subError) { |
||||
stderr(`Failed to record new anvil node entry; CAUSE: ${subError}`); |
||||
|
||||
response.status(500).send(); |
||||
|
||||
return; |
||||
} |
||||
|
||||
response.status(204).send(); |
||||
}; |
@ -0,0 +1,285 @@ |
||||
import assert from 'assert'; |
||||
import { RequestHandler } from 'express'; |
||||
|
||||
import { |
||||
REP_INTEGER, |
||||
REP_IPV4, |
||||
REP_IPV4_CSV, |
||||
REP_PEACEFUL_STRING, |
||||
REP_UUID, |
||||
} from '../../consts/REG_EXP_PATTERNS'; |
||||
|
||||
import { sub } from '../../accessModule'; |
||||
import { sanitize } from '../../sanitize'; |
||||
import { stdout } from '../../shell'; |
||||
|
||||
export const buildManifest = ( |
||||
...[request]: Parameters< |
||||
RequestHandler< |
||||
{ manifestUuid?: string }, |
||||
undefined, |
||||
BuildManifestRequestBody |
||||
> |
||||
> |
||||
) => { |
||||
const { |
||||
body: { |
||||
domain: rawDomain, |
||||
hostConfig: { hosts: hostList = {} } = {}, |
||||
networkConfig: { |
||||
dnsCsv: rawDns, |
||||
mtu: rawMtu = 1500, |
||||
networks: networkList = {}, |
||||
ntpCsv: rawNtp, |
||||
} = {}, |
||||
prefix: rawPrefix, |
||||
sequence: rawSequence, |
||||
} = {}, |
||||
params: { manifestUuid: rawManifestUuid = 'new' }, |
||||
} = request; |
||||
|
||||
stdout('Begin building install manifest.'); |
||||
|
||||
const dns = sanitize(rawDns, 'string'); |
||||
assert(REP_IPV4_CSV.test(dns), `DNS must be an IPv4 CSV; got [${dns}]`); |
||||
|
||||
const domain = sanitize(rawDomain, 'string'); |
||||
assert( |
||||
REP_PEACEFUL_STRING.test(domain), |
||||
`Domain must be a peaceful string; got [${domain}]`, |
||||
); |
||||
|
||||
const manifestUuid = sanitize(rawManifestUuid, 'string'); |
||||
assert( |
||||
REP_UUID.test(manifestUuid), |
||||
`Manifest UUID must be a UUIDv4; got [${manifestUuid}]`, |
||||
); |
||||
|
||||
const mtu = sanitize(rawMtu, 'number'); |
||||
assert(REP_INTEGER.test(String(mtu)), `MTU must be an integer; got [${mtu}]`); |
||||
|
||||
const ntp = sanitize(rawNtp, 'string'); |
||||
|
||||
if (ntp) { |
||||
assert(REP_IPV4_CSV.test(ntp), `NTP must be an IPv4 CSV; got [${ntp}]`); |
||||
} |
||||
|
||||
const prefix = sanitize(rawPrefix, 'string'); |
||||
assert( |
||||
REP_PEACEFUL_STRING.test(prefix), |
||||
`Prefix must be a peaceful string; got [${prefix}]`, |
||||
); |
||||
|
||||
const sequence = sanitize(rawSequence, 'number'); |
||||
assert( |
||||
REP_INTEGER.test(String(sequence)), |
||||
`Sequence must be an integer; got [${sequence}]`, |
||||
); |
||||
|
||||
const { counts: networkCountContainer, networks: networkContainer } = |
||||
Object.values(networkList).reduce<{ |
||||
counts: Record<string, number>; |
||||
networks: Record<string, string>; |
||||
}>( |
||||
( |
||||
previous, |
||||
{ |
||||
networkGateway: rawGateway, |
||||
networkMinIp: rawMinIp, |
||||
networkNumber: rawNetworkNumber, |
||||
networkSubnetMask: rawSubnetMask, |
||||
networkType: rawNetworkType, |
||||
}, |
||||
) => { |
||||
const networkType = sanitize(rawNetworkType, 'string'); |
||||
assert( |
||||
REP_PEACEFUL_STRING.test(networkType), |
||||
`Network type must be a peaceful string; got [${networkType}]`, |
||||
); |
||||
|
||||
const networkNumber = sanitize(rawNetworkNumber, 'number'); |
||||
assert( |
||||
REP_INTEGER.test(String(networkNumber)), |
||||
`Network number must be an integer; got [${networkNumber}]`, |
||||
); |
||||
|
||||
const networkId = `${networkType}${networkNumber}`; |
||||
|
||||
const gateway = sanitize(rawGateway, 'string'); |
||||
|
||||
if (networkType === 'ifn') { |
||||
assert( |
||||
REP_IPV4.test(gateway), |
||||
`Gateway of ${networkId} must be an IPv4; got [${gateway}]`, |
||||
); |
||||
} |
||||
|
||||
const minIp = sanitize(rawMinIp, 'string'); |
||||
assert( |
||||
REP_IPV4.test(minIp), |
||||
`Minimum IP of ${networkId} must be an IPv4; got [${minIp}]`, |
||||
); |
||||
|
||||
const subnetMask = sanitize(rawSubnetMask, 'string'); |
||||
assert( |
||||
REP_IPV4.test(subnetMask), |
||||
`Subnet mask of ${networkId} must be an IPv4; got [${subnetMask}]`, |
||||
); |
||||
|
||||
const { counts: countContainer, networks: networkContainer } = previous; |
||||
|
||||
const countKey = `${networkType}_count`; |
||||
const countValue = countContainer[countKey] ?? 0; |
||||
|
||||
countContainer[countKey] = countValue + 1; |
||||
|
||||
const gatewayKey = `${networkId}_gateway`; |
||||
const minIpKey = `${networkId}_network`; |
||||
const subnetMaskKey = `${networkId}_subnet`; |
||||
|
||||
networkContainer[gatewayKey] = gateway; |
||||
networkContainer[minIpKey] = minIp; |
||||
networkContainer[subnetMaskKey] = subnetMask; |
||||
|
||||
return previous; |
||||
}, |
||||
{ counts: {}, networks: {} }, |
||||
); |
||||
|
||||
const hostContainer = Object.values(hostList).reduce<Record<string, string>>( |
||||
( |
||||
previous, |
||||
{ |
||||
fences, |
||||
hostNumber: rawHostNumber, |
||||
hostType: rawHostType, |
||||
ipmiIp: rawIpmiIp, |
||||
networks, |
||||
upses, |
||||
}, |
||||
) => { |
||||
const hostType = sanitize(rawHostType, 'string'); |
||||
assert( |
||||
REP_PEACEFUL_STRING.test(hostType), |
||||
`Host type must be a peaceful string; got [${hostType}]`, |
||||
); |
||||
|
||||
const hostNumber = sanitize(rawHostNumber, 'number'); |
||||
assert( |
||||
REP_INTEGER.test(String(hostNumber)), |
||||
`Host number must be an integer; got [${hostNumber}]`, |
||||
); |
||||
|
||||
const hostId = `${hostType}${hostNumber}`; |
||||
|
||||
const ipmiIp = sanitize(rawIpmiIp, 'string'); |
||||
assert( |
||||
REP_IPV4.test(ipmiIp), |
||||
`IPMI IP of ${hostId} must be an IPv4; got [${ipmiIp}]`, |
||||
); |
||||
|
||||
const ipmiIpKey = `${hostId}_ipmi_ip`; |
||||
|
||||
previous[ipmiIpKey] = ipmiIp; |
||||
|
||||
Object.values(networks).forEach( |
||||
({ |
||||
networkIp: rawIp, |
||||
networkNumber: rawNetworkNumber, |
||||
networkType: rawNetworkType, |
||||
}) => { |
||||
const networkType = sanitize(rawNetworkType, 'string'); |
||||
assert( |
||||
REP_PEACEFUL_STRING.test(networkType), |
||||
`Network type must be a peaceful string; got [${networkType}]`, |
||||
); |
||||
|
||||
const networkNumber = sanitize(rawNetworkNumber, 'number'); |
||||
assert( |
||||
REP_INTEGER.test(String(networkNumber)), |
||||
`Network number must be an integer; got [${networkNumber}]`, |
||||
); |
||||
|
||||
const networkId = `${networkType}${networkNumber}`; |
||||
|
||||
const ip = sanitize(rawIp, 'string'); |
||||
assert( |
||||
REP_IPV4.test(ip), |
||||
`IP of host network ${networkId} must be an IPv4; got [${ip}]`, |
||||
); |
||||
|
||||
const networkIpKey = `${hostId}_${networkId}_ip`; |
||||
|
||||
previous[networkIpKey] = ip; |
||||
}, |
||||
); |
||||
|
||||
Object.values(fences).forEach( |
||||
({ fenceName: rawFenceName, fencePort: rawPort }) => { |
||||
const fenceName = sanitize(rawFenceName, 'string'); |
||||
assert( |
||||
REP_PEACEFUL_STRING.test(fenceName), |
||||
`Fence name must be a peaceful string; got [${fenceName}]`, |
||||
); |
||||
|
||||
const fenceKey = `${hostId}_fence_${fenceName}`; |
||||
|
||||
const port = sanitize(rawPort, 'string'); |
||||
assert( |
||||
REP_PEACEFUL_STRING.test(port), |
||||
`Port of ${fenceName} must be a peaceful string; got [${port}]`, |
||||
); |
||||
|
||||
previous[fenceKey] = port; |
||||
}, |
||||
); |
||||
|
||||
Object.values(upses).forEach( |
||||
({ isUsed: rawIsUsed, upsName: rawUpsName }) => { |
||||
const upsName = sanitize(rawUpsName, 'string'); |
||||
assert( |
||||
REP_PEACEFUL_STRING.test(upsName), |
||||
`UPS name must be a peaceful string; got [${upsName}]`, |
||||
); |
||||
|
||||
const upsKey = `${hostId}_ups_${upsName}`; |
||||
|
||||
const isUsed = sanitize(rawIsUsed, 'boolean'); |
||||
|
||||
if (isUsed) { |
||||
previous[upsKey] = 'checked'; |
||||
} |
||||
}, |
||||
); |
||||
|
||||
return previous; |
||||
}, |
||||
{}, |
||||
); |
||||
|
||||
let result: { name: string; uuid: string } | undefined; |
||||
|
||||
try { |
||||
const [uuid, name] = sub('generate_manifest', { |
||||
subModuleName: 'Striker', |
||||
subParams: { |
||||
dns, |
||||
domain, |
||||
manifest_uuid: manifestUuid, |
||||
mtu, |
||||
ntp, |
||||
prefix, |
||||
sequence, |
||||
...networkCountContainer, |
||||
...networkContainer, |
||||
...hostContainer, |
||||
}, |
||||
}).stdout as [manifestUuid: string, anvilName: string]; |
||||
|
||||
result = { name, uuid }; |
||||
} catch (subError) { |
||||
throw new Error(`Failed to generate manifest; CAUSE: ${subError}`); |
||||
} |
||||
|
||||
return result; |
||||
}; |
@ -0,0 +1,29 @@ |
||||
import { AssertionError } from 'assert'; |
||||
import { RequestHandler } from 'express'; |
||||
|
||||
import { buildManifest } from './buildManifest'; |
||||
import { stderr } from '../../shell'; |
||||
|
||||
export const createManifest: RequestHandler = (...handlerArgs) => { |
||||
const [, response] = handlerArgs; |
||||
|
||||
let result: Record<string, string> = {}; |
||||
|
||||
try { |
||||
result = buildManifest(...handlerArgs); |
||||
} catch (buildError) { |
||||
stderr(`Failed to create new install manifest; CAUSE ${buildError}`); |
||||
|
||||
let code = 500; |
||||
|
||||
if (buildError instanceof AssertionError) { |
||||
code = 400; |
||||
} |
||||
|
||||
response.status(code).send(); |
||||
|
||||
return; |
||||
} |
||||
|
||||
response.status(201).send(result); |
||||
}; |
@ -0,0 +1,37 @@ |
||||
import { RequestHandler } from 'express'; |
||||
|
||||
import { sub } from '../../accessModule'; |
||||
import { stderr, stdout } from '../../shell'; |
||||
|
||||
export const deleteManifest: RequestHandler< |
||||
{ manifestUuid: string }, |
||||
undefined, |
||||
{ uuids: string[] } |
||||
> = (request, response) => { |
||||
const { |
||||
params: { manifestUuid: rawManifestUuid }, |
||||
body: { uuids: rawManifestUuidList } = {}, |
||||
} = request; |
||||
|
||||
const manifestUuidList: string[] = rawManifestUuidList |
||||
? rawManifestUuidList |
||||
: [rawManifestUuid]; |
||||
|
||||
manifestUuidList.forEach((uuid) => { |
||||
stdout(`Begin delete manifest ${uuid}.`); |
||||
|
||||
try { |
||||
sub('insert_or_update_manifests', { |
||||
subParams: { delete: 1, manifest_uuid: uuid }, |
||||
}); |
||||
} catch (subError) { |
||||
stderr(`Failed to delete manifest ${uuid}; CAUSE: ${subError}`); |
||||
|
||||
response.status(500).send(); |
||||
|
||||
return; |
||||
} |
||||
}); |
||||
|
||||
response.status(204).send(); |
||||
}; |
@ -0,0 +1,34 @@ |
||||
import { RequestHandler } from 'express'; |
||||
|
||||
import buildGetRequestHandler from '../buildGetRequestHandler'; |
||||
import { buildQueryResultReducer } from '../../buildQueryResultModifier'; |
||||
|
||||
export const getManifest: RequestHandler = buildGetRequestHandler( |
||||
(response, buildQueryOptions) => { |
||||
const query = ` |
||||
SELECT |
||||
manifest_uuid, |
||||
manifest_name |
||||
FROM manifests |
||||
WHERE manifest_note != 'DELETED' |
||||
ORDER BY manifest_name ASC;`;
|
||||
const afterQueryReturn: QueryResultModifierFunction | undefined = |
||||
buildQueryResultReducer<{ [manifestUUID: string]: ManifestOverview }>( |
||||
(previous, [manifestUUID, manifestName]) => { |
||||
previous[manifestUUID] = { |
||||
manifestName, |
||||
manifestUUID, |
||||
}; |
||||
|
||||
return previous; |
||||
}, |
||||
{}, |
||||
); |
||||
|
||||
if (buildQueryOptions) { |
||||
buildQueryOptions.afterQueryReturn = afterQueryReturn; |
||||
} |
||||
|
||||
return query; |
||||
}, |
||||
); |
@ -0,0 +1,266 @@ |
||||
import { RequestHandler } from 'express'; |
||||
|
||||
import { getAnvilData } from '../../accessModule'; |
||||
import { getEntityParts } from '../../disassembleEntityId'; |
||||
import { stderr, stdout } from '../../shell'; |
||||
|
||||
const handleSortEntries = <T extends [string, unknown]>( |
||||
[aId]: T, |
||||
[bId]: T, |
||||
): number => { |
||||
const { name: at, number: an } = getEntityParts(aId); |
||||
const { name: bt, number: bn } = getEntityParts(bId); |
||||
|
||||
let result = 0; |
||||
|
||||
if (at === bt) { |
||||
if (an > bn) { |
||||
result = 1; |
||||
} else if (an < bn) { |
||||
result = -1; |
||||
} |
||||
} else if (at > bt) { |
||||
result = 1; |
||||
} else if (at < bt) { |
||||
result = -1; |
||||
} |
||||
|
||||
return result; |
||||
}; |
||||
|
||||
/** |
||||
* This handler sorts networks in ascending order. But, it groups IFNs at the |
||||
* end of the list in ascending order. |
||||
* |
||||
* When the sort callback returns: |
||||
* - positive, element `a` will get a higher index than element `b` |
||||
* - negative, element `a` will get a lower index than element `b` |
||||
* - zero, elements' index will remain unchanged |
||||
*/ |
||||
const handleSortNetworks = <T extends [string, unknown]>( |
||||
[aId]: T, |
||||
[bId]: T, |
||||
): number => { |
||||
const isAIfn = /^ifn/.test(aId); |
||||
const isBIfn = /^ifn/.test(bId); |
||||
const { name: at, number: an } = getEntityParts(aId); |
||||
const { name: bt, number: bn } = getEntityParts(bId); |
||||
|
||||
let result = 0; |
||||
|
||||
if (at === bt) { |
||||
if (an > bn) { |
||||
result = 1; |
||||
} else if (an < bn) { |
||||
result = -1; |
||||
} |
||||
} else if (isAIfn) { |
||||
result = 1; |
||||
} else if (isBIfn) { |
||||
result = -1; |
||||
} else if (at > bt) { |
||||
result = 1; |
||||
} else if (at < bt) { |
||||
result = -1; |
||||
} |
||||
|
||||
return result; |
||||
}; |
||||
|
||||
export const getManifestDetail: RequestHandler = (request, response) => { |
||||
const { |
||||
params: { manifestUUID }, |
||||
} = request; |
||||
|
||||
let rawManifestListData: AnvilDataManifestListHash | undefined; |
||||
|
||||
try { |
||||
({ manifests: rawManifestListData } = getAnvilData<{ |
||||
manifests?: AnvilDataManifestListHash; |
||||
}>( |
||||
{ manifests: true }, |
||||
{ |
||||
predata: [['Striker->load_manifest', { manifest_uuid: manifestUUID }]], |
||||
}, |
||||
)); |
||||
} catch (subError) { |
||||
stderr( |
||||
`Failed to get install manifest ${manifestUUID}; CAUSE: ${subError}`, |
||||
); |
||||
|
||||
response.status(500).send(); |
||||
|
||||
return; |
||||
} |
||||
|
||||
stdout( |
||||
`Raw install manifest list:\n${JSON.stringify( |
||||
rawManifestListData, |
||||
null, |
||||
2, |
||||
)}`,
|
||||
); |
||||
|
||||
if (!rawManifestListData) { |
||||
response.status(404).send(); |
||||
|
||||
return; |
||||
} |
||||
|
||||
const { |
||||
manifest_uuid: { |
||||
[manifestUUID]: { |
||||
parsed: { |
||||
domain, |
||||
fences: fenceUuidList = {}, |
||||
machine, |
||||
name, |
||||
networks: { dns: dnsCsv, mtu, name: networkList, ntp: ntpCsv }, |
||||
prefix, |
||||
sequence, |
||||
upses: upsUuidList = {}, |
||||
}, |
||||
}, |
||||
}, |
||||
} = rawManifestListData; |
||||
|
||||
const manifestData: ManifestDetail = { |
||||
domain, |
||||
hostConfig: { |
||||
hosts: Object.entries(machine) |
||||
.sort(handleSortEntries) |
||||
.reduce<ManifestDetailHostList>( |
||||
( |
||||
previous, |
||||
[ |
||||
hostId, |
||||
{ |
||||
fence = {}, |
||||
ipmi_ip: ipmiIp, |
||||
name: hostName, |
||||
network, |
||||
ups = {}, |
||||
}, |
||||
], |
||||
) => { |
||||
const { name: hostType, number: hostNumber } = |
||||
getEntityParts(hostId); |
||||
|
||||
stdout(`host=${hostType},n=${hostNumber}`); |
||||
|
||||
// Only include node-type host(s).
|
||||
if (hostType !== 'node') { |
||||
return previous; |
||||
} |
||||
|
||||
previous[hostId] = { |
||||
fences: Object.entries(fence) |
||||
.sort(handleSortEntries) |
||||
.reduce<ManifestDetailFenceList>( |
||||
(fences, [fenceName, { port: fencePort }]) => { |
||||
const fenceUuidContainer = fenceUuidList[fenceName]; |
||||
|
||||
if (fenceUuidContainer) { |
||||
const { uuid: fenceUuid } = fenceUuidContainer; |
||||
|
||||
fences[fenceName] = { |
||||
fenceName, |
||||
fencePort, |
||||
fenceUuid, |
||||
}; |
||||
} |
||||
|
||||
return fences; |
||||
}, |
||||
{}, |
||||
), |
||||
hostName, |
||||
hostNumber, |
||||
hostType, |
||||
ipmiIp, |
||||
networks: Object.entries(network) |
||||
.sort(handleSortNetworks) |
||||
.reduce<ManifestDetailHostNetworkList>( |
||||
(hostNetworks, [networkId, { ip: networkIp }]) => { |
||||
const { name: networkType, number: networkNumber } = |
||||
getEntityParts(networkId); |
||||
|
||||
stdout(`hostnetwork=${networkType},n=${networkNumber}`); |
||||
|
||||
hostNetworks[networkId] = { |
||||
networkIp, |
||||
networkNumber, |
||||
networkType, |
||||
}; |
||||
|
||||
return hostNetworks; |
||||
}, |
||||
{}, |
||||
), |
||||
upses: Object.entries(ups) |
||||
.sort(handleSortEntries) |
||||
.reduce<ManifestDetailUpsList>((upses, [upsName, { used }]) => { |
||||
const upsUuidContainer = upsUuidList[upsName]; |
||||
|
||||
if (upsUuidContainer) { |
||||
const { uuid: upsUuid } = upsUuidContainer; |
||||
|
||||
upses[upsName] = { |
||||
isUsed: Boolean(used), |
||||
upsName, |
||||
upsUuid, |
||||
}; |
||||
} |
||||
|
||||
return upses; |
||||
}, {}), |
||||
}; |
||||
|
||||
return previous; |
||||
}, |
||||
{}, |
||||
), |
||||
}, |
||||
name, |
||||
networkConfig: { |
||||
dnsCsv, |
||||
mtu: Number.parseInt(mtu), |
||||
networks: Object.entries(networkList) |
||||
.sort(handleSortNetworks) |
||||
.reduce<ManifestDetailNetworkList>( |
||||
( |
||||
networks, |
||||
[ |
||||
networkId, |
||||
{ |
||||
gateway: networkGateway, |
||||
network: networkMinIp, |
||||
subnet: networkSubnetMask, |
||||
}, |
||||
], |
||||
) => { |
||||
const { name: networkType, number: networkNumber } = |
||||
getEntityParts(networkId); |
||||
|
||||
stdout(`network=${networkType},n=${networkNumber}`); |
||||
|
||||
networks[networkId] = { |
||||
networkGateway, |
||||
networkMinIp, |
||||
networkNumber, |
||||
networkSubnetMask, |
||||
networkType, |
||||
}; |
||||
|
||||
return networks; |
||||
}, |
||||
{}, |
||||
), |
||||
ntpCsv, |
||||
}, |
||||
prefix, |
||||
sequence: Number.parseInt(sequence), |
||||
}; |
||||
|
||||
response.status(200).send(manifestData); |
||||
}; |
@ -0,0 +1,119 @@ |
||||
import { RequestHandler } from 'express'; |
||||
|
||||
import { dbQuery, getLocalHostName } from '../../accessModule'; |
||||
import { |
||||
getHostNameDomain, |
||||
getHostNamePrefix, |
||||
getShortHostName, |
||||
} from '../../disassembleHostName'; |
||||
import { stderr } from '../../shell'; |
||||
|
||||
export const getManifestTemplate: RequestHandler = (request, response) => { |
||||
let localHostName = ''; |
||||
|
||||
try { |
||||
localHostName = getLocalHostName(); |
||||
} catch (subError) { |
||||
stderr(String(subError)); |
||||
|
||||
response.status(500).send(); |
||||
|
||||
return; |
||||
} |
||||
|
||||
const localShortHostName = getShortHostName(localHostName); |
||||
|
||||
const domain = getHostNameDomain(localHostName); |
||||
const prefix = getHostNamePrefix(localShortHostName); |
||||
|
||||
let rawQueryResult: Array< |
||||
[ |
||||
fenceUUID: string, |
||||
fenceName: string, |
||||
upsUUID: string, |
||||
upsName: string, |
||||
manifestUuid: string, |
||||
lastSequence: string, |
||||
] |
||||
>; |
||||
|
||||
try { |
||||
({ stdout: rawQueryResult } = dbQuery( |
||||
`SELECT
|
||||
a.fence_uuid, |
||||
a.fence_name, |
||||
b.ups_uuid, |
||||
b.ups_name, |
||||
c.last_sequence |
||||
FROM ( |
||||
SELECT |
||||
ROW_NUMBER() OVER (ORDER BY fence_name), |
||||
fence_uuid, |
||||
fence_name |
||||
FROM fences |
||||
ORDER BY fence_name |
||||
) AS a |
||||
FULL JOIN ( |
||||
SELECT |
||||
ROW_NUMBER() OVER (ORDER BY ups_name), |
||||
ups_uuid, |
||||
ups_name |
||||
FROM upses |
||||
ORDER BY ups_name |
||||
) AS b ON a.row_number = b.row_number |
||||
FULL JOIN ( |
||||
SELECT |
||||
ROW_NUMBER() OVER (ORDER BY manifest_name DESC), |
||||
CAST( |
||||
SUBSTRING(manifest_name, '([\\d]*)$') AS INTEGER |
||||
) AS last_sequence |
||||
FROM manifests |
||||
ORDER BY manifest_name DESC |
||||
LIMIT 1 |
||||
) AS c ON a.row_number = c.row_number;`,
|
||||
)); |
||||
} catch (queryError) { |
||||
stderr(`Failed to execute query; CAUSE: ${queryError}`); |
||||
|
||||
response.status(500).send(); |
||||
|
||||
return; |
||||
} |
||||
|
||||
const queryResult = rawQueryResult.reduce< |
||||
Pick<ManifestTemplate, 'fences' | 'sequence' | 'upses'> |
||||
>( |
||||
(previous, [fenceUUID, fenceName, upsUUID, upsName, lastSequence]) => { |
||||
const { fences, upses } = previous; |
||||
|
||||
if (fenceUUID) { |
||||
fences[fenceUUID] = { |
||||
fenceName, |
||||
fenceUUID, |
||||
}; |
||||
} |
||||
|
||||
if (upsUUID) { |
||||
upses[upsUUID] = { |
||||
upsName, |
||||
upsUUID, |
||||
}; |
||||
} |
||||
|
||||
if (lastSequence) { |
||||
previous.sequence = Number.parseInt(lastSequence) + 1; |
||||
} |
||||
|
||||
return previous; |
||||
}, |
||||
{ fences: {}, sequence: 1, upses: {} }, |
||||
); |
||||
|
||||
const result: ManifestTemplate = { |
||||
domain, |
||||
prefix, |
||||
...queryResult, |
||||
}; |
||||
|
||||
response.status(200).send(result); |
||||
}; |
@ -0,0 +1,6 @@ |
||||
export * from './createManifest'; |
||||
export * from './deleteManifest'; |
||||
export * from './getManifest'; |
||||
export * from './getManifestDetail'; |
||||
export * from './getManifestTemplate'; |
||||
export * from './updateManifest'; |
@ -0,0 +1,34 @@ |
||||
import { AssertionError } from 'assert'; |
||||
import { RequestHandler } from 'express'; |
||||
|
||||
import { buildManifest } from './buildManifest'; |
||||
import { stderr } from '../../shell'; |
||||
|
||||
export const updateManifest: RequestHandler = (...args) => { |
||||
const [request, response] = args; |
||||
const { |
||||
params: { manifestUuid }, |
||||
} = request; |
||||
|
||||
let result: Record<string, string> = {}; |
||||
|
||||
try { |
||||
result = buildManifest(...args); |
||||
} catch (buildError) { |
||||
stderr( |
||||
`Failed to update install manifest ${manifestUuid}; CAUSE: ${buildError}`, |
||||
); |
||||
|
||||
let code = 500; |
||||
|
||||
if (buildError instanceof AssertionError) { |
||||
code = 400; |
||||
} |
||||
|
||||
response.status(code).send(); |
||||
|
||||
return; |
||||
} |
||||
|
||||
response.status(200).send(result); |
||||
}; |
@ -0,0 +1,23 @@ |
||||
import express from 'express'; |
||||
|
||||
import { |
||||
createManifest, |
||||
deleteManifest, |
||||
getManifest, |
||||
getManifestDetail, |
||||
getManifestTemplate, |
||||
updateManifest, |
||||
} from '../lib/request_handlers/manifest'; |
||||
|
||||
const router = express.Router(); |
||||
|
||||
router |
||||
.delete('/', deleteManifest) |
||||
.delete('/manifestUuid', deleteManifest) |
||||
.get('/', getManifest) |
||||
.get('/template', getManifestTemplate) |
||||
.get('/:manifestUUID', getManifestDetail) |
||||
.post('/', createManifest) |
||||
.put('/:manifestUuid', updateManifest); |
||||
|
||||
export default router; |
@ -0,0 +1,112 @@ |
||||
type ManifestOverview = { |
||||
manifestName: string; |
||||
manifestUUID: string; |
||||
}; |
||||
|
||||
type ManifestDetailNetwork = { |
||||
networkGateway: string; |
||||
networkMinIp: string; |
||||
networkNumber: number; |
||||
networkSubnetMask: string; |
||||
networkType: string; |
||||
}; |
||||
|
||||
type ManifestDetailNetworkList = { |
||||
[networkId: string]: ManifestDetailNetwork; |
||||
}; |
||||
|
||||
type ManifestDetailFence = { |
||||
fenceName: string; |
||||
fencePort: string; |
||||
fenceUuid: string; |
||||
}; |
||||
|
||||
type ManifestDetailFenceList = { |
||||
[fenceId: string]: ManifestDetailFence; |
||||
}; |
||||
|
||||
type ManifestDetailHostNetwork = { |
||||
networkIp: string; |
||||
networkNumber: number; |
||||
networkType: string; |
||||
}; |
||||
|
||||
type ManifestDetailHostNetworkList = { |
||||
[networkId: string]: ManifestDetailHostNetwork; |
||||
}; |
||||
|
||||
type ManifestDetailUps = { |
||||
isUsed: boolean; |
||||
upsName: string; |
||||
upsUuid: string; |
||||
}; |
||||
|
||||
type ManifestDetailUpsList = { |
||||
[upsId: string]: ManifestDetailUps; |
||||
}; |
||||
|
||||
type ManifestDetailHostList = { |
||||
[hostId: string]: { |
||||
fences: ManifestDetailFenceList; |
||||
hostName: string; |
||||
hostNumber: number; |
||||
hostType: string; |
||||
ipmiIp: string; |
||||
networks: ManifestDetailHostNetworkList; |
||||
upses: ManifestDetailUpsList; |
||||
}; |
||||
}; |
||||
|
||||
type ManifestDetail = { |
||||
domain: string; |
||||
hostConfig: { |
||||
hosts: ManifestDetailHostList; |
||||
}; |
||||
name: string; |
||||
networkConfig: { |
||||
dnsCsv: string; |
||||
mtu: number; |
||||
networks: ManifestDetailNetworkList; |
||||
ntpCsv: string; |
||||
}; |
||||
prefix: string; |
||||
sequence: number; |
||||
}; |
||||
|
||||
type ManifestExecutionHost = { |
||||
hostId?: string; |
||||
hostNumber: number; |
||||
hostType: string; |
||||
hostUuid: string; |
||||
}; |
||||
|
||||
type ManifestExecutionHostList = { |
||||
[hostId: string]: ManifestExecutionHost; |
||||
}; |
||||
|
||||
type ManifestTemplate = { |
||||
domain: string; |
||||
fences: { |
||||
[fenceUUID: string]: { |
||||
fenceName: string; |
||||
fenceUUID: string; |
||||
}; |
||||
}; |
||||
prefix: string; |
||||
sequence: number; |
||||
upses: { |
||||
[upsUUID: string]: { |
||||
upsName: string; |
||||
upsUUID: string; |
||||
}; |
||||
}; |
||||
}; |
||||
|
||||
type BuildManifestRequestBody = Omit<ManifestDetail, 'name'>; |
||||
|
||||
type RunManifestRequestBody = { |
||||
debug?: number; |
||||
description: string; |
||||
hosts: ManifestExecutionHostList; |
||||
password: string; |
||||
}; |
@ -0,0 +1,88 @@ |
||||
import { ReactElement, useMemo, useState } from 'react'; |
||||
|
||||
import AnHostConfigInputGroup from './AnHostConfigInputGroup'; |
||||
import AnIdInputGroup, { |
||||
INPUT_ID_AI_DOMAIN, |
||||
INPUT_ID_AI_PREFIX, |
||||
INPUT_ID_AI_SEQUENCE, |
||||
} from './AnIdInputGroup'; |
||||
import AnNetworkConfigInputGroup, { |
||||
INPUT_ID_ANC_DNS, |
||||
INPUT_ID_ANC_MTU, |
||||
INPUT_ID_ANC_NTP, |
||||
} from './AnNetworkConfigInputGroup'; |
||||
import FlexBox from '../FlexBox'; |
||||
|
||||
const DEFAULT_NETWORK_LIST: ManifestNetworkList = { |
||||
bcn1: { |
||||
networkMinIp: '10.201.0.0', |
||||
networkNumber: 1, |
||||
networkSubnetMask: '255.255.0.0', |
||||
networkType: 'bcn', |
||||
}, |
||||
sn1: { |
||||
networkMinIp: '10.101.0.0', |
||||
networkNumber: 1, |
||||
networkSubnetMask: '255.255.0.0', |
||||
networkType: 'sn', |
||||
}, |
||||
ifn1: { |
||||
networkMinIp: '', |
||||
networkNumber: 1, |
||||
networkSubnetMask: '', |
||||
networkType: 'ifn', |
||||
}, |
||||
}; |
||||
|
||||
const AddManifestInputGroup = < |
||||
M extends { |
||||
[K in |
||||
| typeof INPUT_ID_AI_DOMAIN |
||||
| typeof INPUT_ID_AI_PREFIX |
||||
| typeof INPUT_ID_AI_SEQUENCE |
||||
| typeof INPUT_ID_ANC_DNS |
||||
| typeof INPUT_ID_ANC_MTU |
||||
| typeof INPUT_ID_ANC_NTP]: string; |
||||
}, |
||||
>({ |
||||
formUtils, |
||||
knownFences, |
||||
knownUpses, |
||||
previous: { |
||||
hostConfig: previousHostConfig, |
||||
networkConfig: previousNetworkConfig = {}, |
||||
...previousAnId |
||||
} = {}, |
||||
}: AddManifestInputGroupProps<M>): ReactElement => { |
||||
const { networks: previousNetworkList = DEFAULT_NETWORK_LIST } = |
||||
previousNetworkConfig; |
||||
|
||||
const [networkList, setNetworkList] = |
||||
useState<ManifestNetworkList>(previousNetworkList); |
||||
|
||||
const networkListEntries = useMemo( |
||||
() => Object.entries(networkList), |
||||
[networkList], |
||||
); |
||||
|
||||
return ( |
||||
<FlexBox> |
||||
<AnIdInputGroup formUtils={formUtils} previous={previousAnId} /> |
||||
<AnNetworkConfigInputGroup |
||||
formUtils={formUtils} |
||||
networkListEntries={networkListEntries} |
||||
previous={previousNetworkConfig} |
||||
setNetworkList={setNetworkList} |
||||
/> |
||||
<AnHostConfigInputGroup |
||||
formUtils={formUtils} |
||||
knownFences={knownFences} |
||||
knownUpses={knownUpses} |
||||
networkListEntries={networkListEntries} |
||||
previous={previousHostConfig} |
||||
/> |
||||
</FlexBox> |
||||
); |
||||
}; |
||||
|
||||
export default AddManifestInputGroup; |
@ -0,0 +1,130 @@ |
||||
import { ReactElement, useMemo } from 'react'; |
||||
|
||||
import AnHostInputGroup from './AnHostInputGroup'; |
||||
import Grid from '../Grid'; |
||||
|
||||
const INPUT_ID_PREFIX_AN_HOST_CONFIG = 'an-host-config-input'; |
||||
|
||||
const INPUT_GROUP_ID_PREFIX_AHC = `${INPUT_ID_PREFIX_AN_HOST_CONFIG}-group`; |
||||
const INPUT_GROUP_CELL_ID_PREFIX_AHC = `${INPUT_GROUP_ID_PREFIX_AHC}-cell`; |
||||
|
||||
const DEFAULT_HOST_LIST: ManifestHostList = { |
||||
node1: { |
||||
hostNumber: 1, |
||||
hostType: 'node', |
||||
}, |
||||
node2: { |
||||
hostNumber: 2, |
||||
hostType: 'node', |
||||
}, |
||||
}; |
||||
|
||||
const AnHostConfigInputGroup = <M extends MapToInputTestID>({ |
||||
formUtils, |
||||
knownFences = {}, |
||||
knownUpses = {}, |
||||
networkListEntries, |
||||
previous: { hosts: previousHostList = DEFAULT_HOST_LIST } = {}, |
||||
}: AnHostConfigInputGroupProps<M>): ReactElement => { |
||||
const hostListEntries = useMemo( |
||||
() => Object.entries(previousHostList), |
||||
[previousHostList], |
||||
); |
||||
const knownFenceListValues = useMemo( |
||||
() => Object.values(knownFences), |
||||
[knownFences], |
||||
); |
||||
const knownUpsListValues = useMemo( |
||||
() => Object.values(knownUpses), |
||||
[knownUpses], |
||||
); |
||||
|
||||
const hostListGridLayout = useMemo<GridLayout>( |
||||
() => |
||||
hostListEntries.reduce<GridLayout>( |
||||
(previous, [hostId, previousHostArgs]) => { |
||||
const { |
||||
fences: previousFenceList = {}, |
||||
hostNumber, |
||||
hostType, |
||||
ipmiIp, |
||||
networks: previousNetworkList = {}, |
||||
upses: previousUpsList = {}, |
||||
}: ManifestHost = previousHostArgs; |
||||
|
||||
const fences = knownFenceListValues.reduce<ManifestHostFenceList>( |
||||
(fenceList, { fenceName }) => { |
||||
const { [fenceName]: { fencePort = '' } = {} } = |
||||
previousFenceList; |
||||
|
||||
fenceList[fenceName] = { fenceName, fencePort }; |
||||
|
||||
return fenceList; |
||||
}, |
||||
{}, |
||||
); |
||||
const networks = networkListEntries.reduce<ManifestHostNetworkList>( |
||||
(networkList, [networkId, { networkNumber, networkType }]) => { |
||||
const { [networkId]: { networkIp = '' } = {} } = |
||||
previousNetworkList; |
||||
|
||||
networkList[networkId] = { |
||||
networkIp, |
||||
networkNumber, |
||||
networkType, |
||||
}; |
||||
|
||||
return networkList; |
||||
}, |
||||
{}, |
||||
); |
||||
const upses = knownUpsListValues.reduce<ManifestHostUpsList>( |
||||
(upsList, { upsName }) => { |
||||
const { [upsName]: { isUsed = true } = {} } = previousUpsList; |
||||
|
||||
upsList[upsName] = { isUsed, upsName }; |
||||
|
||||
return upsList; |
||||
}, |
||||
{}, |
||||
); |
||||
|
||||
const cellId = `${INPUT_GROUP_CELL_ID_PREFIX_AHC}-${hostId}`; |
||||
|
||||
previous[cellId] = { |
||||
children: ( |
||||
<AnHostInputGroup |
||||
formUtils={formUtils} |
||||
hostId={hostId} |
||||
hostNumber={hostNumber} |
||||
hostType={hostType} |
||||
previous={{ fences, ipmiIp, networks, upses }} |
||||
/> |
||||
), |
||||
md: 3, |
||||
sm: 2, |
||||
}; |
||||
|
||||
return previous; |
||||
}, |
||||
{}, |
||||
), |
||||
[ |
||||
formUtils, |
||||
hostListEntries, |
||||
knownFenceListValues, |
||||
knownUpsListValues, |
||||
networkListEntries, |
||||
], |
||||
); |
||||
|
||||
return ( |
||||
<Grid |
||||
columns={{ xs: 1, sm: 2, md: 3 }} |
||||
layout={hostListGridLayout} |
||||
spacing="1em" |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export default AnHostConfigInputGroup; |
@ -0,0 +1,404 @@ |
||||
import { ReactElement, useMemo } from 'react'; |
||||
|
||||
import FlexBox from '../FlexBox'; |
||||
import Grid from '../Grid'; |
||||
import InputWithRef from '../InputWithRef'; |
||||
import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; |
||||
import { InnerPanel, InnerPanelBody, InnerPanelHeader } from '../Panels'; |
||||
import SwitchWithLabel from '../SwitchWithLabel'; |
||||
import { |
||||
buildIPAddressTestBatch, |
||||
buildPeacefulStringTestBatch, |
||||
} from '../../lib/test_input'; |
||||
import { BodyText } from '../Text'; |
||||
|
||||
const INPUT_ID_PREFIX_AN_HOST = 'an-host-input'; |
||||
|
||||
const INPUT_CELL_ID_PREFIX_AH = `${INPUT_ID_PREFIX_AN_HOST}-cell`; |
||||
|
||||
const INPUT_LABEL_AH_IPMI_IP = 'IPMI IP'; |
||||
|
||||
const MAP_TO_AH_INPUT_HANDLER: MapToManifestFormInputHandler = { |
||||
fence: (container, input) => { |
||||
const { |
||||
dataset: { hostId = '', fenceId = '', fenceName = '' }, |
||||
value: fencePort, |
||||
} = input; |
||||
const { |
||||
hostConfig: { |
||||
hosts: { [hostId]: host }, |
||||
}, |
||||
} = container; |
||||
const { fences = {} } = host; |
||||
|
||||
fences[fenceId] = { |
||||
fenceName, |
||||
fencePort, |
||||
}; |
||||
host.fences = fences; |
||||
}, |
||||
host: (container, input) => { |
||||
const { |
||||
dataset: { hostId = '', hostNumber: rawHostNumber = '', hostType = '' }, |
||||
} = input; |
||||
const hostNumber = Number.parseInt(rawHostNumber, 10); |
||||
|
||||
container.hostConfig.hosts[hostId] = { |
||||
hostNumber, |
||||
hostType, |
||||
}; |
||||
}, |
||||
ipmi: (container, input) => { |
||||
const { |
||||
dataset: { hostId = '' }, |
||||
value: ipmiIp, |
||||
} = input; |
||||
const { |
||||
hostConfig: { |
||||
hosts: { [hostId]: host }, |
||||
}, |
||||
} = container; |
||||
|
||||
host.ipmiIp = ipmiIp; |
||||
}, |
||||
network: (container, input) => { |
||||
const { |
||||
dataset: { |
||||
hostId = '', |
||||
networkId = '', |
||||
networkNumber: rawNetworkNumber = '', |
||||
networkType = '', |
||||
}, |
||||
value: networkIp, |
||||
} = input; |
||||
const { |
||||
hostConfig: { |
||||
hosts: { [hostId]: host }, |
||||
}, |
||||
} = container; |
||||
const { networks = {} } = host; |
||||
const networkNumber = Number.parseInt(rawNetworkNumber, 10); |
||||
|
||||
networks[networkId] = { |
||||
networkIp, |
||||
networkNumber, |
||||
networkType, |
||||
}; |
||||
host.networks = networks; |
||||
}, |
||||
ups: (container, input) => { |
||||
const { |
||||
checked: isUsed, |
||||
dataset: { hostId = '', upsId = '', upsName = '' }, |
||||
} = input; |
||||
const { |
||||
hostConfig: { |
||||
hosts: { [hostId]: host }, |
||||
}, |
||||
} = container; |
||||
const { upses = {} } = host; |
||||
|
||||
upses[upsId] = { |
||||
isUsed, |
||||
upsName, |
||||
}; |
||||
host.upses = upses; |
||||
}, |
||||
}; |
||||
|
||||
const GRID_COLUMNS = { xs: 1, sm: 2, md: 3 }; |
||||
const GRID_SPACING = '1em'; |
||||
|
||||
const buildInputIdAHFencePort = (hostId: string, fenceId: string): string => |
||||
`${INPUT_ID_PREFIX_AN_HOST}-${hostId}-${fenceId}-port`; |
||||
|
||||
const buildInputIdAHIpmiIp = (hostId: string): string => |
||||
`${INPUT_ID_PREFIX_AN_HOST}-${hostId}-ipmi-ip`; |
||||
|
||||
const buildInputIdAHNetworkIp = (hostId: string, networkId: string): string => |
||||
`${INPUT_ID_PREFIX_AN_HOST}-${hostId}-${networkId}-ip`; |
||||
|
||||
const buildInputIdAHUpsPowerHost = (hostId: string, upsId: string): string => |
||||
`${INPUT_ID_PREFIX_AN_HOST}-${hostId}-${upsId}-power-host`; |
||||
|
||||
const AnHostInputGroup = <M extends MapToInputTestID>({ |
||||
formUtils: { |
||||
buildFinishInputTestBatchFunction, |
||||
buildInputFirstRenderFunction, |
||||
buildInputUnmountFunction, |
||||
setMessage, |
||||
}, |
||||
hostId, |
||||
hostNumber, |
||||
hostType, |
||||
previous: { |
||||
fences: fenceList = {}, |
||||
ipmiIp: previousIpmiIp, |
||||
networks: networkList = {}, |
||||
upses: upsList = {}, |
||||
} = {}, |
||||
// Props that depend on others.
|
||||
hostLabel = `${hostType} ${hostNumber}`, |
||||
}: AnHostInputGroupProps<M>): ReactElement => { |
||||
const fenceListEntries = useMemo( |
||||
() => Object.entries(fenceList), |
||||
[fenceList], |
||||
); |
||||
const networkListEntries = useMemo( |
||||
() => Object.entries(networkList), |
||||
[networkList], |
||||
); |
||||
const upsListEntries = useMemo(() => Object.entries(upsList), [upsList]); |
||||
|
||||
const isShowUpsListGrid = useMemo( |
||||
() => Boolean(upsListEntries.length), |
||||
[upsListEntries.length], |
||||
); |
||||
|
||||
const inputIdAHHost = useMemo( |
||||
() => `${INPUT_ID_PREFIX_AN_HOST}-${hostId}`, |
||||
[hostId], |
||||
); |
||||
const inputIdAHIpmiIp = useMemo(() => buildInputIdAHIpmiIp(hostId), [hostId]); |
||||
|
||||
const inputCellIdAHIpmiIp = useMemo( |
||||
() => `${INPUT_CELL_ID_PREFIX_AH}-${hostId}-ipmi-ip`, |
||||
[hostId], |
||||
); |
||||
|
||||
const fenceListGridLayout = useMemo( |
||||
() => |
||||
fenceListEntries.reduce<GridLayout>( |
||||
(previous, [fenceId, { fenceName, fencePort }]) => { |
||||
const cellId = `${INPUT_CELL_ID_PREFIX_AH}-${hostId}-${fenceId}-port`; |
||||
|
||||
const inputId = buildInputIdAHFencePort(hostId, fenceId); |
||||
const inputLabel = `Port on ${fenceName}`; |
||||
|
||||
previous[cellId] = { |
||||
children: ( |
||||
<InputWithRef |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
baseInputProps={{ |
||||
'data-handler': 'fence', |
||||
'data-host-id': hostId, |
||||
'data-fence-id': fenceId, |
||||
'data-fence-name': fenceName, |
||||
}} |
||||
id={inputId} |
||||
label={inputLabel} |
||||
value={fencePort} |
||||
/> |
||||
} |
||||
inputTestBatch={buildPeacefulStringTestBatch( |
||||
`${hostId} ${inputLabel}`, |
||||
() => { |
||||
setMessage(inputId); |
||||
}, |
||||
{ onFinishBatch: buildFinishInputTestBatchFunction(inputId) }, |
||||
(message) => { |
||||
setMessage(inputId, { children: message }); |
||||
}, |
||||
)} |
||||
onFirstRender={buildInputFirstRenderFunction(inputId)} |
||||
required |
||||
/> |
||||
), |
||||
}; |
||||
|
||||
return previous; |
||||
}, |
||||
{}, |
||||
), |
||||
[ |
||||
buildFinishInputTestBatchFunction, |
||||
buildInputFirstRenderFunction, |
||||
fenceListEntries, |
||||
hostId, |
||||
setMessage, |
||||
], |
||||
); |
||||
|
||||
const networkListGridLayout = useMemo( |
||||
() => |
||||
networkListEntries.reduce<GridLayout>( |
||||
(previous, [networkId, { networkIp, networkNumber, networkType }]) => { |
||||
const cellId = `${INPUT_CELL_ID_PREFIX_AH}-${hostId}-${networkId}-ip`; |
||||
|
||||
const inputId = buildInputIdAHNetworkIp(hostId, networkId); |
||||
const inputLabel = `${networkType.toUpperCase()} ${networkNumber} IP`; |
||||
|
||||
previous[cellId] = { |
||||
children: ( |
||||
<InputWithRef |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
baseInputProps={{ |
||||
'data-handler': 'network', |
||||
'data-host-id': hostId, |
||||
'data-network-id': networkId, |
||||
'data-network-number': networkNumber, |
||||
'data-network-type': networkType, |
||||
}} |
||||
id={inputId} |
||||
label={inputLabel} |
||||
value={networkIp} |
||||
/> |
||||
} |
||||
inputTestBatch={buildIPAddressTestBatch( |
||||
`${hostId} ${inputLabel}`, |
||||
() => { |
||||
setMessage(inputId); |
||||
}, |
||||
{ onFinishBatch: buildFinishInputTestBatchFunction(inputId) }, |
||||
(message) => { |
||||
setMessage(inputId, { children: message }); |
||||
}, |
||||
)} |
||||
onFirstRender={buildInputFirstRenderFunction(inputId)} |
||||
onUnmount={buildInputUnmountFunction(inputId)} |
||||
required |
||||
/> |
||||
), |
||||
}; |
||||
|
||||
return previous; |
||||
}, |
||||
{}, |
||||
), |
||||
[ |
||||
networkListEntries, |
||||
hostId, |
||||
buildFinishInputTestBatchFunction, |
||||
buildInputFirstRenderFunction, |
||||
buildInputUnmountFunction, |
||||
setMessage, |
||||
], |
||||
); |
||||
|
||||
const upsListGridLayout = useMemo( |
||||
() => |
||||
upsListEntries.reduce<GridLayout>( |
||||
(previous, [upsId, { isUsed, upsName }]) => { |
||||
const cellId = `${INPUT_CELL_ID_PREFIX_AH}-${hostId}-${upsId}-power-host`; |
||||
|
||||
const inputId = buildInputIdAHUpsPowerHost(hostId, upsId); |
||||
const inputLabel = `Uses ${upsName}`; |
||||
|
||||
previous[cellId] = { |
||||
children: ( |
||||
<InputWithRef |
||||
input={ |
||||
<SwitchWithLabel |
||||
baseInputProps={{ |
||||
'data-handler': 'ups', |
||||
'data-host-id': hostId, |
||||
'data-ups-id': upsId, |
||||
'data-ups-name': upsName, |
||||
}} |
||||
checked={isUsed} |
||||
id={inputId} |
||||
label={inputLabel} |
||||
/> |
||||
} |
||||
valueType="boolean" |
||||
/> |
||||
), |
||||
}; |
||||
|
||||
return previous; |
||||
}, |
||||
{}, |
||||
), |
||||
[hostId, upsListEntries], |
||||
); |
||||
|
||||
const upsListGrid = useMemo( |
||||
() => |
||||
isShowUpsListGrid && ( |
||||
<Grid |
||||
columns={GRID_COLUMNS} |
||||
layout={upsListGridLayout} |
||||
spacing={GRID_SPACING} |
||||
/> |
||||
), |
||||
[isShowUpsListGrid, upsListGridLayout], |
||||
); |
||||
|
||||
return ( |
||||
<InnerPanel mv={0}> |
||||
<InnerPanelHeader> |
||||
<BodyText>{hostLabel}</BodyText> |
||||
</InnerPanelHeader> |
||||
<InnerPanelBody> |
||||
<input |
||||
hidden |
||||
id={inputIdAHHost} |
||||
readOnly |
||||
data-handler="host" |
||||
data-host-id={hostId} |
||||
data-host-number={hostNumber} |
||||
data-host-type={hostType} |
||||
/> |
||||
<FlexBox> |
||||
<Grid |
||||
columns={GRID_COLUMNS} |
||||
layout={{ |
||||
...networkListGridLayout, |
||||
[inputCellIdAHIpmiIp]: { |
||||
children: ( |
||||
<InputWithRef |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
baseInputProps={{ |
||||
'data-handler': 'ipmi', |
||||
'data-host-id': hostId, |
||||
}} |
||||
id={inputIdAHIpmiIp} |
||||
label={INPUT_LABEL_AH_IPMI_IP} |
||||
value={previousIpmiIp} |
||||
/> |
||||
} |
||||
inputTestBatch={buildIPAddressTestBatch( |
||||
`${hostId} ${INPUT_LABEL_AH_IPMI_IP}`, |
||||
() => { |
||||
setMessage(inputIdAHIpmiIp); |
||||
}, |
||||
{ |
||||
onFinishBatch: |
||||
buildFinishInputTestBatchFunction(inputIdAHIpmiIp), |
||||
}, |
||||
(message) => { |
||||
setMessage(inputIdAHIpmiIp, { children: message }); |
||||
}, |
||||
)} |
||||
onFirstRender={buildInputFirstRenderFunction( |
||||
inputIdAHIpmiIp, |
||||
)} |
||||
onUnmount={buildInputUnmountFunction(inputIdAHIpmiIp)} |
||||
required |
||||
/> |
||||
), |
||||
}, |
||||
...fenceListGridLayout, |
||||
}} |
||||
spacing={GRID_SPACING} |
||||
/> |
||||
{upsListGrid} |
||||
</FlexBox> |
||||
</InnerPanelBody> |
||||
</InnerPanel> |
||||
); |
||||
}; |
||||
|
||||
export { |
||||
INPUT_ID_PREFIX_AN_HOST, |
||||
MAP_TO_AH_INPUT_HANDLER, |
||||
buildInputIdAHFencePort, |
||||
buildInputIdAHIpmiIp, |
||||
buildInputIdAHNetworkIp, |
||||
buildInputIdAHUpsPowerHost, |
||||
}; |
||||
|
||||
export default AnHostInputGroup; |
@ -0,0 +1,135 @@ |
||||
import { ReactElement } from 'react'; |
||||
|
||||
import Grid from '../Grid'; |
||||
import InputWithRef from '../InputWithRef'; |
||||
import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; |
||||
import { |
||||
buildNumberTestBatch, |
||||
buildPeacefulStringTestBatch, |
||||
} from '../../lib/test_input'; |
||||
|
||||
const INPUT_ID_PREFIX_AN_ID = 'an-id-input'; |
||||
|
||||
const INPUT_ID_AI_DOMAIN = `${INPUT_ID_PREFIX_AN_ID}-domain`; |
||||
const INPUT_ID_AI_PREFIX = `${INPUT_ID_PREFIX_AN_ID}-prefix`; |
||||
const INPUT_ID_AI_SEQUENCE = `${INPUT_ID_PREFIX_AN_ID}-sequence`; |
||||
|
||||
const INPUT_LABEL_AI_DOMAIN = 'Domain name'; |
||||
const INPUT_LABEL_AI_PREFIX = 'Prefix'; |
||||
const INPUT_LABEL_AI_SEQUENCE = 'Sequence'; |
||||
|
||||
const AnIdInputGroup = < |
||||
M extends { |
||||
[K in |
||||
| typeof INPUT_ID_AI_DOMAIN |
||||
| typeof INPUT_ID_AI_PREFIX |
||||
| typeof INPUT_ID_AI_SEQUENCE]: string; |
||||
}, |
||||
>({ |
||||
formUtils: { |
||||
buildFinishInputTestBatchFunction, |
||||
buildInputFirstRenderFunction, |
||||
setMessage, |
||||
}, |
||||
previous: { |
||||
domain: previousDomain, |
||||
prefix: previousPrefix, |
||||
sequence: previousSequence, |
||||
} = {}, |
||||
}: AnIdInputGroupProps<M>): ReactElement => ( |
||||
<Grid |
||||
columns={{ xs: 1, sm: 2, md: 3 }} |
||||
layout={{ |
||||
'an-id-input-cell-prefix': { |
||||
children: ( |
||||
<InputWithRef |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={INPUT_ID_AI_PREFIX} |
||||
label={INPUT_LABEL_AI_PREFIX} |
||||
value={previousPrefix} |
||||
/> |
||||
} |
||||
inputTestBatch={buildPeacefulStringTestBatch( |
||||
INPUT_LABEL_AI_PREFIX, |
||||
() => { |
||||
setMessage(INPUT_ID_AI_PREFIX); |
||||
}, |
||||
{ |
||||
onFinishBatch: |
||||
buildFinishInputTestBatchFunction(INPUT_ID_AI_PREFIX), |
||||
}, |
||||
(message) => { |
||||
setMessage(INPUT_ID_AI_PREFIX, { children: message }); |
||||
}, |
||||
)} |
||||
onFirstRender={buildInputFirstRenderFunction(INPUT_ID_AI_PREFIX)} |
||||
required |
||||
/> |
||||
), |
||||
}, |
||||
'an-id-input-cell-domain': { |
||||
children: ( |
||||
<InputWithRef |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={INPUT_ID_AI_DOMAIN} |
||||
label={INPUT_LABEL_AI_DOMAIN} |
||||
value={previousDomain} |
||||
/> |
||||
} |
||||
inputTestBatch={buildPeacefulStringTestBatch( |
||||
INPUT_LABEL_AI_DOMAIN, |
||||
() => { |
||||
setMessage(INPUT_ID_AI_DOMAIN); |
||||
}, |
||||
{ |
||||
onFinishBatch: |
||||
buildFinishInputTestBatchFunction(INPUT_ID_AI_DOMAIN), |
||||
}, |
||||
(message) => { |
||||
setMessage(INPUT_ID_AI_DOMAIN, { children: message }); |
||||
}, |
||||
)} |
||||
onFirstRender={buildInputFirstRenderFunction(INPUT_ID_AI_DOMAIN)} |
||||
required |
||||
/> |
||||
), |
||||
}, |
||||
'an-id-input-cell-sequence': { |
||||
children: ( |
||||
<InputWithRef |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={INPUT_ID_AI_SEQUENCE} |
||||
label={INPUT_LABEL_AI_SEQUENCE} |
||||
value={previousSequence} |
||||
/> |
||||
} |
||||
inputTestBatch={buildNumberTestBatch( |
||||
INPUT_LABEL_AI_SEQUENCE, |
||||
() => { |
||||
setMessage(INPUT_ID_AI_SEQUENCE); |
||||
}, |
||||
{ |
||||
onFinishBatch: |
||||
buildFinishInputTestBatchFunction(INPUT_ID_AI_SEQUENCE), |
||||
}, |
||||
(message) => { |
||||
setMessage(INPUT_ID_AI_SEQUENCE, { children: message }); |
||||
}, |
||||
)} |
||||
onFirstRender={buildInputFirstRenderFunction(INPUT_ID_AI_SEQUENCE)} |
||||
required |
||||
valueType="number" |
||||
/> |
||||
), |
||||
}, |
||||
}} |
||||
spacing="1em" |
||||
/> |
||||
); |
||||
|
||||
export { INPUT_ID_AI_DOMAIN, INPUT_ID_AI_PREFIX, INPUT_ID_AI_SEQUENCE }; |
||||
|
||||
export default AnIdInputGroup; |
@ -0,0 +1,388 @@ |
||||
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 assertIfn = (type: string) => type === 'ifn'; |
||||
const assertMn = (type: string) => type === 'mn'; |
||||
|
||||
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<M>): 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<SelectItem[]>( |
||||
() => |
||||
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<ManifestNetwork> = {}): { |
||||
network: ManifestNetwork; |
||||
networkId: string; |
||||
} => ({ |
||||
network: { |
||||
networkGateway, |
||||
networkMinIp, |
||||
networkNumber, |
||||
networkSubnetMask, |
||||
networkType, |
||||
}, |
||||
networkId: uuidv4(), |
||||
}), |
||||
[getNetworkNumber], |
||||
); |
||||
|
||||
const setNetwork = useCallback( |
||||
(key: string, value?: ManifestNetwork) => |
||||
setNetworkList(buildObjectStateSetterCallback(key, value)), |
||||
[setNetworkList], |
||||
); |
||||
|
||||
const handleNetworkTypeChange = useCallback<AnNetworkTypeChangeEventHandler>( |
||||
( |
||||
{ networkId: targetId, networkType: previousType }, |
||||
{ target: { value } }, |
||||
) => { |
||||
const newType = String(value); |
||||
|
||||
let isIdMatch = false; |
||||
let newTypeNumber = 0; |
||||
|
||||
const newList = networkListEntries.reduce<ManifestNetworkList>( |
||||
(previous, [networkId, networkValue]) => { |
||||
const { networkNumber: initnn, networkType: initnt } = 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; |
||||
} |
||||
|
||||
previous[networkId] = { |
||||
...networkValue, |
||||
networkNumber, |
||||
networkType, |
||||
}; |
||||
} else { |
||||
previous[networkId] = networkValue; |
||||
} |
||||
|
||||
return previous; |
||||
}, |
||||
{}, |
||||
); |
||||
|
||||
setNetworkList(newList); |
||||
}, |
||||
[networkListEntries, setMessageRe, setNetworkList], |
||||
); |
||||
|
||||
const handleNetworkRemove = useCallback<AnNetworkCloseEventHandler>( |
||||
({ networkId: rmId, networkType: rmType }) => { |
||||
let isIdMatch = false; |
||||
let networkNumber = 0; |
||||
|
||||
const newList = networkListEntries.reduce<ManifestNetworkList>( |
||||
(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<GridLayout>(() => { |
||||
let result: GridLayout = {}; |
||||
|
||||
result = networkListEntries.reduce<GridLayout>( |
||||
( |
||||
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: ( |
||||
<AnNetworkInputGroup |
||||
formUtils={formUtils} |
||||
networkId={networkId} |
||||
networkNumber={networkNumber} |
||||
networkType={networkType} |
||||
networkTypeOptions={networkTypeOptions} |
||||
onClose={handleNetworkRemove} |
||||
onNetworkTypeChange={handleNetworkTypeChange} |
||||
previous={{ |
||||
gateway: networkGateway, |
||||
minIp: networkMinIp, |
||||
subnetMask: networkSubnetMask, |
||||
}} |
||||
readonlyNetworkName={!isOptional} |
||||
showCloseButton={isOptional} |
||||
showGateway={isIfn} |
||||
/> |
||||
), |
||||
md: 3, |
||||
sm: 2, |
||||
}; |
||||
|
||||
return previous; |
||||
}, |
||||
result, |
||||
); |
||||
|
||||
return result; |
||||
}, [ |
||||
formUtils, |
||||
networkListEntries, |
||||
networkTypeOptions, |
||||
handleNetworkRemove, |
||||
handleNetworkTypeChange, |
||||
]); |
||||
|
||||
return ( |
||||
<Grid |
||||
columns={{ xs: 1, sm: 2, md: 3 }} |
||||
layout={{ |
||||
...networksGridLayout, |
||||
'an-network-config-cell-add-network': { |
||||
children: ( |
||||
<IconButton |
||||
mapPreset="add" |
||||
onClick={() => { |
||||
const { network: newNet, networkId: newNetId } = buildNetwork(); |
||||
|
||||
setNetwork(newNetId, newNet); |
||||
}} |
||||
/> |
||||
), |
||||
display: 'flex', |
||||
justifyContent: 'center', |
||||
md: 3, |
||||
sm: 2, |
||||
}, |
||||
'an-network-config-input-cell-dns': { |
||||
children: ( |
||||
<InputWithRef |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={INPUT_ID_ANC_DNS} |
||||
label={INPUT_LABEL_ANC_DNS} |
||||
value={previousDnsCsv} |
||||
/> |
||||
} |
||||
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: ( |
||||
<InputWithRef |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={INPUT_ID_ANC_NTP} |
||||
label={INPUT_LABEL_ANC_NTP} |
||||
value={previousNtpCsv} |
||||
/> |
||||
} |
||||
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: ( |
||||
<InputWithRef |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={INPUT_ID_ANC_MTU} |
||||
inputProps={{ placeholder: '1500' }} |
||||
label={INPUT_LABEL_ANC_MTU} |
||||
value={previousMtu} |
||||
/> |
||||
} |
||||
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; |
@ -0,0 +1,345 @@ |
||||
import { ReactElement, ReactNode, useMemo } from 'react'; |
||||
|
||||
import NETWORK_TYPES from '../../lib/consts/NETWORK_TYPES'; |
||||
|
||||
import Grid from '../Grid'; |
||||
import IconButton from '../IconButton'; |
||||
import InputWithRef from '../InputWithRef'; |
||||
import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; |
||||
import { InnerPanel, InnerPanelBody, InnerPanelHeader } from '../Panels'; |
||||
import SelectWithLabel from '../SelectWithLabel'; |
||||
import { buildIPAddressTestBatch } from '../../lib/test_input'; |
||||
|
||||
const INPUT_ID_PREFIX_AN_NETWORK = 'an-network-input'; |
||||
|
||||
const INPUT_CELL_ID_PREFIX_AN = `${INPUT_ID_PREFIX_AN_NETWORK}-cell`; |
||||
|
||||
const MAP_TO_AN_INPUT_HANDLER: MapToManifestFormInputHandler = { |
||||
gateway: (container, input) => { |
||||
const { |
||||
dataset: { networkId = '' }, |
||||
value, |
||||
} = input; |
||||
const { |
||||
networkConfig: { networks }, |
||||
} = container; |
||||
|
||||
networks[networkId].networkGateway = value; |
||||
}, |
||||
minip: (container, input) => { |
||||
const { |
||||
dataset: { networkId = '' }, |
||||
value, |
||||
} = input; |
||||
const { |
||||
networkConfig: { networks }, |
||||
} = container; |
||||
|
||||
networks[networkId].networkMinIp = value; |
||||
}, |
||||
network: (container, input) => { |
||||
const { |
||||
dataset: { networkId = '', networkNumber: rawNn = '', networkType = '' }, |
||||
} = input; |
||||
const { |
||||
networkConfig: { networks }, |
||||
} = container; |
||||
const networkNumber = Number.parseInt(rawNn, 10); |
||||
|
||||
networks[networkId] = { |
||||
networkNumber, |
||||
networkType, |
||||
} as ManifestNetwork; |
||||
}, |
||||
subnetmask: (container, input) => { |
||||
const { |
||||
dataset: { networkId = '' }, |
||||
value, |
||||
} = input; |
||||
const { |
||||
networkConfig: { networks }, |
||||
} = container; |
||||
|
||||
networks[networkId].networkSubnetMask = value; |
||||
}, |
||||
}; |
||||
|
||||
const buildInputIdANGateway = (networkId: string): string => |
||||
`${INPUT_ID_PREFIX_AN_NETWORK}-${networkId}-gateway`; |
||||
|
||||
const buildInputIdANMinIp = (networkId: string): string => |
||||
`${INPUT_ID_PREFIX_AN_NETWORK}-${networkId}-min-ip`; |
||||
|
||||
const buildInputIdANNetworkType = (networkId: string): string => |
||||
`${INPUT_ID_PREFIX_AN_NETWORK}-${networkId}-network-type`; |
||||
|
||||
const buildInputIdANSubnetMask = (networkId: string): string => |
||||
`${INPUT_ID_PREFIX_AN_NETWORK}-${networkId}-subnet-mask`; |
||||
|
||||
const AnNetworkInputGroup = <M extends MapToInputTestID>({ |
||||
formUtils: { |
||||
buildFinishInputTestBatchFunction, |
||||
buildInputFirstRenderFunction, |
||||
buildInputUnmountFunction, |
||||
setMessage, |
||||
}, |
||||
inputGatewayLabel = 'Gateway', |
||||
inputMinIpLabel = 'IP address', |
||||
inputSubnetMaskLabel = 'Subnet mask', |
||||
networkId, |
||||
networkNumber, |
||||
networkType, |
||||
networkTypeOptions, |
||||
onClose, |
||||
onNetworkTypeChange, |
||||
previous: { |
||||
gateway: previousGateway, |
||||
minIp: previousIpAddress, |
||||
subnetMask: previousSubnetMask, |
||||
} = {}, |
||||
readonlyNetworkName: isReadonlyNetworkName, |
||||
showCloseButton: isShowCloseButton, |
||||
showGateway: isShowGateway, |
||||
}: AnNetworkInputGroupProps<M>): ReactElement => { |
||||
const networkName = useMemo( |
||||
() => `${NETWORK_TYPES[networkType]} ${networkNumber}`, |
||||
[networkNumber, networkType], |
||||
); |
||||
|
||||
const inputCellIdGateway = useMemo( |
||||
() => `${INPUT_CELL_ID_PREFIX_AN}-${networkId}-gateway`, |
||||
[networkId], |
||||
); |
||||
const inputCellIdIp = useMemo( |
||||
() => `${INPUT_CELL_ID_PREFIX_AN}-${networkId}-ip`, |
||||
[networkId], |
||||
); |
||||
const inputCellIdSubnetMask = useMemo( |
||||
() => `${INPUT_CELL_ID_PREFIX_AN}-${networkId}-subnet-mask`, |
||||
[networkId], |
||||
); |
||||
|
||||
const inputIdANNetwork = useMemo( |
||||
() => `${INPUT_ID_PREFIX_AN_NETWORK}-${networkId}`, |
||||
[networkId], |
||||
); |
||||
|
||||
const inputIdGateway = useMemo( |
||||
() => buildInputIdANGateway(networkId), |
||||
[networkId], |
||||
); |
||||
const inputIdMinIp = useMemo( |
||||
() => buildInputIdANMinIp(networkId), |
||||
[networkId], |
||||
); |
||||
const inputIdNetworkType = useMemo( |
||||
() => buildInputIdANNetworkType(networkId), |
||||
[networkId], |
||||
); |
||||
const inputIdSubnetMask = useMemo( |
||||
() => buildInputIdANSubnetMask(networkId), |
||||
[networkId], |
||||
); |
||||
|
||||
const inputCellGatewayDisplay = useMemo( |
||||
() => (isShowGateway ? undefined : 'none'), |
||||
[isShowGateway], |
||||
); |
||||
|
||||
const closeButtonElement = useMemo<ReactNode>( |
||||
() => |
||||
isShowCloseButton && ( |
||||
<IconButton |
||||
mapPreset="close" |
||||
iconProps={{ fontSize: 'small' }} |
||||
onClick={(...args) => { |
||||
onClose?.call(null, { networkId, networkType }, ...args); |
||||
}} |
||||
sx={{ |
||||
padding: '.2em', |
||||
position: 'absolute', |
||||
right: '-.6rem', |
||||
top: '-.2rem', |
||||
}} |
||||
/> |
||||
), |
||||
[isShowCloseButton, networkId, networkType, onClose], |
||||
); |
||||
|
||||
const inputGatewayElement = useMemo<ReactNode>(() => { |
||||
let result: ReactNode; |
||||
|
||||
if (isShowGateway && inputIdGateway) { |
||||
result = ( |
||||
<InputWithRef |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
baseInputProps={{ |
||||
'data-handler': 'gateway', |
||||
'data-network-id': networkId, |
||||
}} |
||||
id={inputIdGateway} |
||||
label={inputGatewayLabel} |
||||
value={previousGateway} |
||||
/> |
||||
} |
||||
inputTestBatch={buildIPAddressTestBatch( |
||||
`${networkName} ${inputGatewayLabel}`, |
||||
() => { |
||||
setMessage(inputIdGateway); |
||||
}, |
||||
{ |
||||
onFinishBatch: buildFinishInputTestBatchFunction(inputIdGateway), |
||||
}, |
||||
(message) => { |
||||
setMessage(inputIdGateway, { children: message }); |
||||
}, |
||||
)} |
||||
onFirstRender={buildInputFirstRenderFunction(inputIdGateway)} |
||||
onUnmount={buildInputUnmountFunction(inputIdGateway)} |
||||
required={isShowGateway} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
return result; |
||||
}, [ |
||||
isShowGateway, |
||||
inputIdGateway, |
||||
networkId, |
||||
inputGatewayLabel, |
||||
previousGateway, |
||||
networkName, |
||||
buildFinishInputTestBatchFunction, |
||||
buildInputFirstRenderFunction, |
||||
buildInputUnmountFunction, |
||||
setMessage, |
||||
]); |
||||
|
||||
return ( |
||||
<InnerPanel mv={0}> |
||||
<InnerPanelHeader> |
||||
<InputWithRef |
||||
input={ |
||||
<SelectWithLabel |
||||
id={inputIdNetworkType} |
||||
isReadOnly={isReadonlyNetworkName} |
||||
onChange={(...args) => { |
||||
onNetworkTypeChange?.call( |
||||
null, |
||||
{ networkId, networkType }, |
||||
...args, |
||||
); |
||||
}} |
||||
selectItems={networkTypeOptions} |
||||
selectProps={{ |
||||
renderValue: () => networkName, |
||||
}} |
||||
value={networkType} |
||||
/> |
||||
} |
||||
/> |
||||
{closeButtonElement} |
||||
</InnerPanelHeader> |
||||
<InnerPanelBody> |
||||
<input |
||||
hidden |
||||
id={inputIdANNetwork} |
||||
readOnly |
||||
data-handler="network" |
||||
data-network-id={networkId} |
||||
data-network-number={networkNumber} |
||||
data-network-type={networkType} |
||||
/> |
||||
<Grid |
||||
layout={{ |
||||
[inputCellIdIp]: { |
||||
children: ( |
||||
<InputWithRef |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
baseInputProps={{ |
||||
'data-handler': 'minip', |
||||
'data-network-id': networkId, |
||||
}} |
||||
id={inputIdMinIp} |
||||
label={inputMinIpLabel} |
||||
value={previousIpAddress} |
||||
/> |
||||
} |
||||
inputTestBatch={buildIPAddressTestBatch( |
||||
`${networkName} ${inputMinIpLabel}`, |
||||
() => { |
||||
setMessage(inputIdMinIp); |
||||
}, |
||||
{ |
||||
onFinishBatch: |
||||
buildFinishInputTestBatchFunction(inputIdMinIp), |
||||
}, |
||||
(message) => { |
||||
setMessage(inputIdMinIp, { children: message }); |
||||
}, |
||||
)} |
||||
onFirstRender={buildInputFirstRenderFunction(inputIdMinIp)} |
||||
onUnmount={buildInputUnmountFunction(inputIdMinIp)} |
||||
required |
||||
/> |
||||
), |
||||
}, |
||||
[inputCellIdSubnetMask]: { |
||||
children: ( |
||||
<InputWithRef |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
baseInputProps={{ |
||||
'data-handler': 'subnetmask', |
||||
'data-network-id': networkId, |
||||
}} |
||||
id={inputIdSubnetMask} |
||||
label={inputSubnetMaskLabel} |
||||
value={previousSubnetMask} |
||||
/> |
||||
} |
||||
inputTestBatch={buildIPAddressTestBatch( |
||||
`${networkName} ${inputSubnetMaskLabel}`, |
||||
() => { |
||||
setMessage(inputIdSubnetMask); |
||||
}, |
||||
{ |
||||
onFinishBatch: |
||||
buildFinishInputTestBatchFunction(inputIdSubnetMask), |
||||
}, |
||||
(message) => { |
||||
setMessage(inputIdSubnetMask, { children: message }); |
||||
}, |
||||
)} |
||||
onFirstRender={buildInputFirstRenderFunction( |
||||
inputIdSubnetMask, |
||||
)} |
||||
onUnmount={buildInputUnmountFunction(inputIdSubnetMask)} |
||||
required |
||||
/> |
||||
), |
||||
}, |
||||
[inputCellIdGateway]: { |
||||
children: inputGatewayElement, |
||||
display: inputCellGatewayDisplay, |
||||
}, |
||||
}} |
||||
spacing="1em" |
||||
/> |
||||
</InnerPanelBody> |
||||
</InnerPanel> |
||||
); |
||||
}; |
||||
|
||||
export { |
||||
INPUT_ID_PREFIX_AN_NETWORK, |
||||
MAP_TO_AN_INPUT_HANDLER, |
||||
buildInputIdANGateway, |
||||
buildInputIdANMinIp, |
||||
buildInputIdANNetworkType, |
||||
buildInputIdANSubnetMask, |
||||
}; |
||||
|
||||
export default AnNetworkInputGroup; |
@ -0,0 +1,39 @@ |
||||
import { ReactElement } from 'react'; |
||||
|
||||
import { |
||||
INPUT_ID_AI_DOMAIN, |
||||
INPUT_ID_AI_PREFIX, |
||||
INPUT_ID_AI_SEQUENCE, |
||||
} from './AnIdInputGroup'; |
||||
import { |
||||
INPUT_ID_ANC_DNS, |
||||
INPUT_ID_ANC_MTU, |
||||
INPUT_ID_ANC_NTP, |
||||
} from './AnNetworkConfigInputGroup'; |
||||
import AddManifestInputGroup from './AddManifestInputGroup'; |
||||
|
||||
const EditManifestInputGroup = < |
||||
M extends { |
||||
[K in |
||||
| typeof INPUT_ID_AI_DOMAIN |
||||
| typeof INPUT_ID_AI_PREFIX |
||||
| typeof INPUT_ID_AI_SEQUENCE |
||||
| typeof INPUT_ID_ANC_DNS |
||||
| typeof INPUT_ID_ANC_MTU |
||||
| typeof INPUT_ID_ANC_NTP]: string; |
||||
}, |
||||
>({ |
||||
formUtils, |
||||
knownFences, |
||||
knownUpses, |
||||
previous, |
||||
}: EditManifestInputGroupProps<M>): ReactElement => ( |
||||
<AddManifestInputGroup |
||||
formUtils={formUtils} |
||||
knownFences={knownFences} |
||||
knownUpses={knownUpses} |
||||
previous={previous} |
||||
/> |
||||
); |
||||
|
||||
export default EditManifestInputGroup; |
@ -0,0 +1,572 @@ |
||||
import { FC, ReactNode, useCallback, useMemo, useRef, useState } from 'react'; |
||||
|
||||
import API_BASE_URL from '../../lib/consts/API_BASE_URL'; |
||||
|
||||
import AddManifestInputGroup from './AddManifestInputGroup'; |
||||
import { |
||||
INPUT_ID_AI_DOMAIN, |
||||
INPUT_ID_AI_PREFIX, |
||||
INPUT_ID_AI_SEQUENCE, |
||||
} from './AnIdInputGroup'; |
||||
import { |
||||
INPUT_ID_PREFIX_AN_HOST, |
||||
MAP_TO_AH_INPUT_HANDLER, |
||||
} from './AnHostInputGroup'; |
||||
import { |
||||
INPUT_ID_PREFIX_AN_NETWORK, |
||||
MAP_TO_AN_INPUT_HANDLER, |
||||
} from './AnNetworkInputGroup'; |
||||
import { |
||||
INPUT_ID_ANC_DNS, |
||||
INPUT_ID_ANC_MTU, |
||||
INPUT_ID_ANC_NTP, |
||||
} from './AnNetworkConfigInputGroup'; |
||||
import api from '../../lib/api'; |
||||
import ConfirmDialog from '../ConfirmDialog'; |
||||
import EditManifestInputGroup from './EditManifestInputGroup'; |
||||
import FlexBox from '../FlexBox'; |
||||
import FormDialog from '../FormDialog'; |
||||
import handleAPIError from '../../lib/handleAPIError'; |
||||
import IconButton from '../IconButton'; |
||||
import List from '../List'; |
||||
import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup'; |
||||
import { Panel, PanelHeader } from '../Panels'; |
||||
import periodicFetch from '../../lib/fetchers/periodicFetch'; |
||||
import RunManifestInputGroup, { |
||||
buildInputIdRMHost, |
||||
INPUT_ID_RM_AN_CONFIRM_PASSWORD, |
||||
INPUT_ID_RM_AN_DESCRIPTION, |
||||
INPUT_ID_RM_AN_PASSWORD, |
||||
} from './RunManifestInputGroup'; |
||||
import Spinner from '../Spinner'; |
||||
import { BodyText, HeaderText } from '../Text'; |
||||
import useConfirmDialogProps from '../../hooks/useConfirmDialogProps'; |
||||
import useFormUtils from '../../hooks/useFormUtils'; |
||||
import useIsFirstRender from '../../hooks/useIsFirstRender'; |
||||
import useProtectedState from '../../hooks/useProtectedState'; |
||||
|
||||
const MSG_ID_API = 'api'; |
||||
|
||||
const getFormData = ( |
||||
...[{ target }]: DivFormEventHandlerParameters |
||||
): APIBuildManifestRequestBody => { |
||||
const { elements } = target as HTMLFormElement; |
||||
|
||||
const { value: domain } = elements.namedItem( |
||||
INPUT_ID_AI_DOMAIN, |
||||
) as HTMLInputElement; |
||||
const { value: prefix } = elements.namedItem( |
||||
INPUT_ID_AI_PREFIX, |
||||
) as HTMLInputElement; |
||||
const { value: rawSequence } = elements.namedItem( |
||||
INPUT_ID_AI_SEQUENCE, |
||||
) as HTMLInputElement; |
||||
const { value: dnsCsv } = elements.namedItem( |
||||
INPUT_ID_ANC_DNS, |
||||
) as HTMLInputElement; |
||||
const { value: rawMtu } = elements.namedItem( |
||||
INPUT_ID_ANC_MTU, |
||||
) as HTMLInputElement; |
||||
const { value: ntpCsv } = elements.namedItem( |
||||
INPUT_ID_ANC_NTP, |
||||
) as HTMLInputElement; |
||||
|
||||
const mtu = Number.parseInt(rawMtu, 10); |
||||
const sequence = Number.parseInt(rawSequence, 10); |
||||
|
||||
return Object.values(elements).reduce<APIBuildManifestRequestBody>( |
||||
(previous, element) => { |
||||
const { id: inputId } = element; |
||||
|
||||
if (RegExp(`^${INPUT_ID_PREFIX_AN_HOST}`).test(inputId)) { |
||||
const input = element as HTMLInputElement; |
||||
|
||||
const { |
||||
dataset: { handler: key = '' }, |
||||
} = input; |
||||
|
||||
MAP_TO_AH_INPUT_HANDLER[key]?.call(null, previous, input); |
||||
} else if (RegExp(`^${INPUT_ID_PREFIX_AN_NETWORK}`).test(inputId)) { |
||||
const input = element as HTMLInputElement; |
||||
|
||||
const { |
||||
dataset: { handler: key = '' }, |
||||
} = input; |
||||
|
||||
MAP_TO_AN_INPUT_HANDLER[key]?.call(null, previous, input); |
||||
} |
||||
|
||||
return previous; |
||||
}, |
||||
{ |
||||
domain, |
||||
hostConfig: { hosts: {} }, |
||||
networkConfig: { |
||||
dnsCsv, |
||||
mtu, |
||||
networks: {}, |
||||
ntpCsv, |
||||
}, |
||||
prefix, |
||||
sequence, |
||||
}, |
||||
); |
||||
}; |
||||
|
||||
const getRunFormData = ( |
||||
mdetailHosts: ManifestHostList, |
||||
...[{ target }]: DivFormEventHandlerParameters |
||||
): APIRunManifestRequestBody => { |
||||
const { elements } = target as HTMLFormElement; |
||||
|
||||
const { value: description } = elements.namedItem( |
||||
INPUT_ID_RM_AN_DESCRIPTION, |
||||
) as HTMLInputElement; |
||||
const { value: password } = elements.namedItem( |
||||
INPUT_ID_RM_AN_PASSWORD, |
||||
) as HTMLInputElement; |
||||
|
||||
const hosts = Object.entries(mdetailHosts).reduce< |
||||
APIRunManifestRequestBody['hosts'] |
||||
>((previous, [hostId, { hostNumber, hostType }]) => { |
||||
const inputId = buildInputIdRMHost(hostId); |
||||
const { value: hostUuid } = elements.namedItem(inputId) as HTMLInputElement; |
||||
|
||||
previous[hostId] = { hostNumber, hostType, hostUuid }; |
||||
|
||||
return previous; |
||||
}, {}); |
||||
|
||||
return { description, hosts, password }; |
||||
}; |
||||
|
||||
const ManageManifestPanel: FC = () => { |
||||
const isFirstRender = useIsFirstRender(); |
||||
|
||||
const confirmDialogRef = useRef<ConfirmDialogForwardedRefContent>({}); |
||||
const addManifestFormDialogRef = useRef<ConfirmDialogForwardedRefContent>({}); |
||||
const editManifestFormDialogRef = useRef<ConfirmDialogForwardedRefContent>( |
||||
{}, |
||||
); |
||||
const runManifestFormDialogRef = useRef<ConfirmDialogForwardedRefContent>({}); |
||||
const messageGroupRef = useRef<MessageGroupForwardedRefContent>({}); |
||||
|
||||
const [confirmDialogProps, setConfirmDialogProps] = useConfirmDialogProps(); |
||||
|
||||
const [hostOverviews, setHostOverviews] = useProtectedState< |
||||
APIHostOverviewList | undefined |
||||
>(undefined); |
||||
const [isEditManifests, setIsEditManifests] = useState<boolean>(false); |
||||
const [isLoadingHostOverviews, setIsLoadingHostOverviews] = |
||||
useProtectedState<boolean>(true); |
||||
const [isLoadingManifestDetail, setIsLoadingManifestDetail] = |
||||
useProtectedState<boolean>(true); |
||||
const [isLoadingManifestTemplate, setIsLoadingManifestTemplate] = |
||||
useProtectedState<boolean>(true); |
||||
const [isSubmittingForm, setIsSubmittingForm] = |
||||
useProtectedState<boolean>(false); |
||||
const [manifestDetail, setManifestDetail] = useProtectedState< |
||||
APIManifestDetail | undefined |
||||
>(undefined); |
||||
const [manifestTemplate, setManifestTemplate] = useProtectedState< |
||||
APIManifestTemplate | undefined |
||||
>(undefined); |
||||
|
||||
const { data: manifestOverviews, isLoading: isLoadingManifestOverviews } = |
||||
periodicFetch<APIManifestOverviewList>(`${API_BASE_URL}/manifest`, { |
||||
refreshInterval: 60000, |
||||
}); |
||||
|
||||
const formUtils = useFormUtils( |
||||
[ |
||||
INPUT_ID_AI_DOMAIN, |
||||
INPUT_ID_AI_PREFIX, |
||||
INPUT_ID_AI_SEQUENCE, |
||||
INPUT_ID_ANC_DNS, |
||||
INPUT_ID_ANC_MTU, |
||||
INPUT_ID_ANC_NTP, |
||||
], |
||||
messageGroupRef, |
||||
); |
||||
const { isFormInvalid, setMessage } = formUtils; |
||||
|
||||
const runFormUtils = useFormUtils( |
||||
[ |
||||
INPUT_ID_RM_AN_CONFIRM_PASSWORD, |
||||
INPUT_ID_RM_AN_DESCRIPTION, |
||||
INPUT_ID_RM_AN_PASSWORD, |
||||
], |
||||
messageGroupRef, |
||||
); |
||||
const { isFormInvalid: isRunFormInvalid } = runFormUtils; |
||||
|
||||
const { |
||||
hostConfig: { hosts: mdetailHosts = {} } = {}, |
||||
name: mdetailName, |
||||
uuid: mdetailUuid, |
||||
} = useMemo<Partial<APIManifestDetail>>( |
||||
() => manifestDetail ?? {}, |
||||
[manifestDetail], |
||||
); |
||||
const { |
||||
domain: mtemplateDomain, |
||||
fences: knownFences, |
||||
prefix: mtemplatePrefix, |
||||
sequence: mtemplateSequence, |
||||
upses: knownUpses, |
||||
} = useMemo<Partial<APIManifestTemplate>>( |
||||
() => manifestTemplate ?? {}, |
||||
[manifestTemplate], |
||||
); |
||||
|
||||
const submitForm = useCallback( |
||||
({ |
||||
body, |
||||
getErrorMsg, |
||||
method, |
||||
successMsg, |
||||
url, |
||||
}: { |
||||
body: Record<string, unknown>; |
||||
getErrorMsg: (parentMsg: ReactNode) => ReactNode; |
||||
method: 'post' | 'put'; |
||||
successMsg: ReactNode; |
||||
url: string; |
||||
}) => { |
||||
setIsSubmittingForm(true); |
||||
|
||||
api[method](url, body) |
||||
.then(() => { |
||||
setMessage(MSG_ID_API, { |
||||
children: successMsg, |
||||
}); |
||||
}) |
||||
.catch((apiError) => { |
||||
const emsg = handleAPIError(apiError); |
||||
|
||||
emsg.children = getErrorMsg(emsg.children); |
||||
setMessage(MSG_ID_API, emsg); |
||||
}) |
||||
.finally(() => { |
||||
setIsSubmittingForm(false); |
||||
}); |
||||
}, |
||||
[setIsSubmittingForm, setMessage], |
||||
); |
||||
|
||||
const addManifestFormDialogProps = useMemo<ConfirmDialogProps>( |
||||
() => ({ |
||||
actionProceedText: 'Add', |
||||
content: ( |
||||
<AddManifestInputGroup |
||||
formUtils={formUtils} |
||||
knownFences={knownFences} |
||||
knownUpses={knownUpses} |
||||
previous={{ |
||||
domain: mtemplateDomain, |
||||
prefix: mtemplatePrefix, |
||||
sequence: mtemplateSequence, |
||||
}} |
||||
/> |
||||
), |
||||
onSubmitAppend: (...args) => { |
||||
const body = getFormData(...args); |
||||
|
||||
setConfirmDialogProps({ |
||||
actionProceedText: 'Add', |
||||
content: <></>, |
||||
onProceedAppend: () => { |
||||
submitForm({ |
||||
body, |
||||
getErrorMsg: (parentMsg) => ( |
||||
<>Failed to add install manifest. {parentMsg}</> |
||||
), |
||||
method: 'post', |
||||
successMsg: 'Successfully added install manifest', |
||||
url: '/manifest', |
||||
}); |
||||
}, |
||||
titleText: `Add install manifest?`, |
||||
}); |
||||
|
||||
confirmDialogRef.current.setOpen?.call(null, true); |
||||
}, |
||||
titleText: 'Add an install manifest', |
||||
}), |
||||
[ |
||||
formUtils, |
||||
knownFences, |
||||
knownUpses, |
||||
mtemplateDomain, |
||||
mtemplatePrefix, |
||||
mtemplateSequence, |
||||
setConfirmDialogProps, |
||||
submitForm, |
||||
], |
||||
); |
||||
|
||||
const editManifestFormDialogProps = useMemo<ConfirmDialogProps>( |
||||
() => ({ |
||||
actionProceedText: 'Edit', |
||||
content: ( |
||||
<EditManifestInputGroup |
||||
formUtils={formUtils} |
||||
knownFences={knownFences} |
||||
knownUpses={knownUpses} |
||||
previous={manifestDetail} |
||||
/> |
||||
), |
||||
onSubmitAppend: (...args) => { |
||||
const body = getFormData(...args); |
||||
|
||||
setConfirmDialogProps({ |
||||
actionProceedText: 'Edit', |
||||
content: <></>, |
||||
onProceedAppend: () => { |
||||
submitForm({ |
||||
body, |
||||
getErrorMsg: (parentMsg) => ( |
||||
<>Failed to update install manifest. {parentMsg}</> |
||||
), |
||||
method: 'put', |
||||
successMsg: `Successfully updated install manifest ${mdetailName}`, |
||||
url: `/manifest/${mdetailUuid}`, |
||||
}); |
||||
}, |
||||
titleText: `Update install manifest ${mdetailName}?`, |
||||
}); |
||||
|
||||
confirmDialogRef.current.setOpen?.call(null, true); |
||||
}, |
||||
loading: isLoadingManifestDetail, |
||||
titleText: `Update install manifest ${mdetailName}`, |
||||
}), |
||||
[ |
||||
formUtils, |
||||
knownFences, |
||||
knownUpses, |
||||
manifestDetail, |
||||
isLoadingManifestDetail, |
||||
mdetailName, |
||||
setConfirmDialogProps, |
||||
submitForm, |
||||
mdetailUuid, |
||||
], |
||||
); |
||||
|
||||
const runManifestFormDialogProps = useMemo<ConfirmDialogProps>( |
||||
() => ({ |
||||
actionProceedText: 'Run', |
||||
content: ( |
||||
<RunManifestInputGroup |
||||
formUtils={runFormUtils} |
||||
knownFences={knownFences} |
||||
knownHosts={hostOverviews} |
||||
knownUpses={knownUpses} |
||||
previous={manifestDetail} |
||||
/> |
||||
), |
||||
loading: isLoadingManifestDetail, |
||||
onSubmitAppend: (...args) => { |
||||
const body = getRunFormData(mdetailHosts, ...args); |
||||
|
||||
setConfirmDialogProps({ |
||||
actionProceedText: 'Run', |
||||
content: <></>, |
||||
onProceedAppend: () => { |
||||
submitForm({ |
||||
body, |
||||
getErrorMsg: (parentMsg) => ( |
||||
<>Failed to run install manifest. {parentMsg}</> |
||||
), |
||||
method: 'put', |
||||
successMsg: `Successfully ran install manifest ${mdetailName}`, |
||||
url: `/command/run-manifest/${mdetailUuid}`, |
||||
}); |
||||
}, |
||||
titleText: `Run install manifest ${mdetailName}?`, |
||||
}); |
||||
|
||||
confirmDialogRef.current.setOpen?.call(null, true); |
||||
}, |
||||
titleText: `Run install manifest ${mdetailName}`, |
||||
}), |
||||
[ |
||||
runFormUtils, |
||||
knownFences, |
||||
hostOverviews, |
||||
knownUpses, |
||||
manifestDetail, |
||||
isLoadingManifestDetail, |
||||
mdetailName, |
||||
mdetailHosts, |
||||
setConfirmDialogProps, |
||||
submitForm, |
||||
mdetailUuid, |
||||
], |
||||
); |
||||
|
||||
const getManifestDetail = useCallback( |
||||
(manifestUuid: string, finallyAppend?: () => void) => { |
||||
setIsLoadingManifestDetail(true); |
||||
|
||||
api |
||||
.get<APIManifestDetail>(`manifest/${manifestUuid}`) |
||||
.then(({ data }) => { |
||||
data.uuid = manifestUuid; |
||||
|
||||
setManifestDetail(data); |
||||
}) |
||||
.catch((error) => { |
||||
handleAPIError(error); |
||||
}) |
||||
.finally(() => { |
||||
setIsLoadingManifestDetail(false); |
||||
finallyAppend?.call(null); |
||||
}); |
||||
}, |
||||
[setIsLoadingManifestDetail, setManifestDetail], |
||||
); |
||||
|
||||
const listElement = useMemo( |
||||
() => ( |
||||
<List |
||||
allowEdit |
||||
allowItemButton={isEditManifests} |
||||
edit={isEditManifests} |
||||
header |
||||
listEmpty="No manifest(s) registered." |
||||
listItems={manifestOverviews} |
||||
onAdd={() => { |
||||
addManifestFormDialogRef.current.setOpen?.call(null, true); |
||||
}} |
||||
onEdit={() => { |
||||
setIsEditManifests((previous) => !previous); |
||||
}} |
||||
onItemClick={({ manifestName, manifestUUID }) => { |
||||
setManifestDetail({ |
||||
name: manifestName, |
||||
uuid: manifestUUID, |
||||
} as APIManifestDetail); |
||||
editManifestFormDialogRef.current.setOpen?.call(null, true); |
||||
getManifestDetail(manifestUUID); |
||||
}} |
||||
renderListItem={(manifestUUID, { manifestName }) => ( |
||||
<FlexBox fullWidth row> |
||||
<IconButton |
||||
disabled={isEditManifests} |
||||
mapPreset="play" |
||||
onClick={() => { |
||||
setManifestDetail({ |
||||
name: manifestName, |
||||
uuid: manifestUUID, |
||||
} as APIManifestDetail); |
||||
runManifestFormDialogRef.current.setOpen?.call(null, true); |
||||
getManifestDetail(manifestUUID); |
||||
}} |
||||
variant="normal" |
||||
/> |
||||
<BodyText>{manifestName}</BodyText> |
||||
</FlexBox> |
||||
)} |
||||
/> |
||||
), |
||||
[getManifestDetail, isEditManifests, manifestOverviews, setManifestDetail], |
||||
); |
||||
|
||||
const panelContent = useMemo( |
||||
() => |
||||
isLoadingHostOverviews || |
||||
isLoadingManifestTemplate || |
||||
isLoadingManifestOverviews ? ( |
||||
<Spinner /> |
||||
) : ( |
||||
listElement |
||||
), |
||||
[ |
||||
isLoadingHostOverviews, |
||||
isLoadingManifestOverviews, |
||||
isLoadingManifestTemplate, |
||||
listElement, |
||||
], |
||||
); |
||||
|
||||
const messageArea = useMemo( |
||||
() => ( |
||||
<MessageGroup |
||||
count={1} |
||||
defaultMessageType="warning" |
||||
ref={messageGroupRef} |
||||
/> |
||||
), |
||||
[], |
||||
); |
||||
|
||||
if (isFirstRender) { |
||||
api |
||||
.get<APIManifestTemplate>('/manifest/template') |
||||
.then(({ data }) => { |
||||
setManifestTemplate(data); |
||||
}) |
||||
.catch((error) => { |
||||
handleAPIError(error); |
||||
}) |
||||
.finally(() => { |
||||
setIsLoadingManifestTemplate(false); |
||||
}); |
||||
|
||||
api |
||||
.get<APIHostOverviewList>('/host', { params: { types: 'node' } }) |
||||
.then(({ data }) => { |
||||
setHostOverviews(data); |
||||
}) |
||||
.catch((apiError) => { |
||||
handleAPIError(apiError); |
||||
}) |
||||
.finally(() => { |
||||
setIsLoadingHostOverviews(false); |
||||
}); |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
<Panel> |
||||
<PanelHeader> |
||||
<HeaderText>Manage manifests</HeaderText> |
||||
</PanelHeader> |
||||
{panelContent} |
||||
</Panel> |
||||
<FormDialog |
||||
{...addManifestFormDialogProps} |
||||
disableProceed={isFormInvalid} |
||||
loadingAction={isSubmittingForm} |
||||
preActionArea={messageArea} |
||||
ref={addManifestFormDialogRef} |
||||
scrollContent |
||||
/> |
||||
<FormDialog |
||||
{...editManifestFormDialogProps} |
||||
disableProceed={isFormInvalid} |
||||
loadingAction={isSubmittingForm} |
||||
preActionArea={messageArea} |
||||
ref={editManifestFormDialogRef} |
||||
scrollContent |
||||
/> |
||||
<FormDialog |
||||
{...runManifestFormDialogProps} |
||||
disableProceed={isRunFormInvalid} |
||||
loadingAction={isSubmittingForm} |
||||
preActionArea={messageArea} |
||||
ref={runManifestFormDialogRef} |
||||
scrollContent |
||||
/> |
||||
<ConfirmDialog |
||||
closeOnProceed |
||||
{...confirmDialogProps} |
||||
ref={confirmDialogRef} |
||||
/> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default ManageManifestPanel; |
@ -0,0 +1,454 @@ |
||||
import { styled } from '@mui/material'; |
||||
import { ReactElement, useMemo, useRef } from 'react'; |
||||
|
||||
import INPUT_TYPES from '../../lib/consts/INPUT_TYPES'; |
||||
|
||||
import FlexBox from '../FlexBox'; |
||||
import Grid from '../Grid'; |
||||
import InputWithRef, { InputForwardedRefContent } from '../InputWithRef'; |
||||
import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; |
||||
import SelectWithLabel from '../SelectWithLabel'; |
||||
import { buildPeacefulStringTestBatch } from '../../lib/test_input'; |
||||
import { BodyText, MonoText } from '../Text'; |
||||
|
||||
const INPUT_ID_PREFIX_RUN_MANIFEST = 'run-manifest-input'; |
||||
const INPUT_ID_PREFIX_RM_HOST = `${INPUT_ID_PREFIX_RUN_MANIFEST}-host`; |
||||
|
||||
const INPUT_ID_RM_AN_DESCRIPTION = `${INPUT_ID_PREFIX_RUN_MANIFEST}-an-description`; |
||||
const INPUT_ID_RM_AN_PASSWORD = `${INPUT_ID_PREFIX_RUN_MANIFEST}-an-password`; |
||||
const INPUT_ID_RM_AN_CONFIRM_PASSWORD = `${INPUT_ID_PREFIX_RUN_MANIFEST}-an-confirm-password`; |
||||
|
||||
const INPUT_LABEL_RM_AN_DESCRIPTION = 'Description'; |
||||
const INPUT_LABEL_RM_AN_PASSWORD = 'Password'; |
||||
const INPUT_LABEL_RM_AN_CONFIRM_PASSWORD = 'Confirm password'; |
||||
|
||||
const MANIFEST_PARAM_NONE = '--'; |
||||
|
||||
const EndMono = styled(MonoText)({ |
||||
justifyContent: 'end', |
||||
}); |
||||
|
||||
const buildInputIdRMHost = (hostId: string): string => |
||||
`${INPUT_ID_PREFIX_RM_HOST}-${hostId}`; |
||||
|
||||
const RunManifestInputGroup = <M extends MapToInputTestID>({ |
||||
formUtils: { |
||||
buildFinishInputTestBatchFunction, |
||||
buildInputFirstRenderFunction, |
||||
setMessage, |
||||
}, |
||||
knownFences = {}, |
||||
knownHosts = {}, |
||||
knownUpses = {}, |
||||
previous: { domain: anDomain, hostConfig = {}, networkConfig = {} } = {}, |
||||
}: RunManifestInputGroupProps<M>): ReactElement => { |
||||
const passwordRef = useRef<InputForwardedRefContent<'string'>>({}); |
||||
|
||||
const { hosts: initHostList = {} } = hostConfig; |
||||
const { |
||||
dnsCsv, |
||||
mtu, |
||||
networks: initNetworkList = {}, |
||||
ntpCsv = MANIFEST_PARAM_NONE, |
||||
} = networkConfig; |
||||
|
||||
const hostListEntries = useMemo( |
||||
() => Object.entries(initHostList), |
||||
[initHostList], |
||||
); |
||||
const knownFenceListEntries = useMemo( |
||||
() => Object.entries(knownFences), |
||||
[knownFences], |
||||
); |
||||
const knownHostListEntries = useMemo( |
||||
() => Object.entries(knownHosts), |
||||
[knownHosts], |
||||
); |
||||
const knownUpsListEntries = useMemo( |
||||
() => Object.entries(knownUpses), |
||||
[knownUpses], |
||||
); |
||||
const networkListEntries = useMemo( |
||||
() => Object.entries(initNetworkList), |
||||
[initNetworkList], |
||||
); |
||||
|
||||
const hostOptionList = useMemo( |
||||
() => |
||||
knownHostListEntries.map<SelectItem>(([, { hostName, hostUUID }]) => ({ |
||||
displayValue: hostName, |
||||
value: hostUUID, |
||||
})), |
||||
[knownHostListEntries], |
||||
); |
||||
|
||||
const { |
||||
headers: hostHeaderRow, |
||||
hosts: hostSelectRow, |
||||
hostNames: hostNewNameRow, |
||||
} = useMemo( |
||||
() => |
||||
hostListEntries.reduce<{ |
||||
headers: GridLayout; |
||||
hosts: GridLayout; |
||||
hostNames: GridLayout; |
||||
}>( |
||||
(previous, [hostId, { hostName, hostNumber, hostType }]) => { |
||||
const { headers, hosts, hostNames } = previous; |
||||
|
||||
const prettyId = `${hostType}${hostNumber}`; |
||||
|
||||
headers[`run-manifest-column-header-cell-${hostId}`] = { |
||||
children: <BodyText>{prettyId}</BodyText>, |
||||
}; |
||||
|
||||
const inputId = buildInputIdRMHost(hostId); |
||||
const inputLabel = `${prettyId} host`; |
||||
|
||||
hosts[`run-manifest-host-cell-${hostId}`] = { |
||||
children: ( |
||||
<InputWithRef |
||||
input={ |
||||
<SelectWithLabel |
||||
id={inputId} |
||||
label={inputLabel} |
||||
selectItems={hostOptionList} |
||||
value="" |
||||
/> |
||||
} |
||||
inputTestBatch={buildPeacefulStringTestBatch( |
||||
inputLabel, |
||||
() => { |
||||
setMessage(inputId); |
||||
}, |
||||
{ onFinishBatch: buildFinishInputTestBatchFunction(inputId) }, |
||||
(message) => { |
||||
setMessage(inputId, { children: message }); |
||||
}, |
||||
)} |
||||
onFirstRender={buildInputFirstRenderFunction(inputId)} |
||||
required |
||||
/> |
||||
), |
||||
}; |
||||
|
||||
hostNames[`run-manifest-new-host-name-cell-${hostId}`] = { |
||||
children: ( |
||||
<MonoText> |
||||
{hostName}.{anDomain} |
||||
</MonoText> |
||||
), |
||||
}; |
||||
|
||||
return previous; |
||||
}, |
||||
{ |
||||
headers: { |
||||
'run-manifest-column-header-cell-offset': {}, |
||||
}, |
||||
hosts: { |
||||
'run-manifest-host-cell-header': { |
||||
children: <BodyText>Uses host</BodyText>, |
||||
}, |
||||
}, |
||||
hostNames: { |
||||
'run-manifest-new-host-name-cell-header': { |
||||
children: <BodyText>New hostname</BodyText>, |
||||
}, |
||||
}, |
||||
}, |
||||
), |
||||
[ |
||||
anDomain, |
||||
buildFinishInputTestBatchFunction, |
||||
buildInputFirstRenderFunction, |
||||
hostListEntries, |
||||
hostOptionList, |
||||
setMessage, |
||||
], |
||||
); |
||||
|
||||
const { |
||||
gateway: defaultGatewayGridLayout, |
||||
hostNetworks: hostNetworkRowList, |
||||
} = useMemo( |
||||
() => |
||||
networkListEntries.reduce<{ |
||||
gateway: GridLayout; |
||||
hostNetworks: GridLayout; |
||||
}>( |
||||
( |
||||
previous, |
||||
[networkId, { networkGateway, networkNumber, networkType }], |
||||
) => { |
||||
const { gateway, hostNetworks } = previous; |
||||
|
||||
const idPrefix = `run-manifest-host-network-cell-${networkId}`; |
||||
|
||||
const networkShortName = `${networkType.toUpperCase()}${networkNumber}`; |
||||
|
||||
hostNetworks[`${idPrefix}-header`] = { |
||||
children: <BodyText>{networkShortName}</BodyText>, |
||||
}; |
||||
|
||||
hostListEntries.forEach(([hostId, { networks = {} }]) => { |
||||
const { |
||||
[networkId]: { networkIp: ip = MANIFEST_PARAM_NONE } = {}, |
||||
} = networks; |
||||
|
||||
hostNetworks[`${idPrefix}-${hostId}-ip`] = { |
||||
children: <MonoText>{ip}</MonoText>, |
||||
}; |
||||
}); |
||||
|
||||
const cellId = 'run-manifest-gateway-cell'; |
||||
|
||||
if (networkGateway && !gateway[cellId]) { |
||||
gateway[cellId] = { |
||||
children: <EndMono>{networkGateway}</EndMono>, |
||||
}; |
||||
} |
||||
|
||||
return previous; |
||||
}, |
||||
{ |
||||
gateway: { |
||||
'run-manifest-gateway-cell-header': { |
||||
children: <BodyText>Gateway</BodyText>, |
||||
}, |
||||
}, |
||||
hostNetworks: {}, |
||||
}, |
||||
), |
||||
[hostListEntries, networkListEntries], |
||||
); |
||||
|
||||
const hostFenceRowList = useMemo( |
||||
() => |
||||
knownFenceListEntries.reduce<GridLayout>( |
||||
(previous, [fenceUuid, { fenceName }]) => { |
||||
const idPrefix = `run-manifest-fence-cell-${fenceUuid}`; |
||||
|
||||
previous[`${idPrefix}-header`] = { |
||||
children: <BodyText>Port on {fenceName}</BodyText>, |
||||
}; |
||||
|
||||
hostListEntries.forEach(([hostId, { fences = {} }]) => { |
||||
const { [fenceName]: { fencePort = MANIFEST_PARAM_NONE } = {} } = |
||||
fences; |
||||
|
||||
previous[`${idPrefix}-${hostId}-port`] = { |
||||
children: <MonoText>{fencePort}</MonoText>, |
||||
}; |
||||
}); |
||||
|
||||
return previous; |
||||
}, |
||||
{}, |
||||
), |
||||
[hostListEntries, knownFenceListEntries], |
||||
); |
||||
|
||||
const hostUpsRowList = useMemo( |
||||
() => |
||||
knownUpsListEntries.reduce<GridLayout>( |
||||
(previous, [upsUuid, { upsName }]) => { |
||||
const idPrefix = `run-manifest-ups-cell-${upsUuid}`; |
||||
|
||||
previous[`${idPrefix}-header`] = { |
||||
children: <BodyText>Uses {upsName}</BodyText>, |
||||
}; |
||||
|
||||
hostListEntries.forEach(([hostId, { upses = {} }]) => { |
||||
const { [upsName]: { isUsed = false } = {} } = upses; |
||||
|
||||
previous[`${idPrefix}-${hostId}-is-used`] = { |
||||
children: <MonoText>{isUsed ? 'yes' : 'no'}</MonoText>, |
||||
}; |
||||
}); |
||||
|
||||
return previous; |
||||
}, |
||||
{}, |
||||
), |
||||
[hostListEntries, knownUpsListEntries], |
||||
); |
||||
|
||||
const confirmPasswordProps = useMemo(() => { |
||||
const inputTestBatch = buildPeacefulStringTestBatch( |
||||
INPUT_LABEL_RM_AN_CONFIRM_PASSWORD, |
||||
() => { |
||||
setMessage(INPUT_ID_RM_AN_CONFIRM_PASSWORD); |
||||
}, |
||||
{ |
||||
onFinishBatch: buildFinishInputTestBatchFunction( |
||||
INPUT_ID_RM_AN_CONFIRM_PASSWORD, |
||||
), |
||||
}, |
||||
(message) => { |
||||
setMessage(INPUT_ID_RM_AN_CONFIRM_PASSWORD, { children: message }); |
||||
}, |
||||
); |
||||
|
||||
const onFirstRender = buildInputFirstRenderFunction( |
||||
INPUT_ID_RM_AN_CONFIRM_PASSWORD, |
||||
); |
||||
|
||||
inputTestBatch.tests.push({ |
||||
onFailure: () => { |
||||
setMessage(INPUT_ID_RM_AN_CONFIRM_PASSWORD, { |
||||
children: <>Confirm password must match password.</>, |
||||
}); |
||||
}, |
||||
test: ({ value }) => passwordRef.current.getValue?.call(null) === value, |
||||
}); |
||||
|
||||
return { |
||||
inputTestBatch, |
||||
onFirstRender, |
||||
}; |
||||
}, [ |
||||
buildFinishInputTestBatchFunction, |
||||
buildInputFirstRenderFunction, |
||||
setMessage, |
||||
]); |
||||
|
||||
return ( |
||||
<FlexBox> |
||||
<Grid |
||||
columns={{ xs: 1, sm: 2 }} |
||||
layout={{ |
||||
'run-manifest-input-cell-an-description': { |
||||
children: ( |
||||
<InputWithRef |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={INPUT_ID_RM_AN_DESCRIPTION} |
||||
label={INPUT_LABEL_RM_AN_DESCRIPTION} |
||||
/> |
||||
} |
||||
inputTestBatch={buildPeacefulStringTestBatch( |
||||
INPUT_LABEL_RM_AN_DESCRIPTION, |
||||
() => { |
||||
setMessage(INPUT_ID_RM_AN_DESCRIPTION); |
||||
}, |
||||
{ |
||||
onFinishBatch: buildFinishInputTestBatchFunction( |
||||
INPUT_ID_RM_AN_DESCRIPTION, |
||||
), |
||||
}, |
||||
(message) => { |
||||
setMessage(INPUT_ID_RM_AN_DESCRIPTION, { |
||||
children: message, |
||||
}); |
||||
}, |
||||
)} |
||||
onFirstRender={buildInputFirstRenderFunction( |
||||
INPUT_ID_RM_AN_DESCRIPTION, |
||||
)} |
||||
required |
||||
/> |
||||
), |
||||
sm: 2, |
||||
}, |
||||
'run-manifest-input-cell-an-password': { |
||||
children: ( |
||||
<InputWithRef |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={INPUT_ID_RM_AN_PASSWORD} |
||||
label={INPUT_LABEL_RM_AN_PASSWORD} |
||||
type={INPUT_TYPES.password} |
||||
/> |
||||
} |
||||
inputTestBatch={buildPeacefulStringTestBatch( |
||||
INPUT_LABEL_RM_AN_PASSWORD, |
||||
() => { |
||||
setMessage(INPUT_ID_RM_AN_PASSWORD); |
||||
}, |
||||
{ |
||||
onFinishBatch: buildFinishInputTestBatchFunction( |
||||
INPUT_ID_RM_AN_PASSWORD, |
||||
), |
||||
}, |
||||
(message) => { |
||||
setMessage(INPUT_ID_RM_AN_PASSWORD, { children: message }); |
||||
}, |
||||
)} |
||||
onFirstRender={buildInputFirstRenderFunction( |
||||
INPUT_ID_RM_AN_PASSWORD, |
||||
)} |
||||
ref={passwordRef} |
||||
required |
||||
/> |
||||
), |
||||
}, |
||||
'run-manifest-input-cell-an-confirm-password': { |
||||
children: ( |
||||
<InputWithRef |
||||
input={ |
||||
<OutlinedInputWithLabel |
||||
id={INPUT_ID_RM_AN_CONFIRM_PASSWORD} |
||||
label={INPUT_LABEL_RM_AN_CONFIRM_PASSWORD} |
||||
type={INPUT_TYPES.password} |
||||
/> |
||||
} |
||||
required |
||||
{...confirmPasswordProps} |
||||
/> |
||||
), |
||||
}, |
||||
}} |
||||
spacing="1em" |
||||
/> |
||||
<Grid |
||||
alignItems="center" |
||||
columns={{ xs: hostListEntries.length + 1 }} |
||||
layout={{ |
||||
...hostHeaderRow, |
||||
...hostSelectRow, |
||||
...hostNewNameRow, |
||||
...hostNetworkRowList, |
||||
...hostFenceRowList, |
||||
...hostUpsRowList, |
||||
}} |
||||
columnSpacing="1em" |
||||
rowSpacing="0.4em" |
||||
/> |
||||
<Grid |
||||
columns={{ xs: 2 }} |
||||
layout={{ |
||||
...defaultGatewayGridLayout, |
||||
'run-manifest-dns-csv-cell-header': { |
||||
children: <BodyText>DNS</BodyText>, |
||||
}, |
||||
'run-manifest-dns-csv-cell': { |
||||
children: <EndMono>{dnsCsv}</EndMono>, |
||||
}, |
||||
'run-manifest-ntp-csv-cell-header': { |
||||
children: <BodyText>NTP</BodyText>, |
||||
}, |
||||
'run-manifest-ntp-csv-cell': { |
||||
children: <EndMono>{ntpCsv}</EndMono>, |
||||
}, |
||||
'run-manifest-mtu-cell-header': { |
||||
children: <BodyText>MTU</BodyText>, |
||||
}, |
||||
'run-manifest-mtu-cell': { |
||||
children: <EndMono>{mtu}</EndMono>, |
||||
}, |
||||
}} |
||||
spacing="0.4em" |
||||
/> |
||||
</FlexBox> |
||||
); |
||||
}; |
||||
|
||||
export { |
||||
INPUT_ID_RM_AN_CONFIRM_PASSWORD, |
||||
INPUT_ID_RM_AN_DESCRIPTION, |
||||
INPUT_ID_RM_AN_PASSWORD, |
||||
buildInputIdRMHost, |
||||
}; |
||||
|
||||
export default RunManifestInputGroup; |
@ -0,0 +1,3 @@ |
||||
import ManageManifestPanel from './ManageManifestPanel'; |
||||
|
||||
export default ManageManifestPanel; |
@ -1,17 +1,20 @@ |
||||
import { Box, BoxProps } from '@mui/material'; |
||||
import { FC } from 'react'; |
||||
import { Box, BoxProps, SxProps, Theme } from '@mui/material'; |
||||
import { FC, useMemo } from 'react'; |
||||
|
||||
const InnerPanelBody: FC<BoxProps> = ({ sx, ...innerPanelBodyRestProps }) => ( |
||||
<Box |
||||
{...{ |
||||
...innerPanelBodyRestProps, |
||||
sx: { |
||||
padding: '.3em .7em', |
||||
const InnerPanelBody: FC<BoxProps> = ({ sx, ...innerPanelBodyRestProps }) => { |
||||
const combinedSx = useMemo<SxProps<Theme>>( |
||||
() => ({ |
||||
position: 'relative', |
||||
zIndex: 20, |
||||
|
||||
...sx, |
||||
}, |
||||
}} |
||||
/> |
||||
); |
||||
}), |
||||
[sx], |
||||
); |
||||
|
||||
return ( |
||||
<Box padding=".3em .7em" {...innerPanelBodyRestProps} sx={combinedSx} /> |
||||
); |
||||
}; |
||||
|
||||
export default InnerPanelBody; |
||||
|
@ -1,9 +1,74 @@ |
||||
/** |
||||
* Checks whether specified `key` is unset in given object. Always returns |
||||
* `true` when overwrite is allowed. |
||||
*/ |
||||
const checkUnset = <S extends BaseObject>( |
||||
obj: S, |
||||
key: keyof S, |
||||
{ isOverwrite = false }: { isOverwrite?: boolean } = {}, |
||||
): boolean => !(key in obj) || isOverwrite; |
||||
|
||||
const defaultObjectStatePropSetter = <S extends BaseObject>( |
||||
...[, result, key, value]: Parameters<ObjectStatePropSetter<S>> |
||||
): ReturnType<ObjectStatePropSetter<S>> => { |
||||
if (value !== undefined) { |
||||
result[key] = value; |
||||
} |
||||
}; |
||||
|
||||
const buildObjectStateSetterCallback = |
||||
<S extends Record<string, unknown>>(key: keyof S, value: S[keyof S]) => |
||||
({ [key]: toReplace, ...restPrevious }: S): S => |
||||
({ |
||||
...restPrevious, |
||||
[key]: value, |
||||
} as S); |
||||
<S extends BaseObject>( |
||||
key: keyof S, |
||||
value?: S[keyof S], |
||||
{ |
||||
guard = () => true, |
||||
set = defaultObjectStatePropSetter, |
||||
}: BuildObjectStateSetterCallbackOptions<S> = {}, |
||||
): BuildObjectStateSetterCallbackReturnType<S> => |
||||
(previous: S): S => { |
||||
const { [key]: toReplace, ...restPrevious } = previous; |
||||
const result = { ...restPrevious } as S; |
||||
|
||||
if (guard(previous, key, value)) { |
||||
set(previous, result, key, value); |
||||
} |
||||
|
||||
return result; |
||||
}; |
||||
|
||||
export const buildProtectedObjectStateSetterCallback = <S extends BaseObject>( |
||||
key: keyof S, |
||||
value?: S[keyof S], |
||||
{ |
||||
isOverwrite, |
||||
guard = (o, k) => checkUnset(o, k, { isOverwrite }), |
||||
set, |
||||
}: BuildObjectStateSetterCallbackOptions<S> = {}, |
||||
): BuildObjectStateSetterCallbackReturnType<S> => |
||||
buildObjectStateSetterCallback(key, value, { isOverwrite, guard, set }); |
||||
|
||||
export const buildRegExpObjectStateSetterCallback = |
||||
<S extends BaseObject>( |
||||
re: RegExp, |
||||
value?: S[keyof S], |
||||
{ |
||||
set = defaultObjectStatePropSetter, |
||||
}: Pick<BuildObjectStateSetterCallbackOptions<S>, 'set'> = {}, |
||||
) => |
||||
(previous: S): S => { |
||||
const result: S = {} as S; |
||||
|
||||
Object.keys(previous).forEach((key) => { |
||||
const k = key as keyof S; |
||||
|
||||
if (re.test(key)) { |
||||
set(previous, result, k, value); |
||||
} else { |
||||
result[k] = previous[k]; |
||||
} |
||||
}); |
||||
|
||||
return result; |
||||
}; |
||||
|
||||
export default buildObjectStateSetterCallback; |
||||
|
@ -1,6 +1,8 @@ |
||||
const NETWORK_TYPES: Record<string, string> = { |
||||
bcn: 'Back-Channel Network', |
||||
ifn: 'Internet-Facing Network', |
||||
mn: 'Migration Network', |
||||
sn: 'Storage Network', |
||||
}; |
||||
|
||||
export default NETWORK_TYPES; |
||||
|
@ -0,0 +1,33 @@ |
||||
import { REP_IPV4_CSV } from '../consts/REG_EXP_PATTERNS'; |
||||
|
||||
import testNotBlank from './testNotBlank'; |
||||
|
||||
const buildIpCsvTestBatch: BuildInputTestBatchFunction = ( |
||||
inputName, |
||||
onSuccess, |
||||
{ isRequired, onFinishBatch, ...defaults } = {}, |
||||
onIpCsvTestFailure, |
||||
) => ({ |
||||
defaults: { ...defaults, onSuccess }, |
||||
isRequired, |
||||
onFinishBatch, |
||||
tests: [ |
||||
{ |
||||
test: testNotBlank, |
||||
}, |
||||
{ |
||||
onFailure: (...args) => { |
||||
onIpCsvTestFailure( |
||||
<> |
||||
{inputName} must be one or more valid IPv4 addresses separated by |
||||
comma(s); without trailing comma. |
||||
</>, |
||||
...args, |
||||
); |
||||
}, |
||||
test: ({ value }) => REP_IPV4_CSV.test(value as string), |
||||
}, |
||||
], |
||||
}); |
||||
|
||||
export default buildIpCsvTestBatch; |
@ -0,0 +1,55 @@ |
||||
type APIManifestOverview = { |
||||
manifestName: string; |
||||
manifestUUID: string; |
||||
}; |
||||
|
||||
type APIManifestOverviewList = { |
||||
[manifestUUID: string]: APIManifestOverview; |
||||
}; |
||||
|
||||
type APIManifestDetail = ManifestAnId & { |
||||
hostConfig: ManifestHostConfig; |
||||
name: string; |
||||
networkConfig: ManifestNetworkConfig; |
||||
uuid?: string; |
||||
}; |
||||
|
||||
type APIManifestTemplateFence = { |
||||
fenceName: string; |
||||
fenceUUID: string; |
||||
}; |
||||
|
||||
type APIManifestTemplateUps = { |
||||
upsName: string; |
||||
upsUUID: string; |
||||
}; |
||||
|
||||
type APIManifestTemplateFenceList = { |
||||
[fenceUuid: string]: APIManifestTemplateFence; |
||||
}; |
||||
|
||||
type APIManifestTemplateUpsList = { |
||||
[upsUuid: string]: APIManifestTemplateUps; |
||||
}; |
||||
|
||||
type APIManifestTemplate = { |
||||
domain: string; |
||||
fences: APIManifestTemplateFenceList; |
||||
prefix: string; |
||||
sequence: number; |
||||
upses: APIManifestTemplateUpsList; |
||||
}; |
||||
|
||||
type APIBuildManifestRequestBody = Omit<APIManifestDetail, 'name' | 'uuid'>; |
||||
|
||||
type APIRunManifestRequestBody = { |
||||
description: string; |
||||
hosts: { |
||||
[hostId: string]: { |
||||
hostNumber: number; |
||||
hostType: string; |
||||
hostUuid: string; |
||||
}; |
||||
}; |
||||
password: string; |
||||
}; |
@ -0,0 +1,24 @@ |
||||
type BaseObject<T = unknown> = Record<number | string | symbol, T>; |
||||
|
||||
type ObjectStatePropGuard<S extends BaseObject> = ( |
||||
previous: S, |
||||
key: keyof S, |
||||
value?: S[keyof S], |
||||
) => boolean; |
||||
|
||||
type ObjectStatePropSetter<S extends BaseObject> = ( |
||||
previous: S, |
||||
result: S, |
||||
key: keyof S, |
||||
value?: S[keyof S], |
||||
) => void; |
||||
|
||||
type BuildObjectStateSetterCallbackOptions<S extends BaseObject> = { |
||||
guard?: ObjectStatePropGuard<S>; |
||||
isOverwrite?: boolean; |
||||
set?: ObjectStatePropSetter<S>; |
||||
}; |
||||
|
||||
type BuildObjectStateSetterCallbackReturnType<S extends BaseObject> = ( |
||||
previous: S, |
||||
) => S; |
@ -1,16 +1,29 @@ |
||||
type CreatableComponent = Parameters<typeof import('react').createElement>[0]; |
||||
|
||||
type IconButtonPresetMapToStateIcon = 'edit' | 'visibility'; |
||||
type IconButtonPresetMapToStateIconBundle = |
||||
| 'add' |
||||
| 'close' |
||||
| 'edit' |
||||
| 'play' |
||||
| 'visibility'; |
||||
|
||||
type IconButtonMapToStateIcon = Record<string, CreatableComponent>; |
||||
type IconButtonStateIconBundle = { |
||||
iconType: CreatableComponent; |
||||
iconProps?: import('@mui/material').SvgIconProps; |
||||
}; |
||||
|
||||
type IconButtonMapToStateIconBundle = Record<string, IconButtonStateIconBundle>; |
||||
|
||||
type IconButtonVariant = 'contained' | 'normal'; |
||||
|
||||
type IconButtonMouseEventHandler = |
||||
import('@mui/material').IconButtonProps['onClick']; |
||||
|
||||
type IconButtonOptionalProps = { |
||||
defaultIcon?: CreatableComponent; |
||||
iconProps?: import('@mui/material').SvgIconProps; |
||||
mapPreset?: IconButtonPresetMapToStateIcon; |
||||
mapToIcon?: IconButtonMapToStateIcon; |
||||
mapPreset?: IconButtonPresetMapToStateIconBundle; |
||||
mapToIcon?: IconButtonMapToStateIconBundle; |
||||
state?: string; |
||||
variant?: IconButtonVariant; |
||||
}; |
||||
|
@ -1 +1,7 @@ |
||||
type InnerPanelProps = import('@mui/material').BoxProps; |
||||
type InnerPanelOptionalProps = { |
||||
headerMarginOffset?: number | string; |
||||
mv?: number | string; |
||||
}; |
||||
|
||||
type InnerPanelProps = InnerPanelOptionalProps & |
||||
import('@mui/material').BoxProps; |
||||
|
@ -0,0 +1,185 @@ |
||||
type ManifestAnId = { |
||||
domain: string; |
||||
prefix: string; |
||||
sequence: number; |
||||
}; |
||||
|
||||
type ManifestNetwork = { |
||||
networkGateway?: string; |
||||
networkMinIp: string; |
||||
networkNumber: number; |
||||
networkSubnetMask: string; |
||||
networkType: string; |
||||
}; |
||||
|
||||
type ManifestNetworkList = { |
||||
[networkId: string]: ManifestNetwork; |
||||
}; |
||||
|
||||
type ManifestNetworkConfig = { |
||||
dnsCsv: string; |
||||
/** Max Transmission Unit (MTU); unit: bytes */ |
||||
mtu: number; |
||||
networks: ManifestNetworkList; |
||||
ntpCsv: string; |
||||
}; |
||||
|
||||
type ManifestHostFenceList = { |
||||
[fenceId: string]: { |
||||
fenceName: string; |
||||
fencePort: string; |
||||
}; |
||||
}; |
||||
|
||||
type ManifestHostNetworkList = { |
||||
[networkId: string]: { |
||||
networkIp: string; |
||||
networkNumber: number; |
||||
networkType: string; |
||||
}; |
||||
}; |
||||
|
||||
type ManifestHostUpsList = { |
||||
[upsId: string]: { |
||||
isUsed: boolean; |
||||
upsName: string; |
||||
}; |
||||
}; |
||||
|
||||
type ManifestHost = { |
||||
fences?: ManifestHostFenceList; |
||||
hostName?: string; |
||||
hostNumber: number; |
||||
hostType: string; |
||||
ipmiIp?: string; |
||||
networks?: ManifestHostNetworkList; |
||||
upses?: ManifestHostUpsList; |
||||
}; |
||||
|
||||
type ManifestHostList = { |
||||
[hostId: string]: ManifestHost; |
||||
}; |
||||
|
||||
type ManifestHostConfig = { |
||||
hosts: ManifestHostList; |
||||
}; |
||||
|
||||
type ManifestFormInputHandler = ( |
||||
container: APIBuildManifestRequestBody, |
||||
input: HTMLInputElement, |
||||
) => void; |
||||
|
||||
type MapToManifestFormInputHandler = Record<string, ManifestFormInputHandler>; |
||||
|
||||
/** ---------- Component types ---------- */ |
||||
|
||||
type AnIdInputGroupOptionalProps = { |
||||
previous?: Partial<ManifestAnId>; |
||||
}; |
||||
|
||||
type AnIdInputGroupProps<M extends MapToInputTestID> = |
||||
AnIdInputGroupOptionalProps & { |
||||
formUtils: FormUtils<M>; |
||||
}; |
||||
|
||||
type AnNetworkEventHandlerPreviousArgs = { |
||||
networkId: string; |
||||
} & Pick<ManifestNetwork, 'networkType'>; |
||||
|
||||
type AnNetworkCloseEventHandler = ( |
||||
args: AnNetworkEventHandlerPreviousArgs, |
||||
...handlerArgs: Parameters<IconButtonMouseEventHandler> |
||||
) => ReturnType<IconButtonMouseEventHandler>; |
||||
|
||||
type AnNetworkTypeChangeEventHandler = ( |
||||
args: AnNetworkEventHandlerPreviousArgs, |
||||
...handlerArgs: Parameters<SelectChangeEventHandler> |
||||
) => ReturnType<SelectChangeEventHandler>; |
||||
|
||||
type AnNetworkInputGroupOptionalProps = { |
||||
inputGatewayLabel?: string; |
||||
inputMinIpLabel?: string; |
||||
inputSubnetMaskLabel?: string; |
||||
onClose?: AnNetworkCloseEventHandler; |
||||
onNetworkTypeChange?: AnNetworkTypeChangeEventHandler; |
||||
previous?: { |
||||
gateway?: string; |
||||
minIp?: string; |
||||
subnetMask?: string; |
||||
}; |
||||
readonlyNetworkName?: boolean; |
||||
showCloseButton?: boolean; |
||||
showGateway?: boolean; |
||||
}; |
||||
|
||||
type AnNetworkInputGroupProps<M extends MapToInputTestID> = |
||||
AnNetworkInputGroupOptionalProps & { |
||||
formUtils: FormUtils<M>; |
||||
networkId: string; |
||||
networkNumber: number; |
||||
networkType: string; |
||||
networkTypeOptions: SelectItem[]; |
||||
}; |
||||
|
||||
type AnHostInputGroupOptionalProps = { |
||||
hostLabel?: string; |
||||
previous?: Pick<ManifestHost, 'fences' | 'ipmiIp' | 'networks' | 'upses'>; |
||||
}; |
||||
|
||||
type AnHostInputGroupProps<M extends MapToInputTestID> = |
||||
AnHostInputGroupOptionalProps & { |
||||
formUtils: FormUtils<M>; |
||||
hostId: string; |
||||
hostNumber: number; |
||||
hostType: string; |
||||
}; |
||||
|
||||
type AnNetworkConfigInputGroupOptionalProps = { |
||||
previous?: Partial<ManifestNetworkConfig>; |
||||
}; |
||||
|
||||
type AnNetworkConfigInputGroupProps<M extends MapToInputTestID> = |
||||
AnNetworkConfigInputGroupOptionalProps & { |
||||
formUtils: FormUtils<M>; |
||||
networkListEntries: Array<[string, ManifestNetwork]>; |
||||
setNetworkList: import('react').Dispatch< |
||||
import('react').SetStateAction<ManifestNetworkList> |
||||
>; |
||||
}; |
||||
|
||||
type AnHostConfigInputGroupOptionalProps = { |
||||
knownFences?: APIManifestTemplateFenceList; |
||||
knownUpses?: APIManifestTemplateUpsList; |
||||
previous?: Partial<ManifestHostConfig>; |
||||
}; |
||||
|
||||
type AnHostConfigInputGroupProps<M extends MapToInputTestID> = |
||||
AnHostConfigInputGroupOptionalProps & { |
||||
formUtils: FormUtils<M>; |
||||
networkListEntries: Array<[string, ManifestNetwork]>; |
||||
}; |
||||
|
||||
type AddManifestInputGroupOptionalProps = Pick< |
||||
AnHostConfigInputGroupOptionalProps, |
||||
'knownFences' | 'knownUpses' |
||||
> & { |
||||
previous?: Partial<ManifestAnId> & { |
||||
hostConfig?: Partial<ManifestHostConfig>; |
||||
networkConfig?: Partial<ManifestNetworkConfig>; |
||||
}; |
||||
}; |
||||
|
||||
type AddManifestInputGroupProps<M extends MapToInputTestID> = |
||||
AddManifestInputGroupOptionalProps & { |
||||
formUtils: FormUtils<M>; |
||||
}; |
||||
|
||||
type EditManifestInputGroupProps<M extends MapToInputTestID> = |
||||
AddManifestInputGroupProps<M>; |
||||
|
||||
type RunManifestInputGroupOptionalProps = { |
||||
knownHosts?: APIHostOverviewList; |
||||
}; |
||||
|
||||
type RunManifestInputGroupProps<M extends MapToInputTestID> = |
||||
RunManifestInputGroupOptionalProps & AddManifestInputGroupProps<M>; |
@ -1,3 +1,3 @@ |
||||
type MessageSetterFunction = ( |
||||
type MessageSetter = ( |
||||
message?: import('../components/MessageBox').Message, |
||||
) => void; |
||||
|
Loading…
Reference in new issue