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 './getHostSSH'; |
||||||
export * from './poweroffHost'; |
export * from './poweroffHost'; |
||||||
export * from './rebootHost'; |
export * from './rebootHost'; |
||||||
|
export * from './runManifest'; |
||||||
export * from './updateSystem'; |
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 { Box, BoxProps, SxProps, Theme } from '@mui/material'; |
||||||
import { FC } from 'react'; |
import { FC, useMemo } from 'react'; |
||||||
|
|
||||||
const InnerPanelBody: FC<BoxProps> = ({ sx, ...innerPanelBodyRestProps }) => ( |
const InnerPanelBody: FC<BoxProps> = ({ sx, ...innerPanelBodyRestProps }) => { |
||||||
<Box |
const combinedSx = useMemo<SxProps<Theme>>( |
||||||
{...{ |
() => ({ |
||||||
...innerPanelBodyRestProps, |
position: 'relative', |
||||||
sx: { |
zIndex: 20, |
||||||
padding: '.3em .7em', |
|
||||||
|
|
||||||
...sx, |
...sx, |
||||||
}, |
}), |
||||||
}} |
[sx], |
||||||
/> |
); |
||||||
); |
|
||||||
|
return ( |
||||||
|
<Box padding=".3em .7em" {...innerPanelBodyRestProps} sx={combinedSx} /> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
export default InnerPanelBody; |
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 = |
const buildObjectStateSetterCallback = |
||||||
<S extends Record<string, unknown>>(key: keyof S, value: S[keyof S]) => |
<S extends BaseObject>( |
||||||
({ [key]: toReplace, ...restPrevious }: S): S => |
key: keyof S, |
||||||
({ |
value?: S[keyof S], |
||||||
...restPrevious, |
{ |
||||||
[key]: value, |
guard = () => true, |
||||||
} as S); |
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; |
export default buildObjectStateSetterCallback; |
||||||
|
@ -1,6 +1,8 @@ |
|||||||
const NETWORK_TYPES: Record<string, string> = { |
const NETWORK_TYPES: Record<string, string> = { |
||||||
bcn: 'Back-Channel Network', |
bcn: 'Back-Channel Network', |
||||||
ifn: 'Internet-Facing Network', |
ifn: 'Internet-Facing Network', |
||||||
|
mn: 'Migration Network', |
||||||
|
sn: 'Storage Network', |
||||||
}; |
}; |
||||||
|
|
||||||
export default NETWORK_TYPES; |
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 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 IconButtonVariant = 'contained' | 'normal'; |
||||||
|
|
||||||
|
type IconButtonMouseEventHandler = |
||||||
|
import('@mui/material').IconButtonProps['onClick']; |
||||||
|
|
||||||
type IconButtonOptionalProps = { |
type IconButtonOptionalProps = { |
||||||
defaultIcon?: CreatableComponent; |
defaultIcon?: CreatableComponent; |
||||||
iconProps?: import('@mui/material').SvgIconProps; |
iconProps?: import('@mui/material').SvgIconProps; |
||||||
mapPreset?: IconButtonPresetMapToStateIcon; |
mapPreset?: IconButtonPresetMapToStateIconBundle; |
||||||
mapToIcon?: IconButtonMapToStateIcon; |
mapToIcon?: IconButtonMapToStateIconBundle; |
||||||
state?: string; |
state?: string; |
||||||
variant?: IconButtonVariant; |
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, |
message?: import('../components/MessageBox').Message, |
||||||
) => void; |
) => void; |
||||||
|
Loading…
Reference in new issue