Merge pull request #343 from ylei-tsubame/manage-manifest

Web UI: add manifests manager
main
Digimer 2 years ago committed by GitHub
commit bdbba0d59d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 71
      Anvil/Tools/Striker.pm
  2. 19
      striker-ui-api/src/lib/accessModule.ts
  3. 1
      striker-ui-api/src/lib/consts/SERVER_PATHS.ts
  4. 20
      striker-ui-api/src/lib/disassembleEntityId.ts
  5. 8
      striker-ui-api/src/lib/disassembleHostName.ts
  6. 2
      striker-ui-api/src/lib/getShortHostName.ts
  7. 1
      striker-ui-api/src/lib/request_handlers/command/index.ts
  8. 196
      striker-ui-api/src/lib/request_handlers/command/runManifest.ts
  9. 2
      striker-ui-api/src/lib/request_handlers/host/buildQueryHostDetail.ts
  10. 2
      striker-ui-api/src/lib/request_handlers/host/getHost.ts
  11. 285
      striker-ui-api/src/lib/request_handlers/manifest/buildManifest.ts
  12. 29
      striker-ui-api/src/lib/request_handlers/manifest/createManifest.ts
  13. 37
      striker-ui-api/src/lib/request_handlers/manifest/deleteManifest.ts
  14. 34
      striker-ui-api/src/lib/request_handlers/manifest/getManifest.ts
  15. 266
      striker-ui-api/src/lib/request_handlers/manifest/getManifestDetail.ts
  16. 119
      striker-ui-api/src/lib/request_handlers/manifest/getManifestTemplate.ts
  17. 6
      striker-ui-api/src/lib/request_handlers/manifest/index.ts
  18. 34
      striker-ui-api/src/lib/request_handlers/manifest/updateManifest.ts
  19. 2
      striker-ui-api/src/routes/command.ts
  20. 2
      striker-ui-api/src/routes/index.ts
  21. 23
      striker-ui-api/src/routes/manifest.ts
  22. 112
      striker-ui-api/src/types/APIManifest.d.ts
  23. 90
      striker-ui-api/src/types/GetAnvilDataFunction.d.ts
  24. 71
      striker-ui/components/ConfirmDialog.tsx
  25. 56
      striker-ui/components/IconButton/IconButton.tsx
  26. 21
      striker-ui/components/InputWithRef.tsx
  27. 1
      striker-ui/components/ManageFence/CommonFenceInputGroup.tsx
  28. 88
      striker-ui/components/ManageManifest/AddManifestInputGroup.tsx
  29. 130
      striker-ui/components/ManageManifest/AnHostConfigInputGroup.tsx
  30. 404
      striker-ui/components/ManageManifest/AnHostInputGroup.tsx
  31. 135
      striker-ui/components/ManageManifest/AnIdInputGroup.tsx
  32. 388
      striker-ui/components/ManageManifest/AnNetworkConfigInputGroup.tsx
  33. 345
      striker-ui/components/ManageManifest/AnNetworkInputGroup.tsx
  34. 39
      striker-ui/components/ManageManifest/EditManifestInputGroup.tsx
  35. 572
      striker-ui/components/ManageManifest/ManageManifestPanel.tsx
  36. 454
      striker-ui/components/ManageManifest/RunManifestInputGroup.tsx
  37. 3
      striker-ui/components/ManageManifest/index.tsx
  38. 14
      striker-ui/components/ManageUps/AddUpsInputGroup.tsx
  39. 14
      striker-ui/components/ManageUps/CommonUpsInputGroup.tsx
  40. 47
      striker-ui/components/MessageGroup.tsx
  41. 24
      striker-ui/components/NetworkInitForm.tsx
  42. 5
      striker-ui/components/OutlinedInputWithLabel.tsx
  43. 34
      striker-ui/components/Panels/InnerPanel.tsx
  44. 29
      striker-ui/components/Panels/InnerPanelBody.tsx
  45. 64
      striker-ui/components/SwitchWithLabel.tsx
  46. 63
      striker-ui/hooks/useFormUtils.ts
  47. 6
      striker-ui/lib/buildMapToMessageSetter.ts
  48. 77
      striker-ui/lib/buildObjectStateSetterCallback.ts
  49. 2
      striker-ui/lib/consts/NETWORK_TYPES.ts
  50. 4
      striker-ui/lib/test_input/buildDomainTestBatch.tsx
  51. 6
      striker-ui/lib/test_input/buildIPAddressTestBatch.tsx
  52. 33
      striker-ui/lib/test_input/buildIpCsvTestBatch.tsx
  53. 8
      striker-ui/lib/test_input/buildPeacefulStringTestBatch.tsx
  54. 6
      striker-ui/lib/test_input/buildUUIDTestBatch.tsx
  55. 4
      striker-ui/lib/test_input/index.ts
  56. 10
      striker-ui/lib/test_input/testInput.ts
  57. 56
      striker-ui/pages/manage-element/index.tsx
  58. 55
      striker-ui/types/APIManifest.d.ts
  59. 24
      striker-ui/types/BuildObjectStateSetterCallback.d.ts
  60. 7
      striker-ui/types/ConfirmDialog.d.ts
  61. 21
      striker-ui/types/FormUtils.d.ts
  62. 21
      striker-ui/types/IconButton.d.ts
  63. 8
      striker-ui/types/InnerPanel.d.ts
  64. 185
      striker-ui/types/ManageManifest.d.ts
  65. 2
      striker-ui/types/MapToMessageSetter.d.ts
  66. 2
      striker-ui/types/MessageSetterFunction.d.ts
  67. 5
      striker-ui/types/Select.d.ts
  68. 3
      striker-ui/types/SwitchWithLabel.d.ts

@ -308,68 +308,89 @@ sub generate_manifest
my $self = shift; my $self = shift;
my $parameter = shift; my $parameter = shift;
my $anvil = $self->parent; my $anvil = $self->parent;
my $debug = defined $parameter->{debug} ? $parameter->{debug} : 3; my $debug = $parameter->{debug} // 3;
my $domain = $parameter->{domain} // $anvil->data->{cgi}{domain}{value};
my $manifest_uuid = $parameter->{manifest_uuid} // $anvil->data->{cgi}{manifest_uuid}{value};
my $name_prefix = $parameter->{prefix} // $anvil->data->{cgi}{prefix}{value};
my $network_dns = $parameter->{dns} // $anvil->data->{cgi}{dns}{value};
my $network_mtu = $parameter->{mtu} // $anvil->data->{cgi}{mtu}{value};
my $network_ntp = $parameter->{ntp} // $anvil->data->{cgi}{ntp}{value};
my $padded_sequence = $parameter->{sequence} // $anvil->data->{cgi}{sequence}{value};
$anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Striker->generate_manifest()" }}); $anvil->Log->entry({source => $THIS_FILE, line => __LINE__, level => $debug, key => "log_0125", variables => { method => "Striker->generate_manifest()" }});
$anvil->Database->get_upses({debug => $debug}); $anvil->Database->get_upses({debug => $debug});
$anvil->Database->get_fences({debug => $debug}); $anvil->Database->get_fences({debug => $debug});
my $manifest_uuid = $anvil->data->{cgi}{manifest_uuid}{value} eq "new" ? "" : $anvil->data->{cgi}{manifest_uuid}{value}; $manifest_uuid = $manifest_uuid eq "new" ? "" : $manifest_uuid;
my $padded_sequence = $anvil->data->{cgi}{sequence}{value};
if (length($padded_sequence) == 1) if (length($padded_sequence) == 1)
{ {
$padded_sequence = sprintf("%02d", $padded_sequence); $padded_sequence = sprintf("%02d", $padded_sequence);
} }
my $anvil_name = $anvil->data->{cgi}{prefix}{value}."-anvil-".$padded_sequence;
my $node1_name = $anvil->data->{cgi}{prefix}{value}."-a".$padded_sequence."n01"; my $anvil_name = $name_prefix."-anvil-".$padded_sequence;
my $node2_name = $anvil->data->{cgi}{prefix}{value}."-a".$padded_sequence."n02"; my $node1_name = $name_prefix."-a".$padded_sequence."n01";
my $dr1_name = $anvil->data->{cgi}{prefix}{value}."-a".$padded_sequence."dr01"; my $node2_name = $name_prefix."-a".$padded_sequence."n02";
my $dr1_name = $name_prefix."-a".$padded_sequence."dr01";
my $machines = {}; my $machines = {};
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { anvil_name => $anvil_name }}); $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { anvil_name => $anvil_name }});
my $manifest_xml = '<?xml version="1.0" encoding="UTF-8"?> my $manifest_xml = '<?xml version="1.0" encoding="UTF-8"?>
<install_manifest name="'.$anvil_name.'" domain="'.$anvil->data->{cgi}{domain}{value}.'"> <install_manifest name="'.$anvil_name.'" domain="'.$domain.'">
<networks mtu="'.$anvil->data->{cgi}{mtu}{value}.'" dns="'.$anvil->data->{cgi}{dns}{value}.'" ntp="'.$anvil->data->{cgi}{ntp}{value}.'"> <networks mtu="'.$network_mtu.'" dns="'.$network_dns.'" ntp="'.$network_ntp.'">
'; ';
foreach my $network ("bcn", "sn", "ifn") foreach my $network ("bcn", "sn", "ifn")
{ {
my $count_key = $network."_count"; my $count_key = $network."_count";
$anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "cgi::${count_key}::value" => $anvil->data->{cgi}{$count_key}{value} }}); my $count_value = $parameter->{$count_key} // $anvil->data->{cgi}{$count_key}{value};
foreach my $i (1..$anvil->data->{cgi}{$count_key}{value}) $anvil->Log->variables({source => $THIS_FILE, line => __LINE__, level => $debug, list => { "${count_key}" => $count_value }});
foreach my $i (1..$count_value)
{ {
my $network_name = $network.$i; my $network_name = $network.$i;
my $network_key = $network_name."_network"; my $network_key = $network_name."_network";
my $subnet_key = $network_name."_subnet"; my $network_value = $parameter->{$network_key} // $anvil->data->{cgi}{$network_key}{value};
my $gateway_key = $network_name."_gateway"; my $subnet_key = $network_name."_subnet";
$manifest_xml .= ' <network name="'.$network_name.'" network="'.$anvil->data->{cgi}{$network_key}{value}.'" subnet="'.$anvil->data->{cgi}{$subnet_key}{value}.'" gateway="'.$anvil->data->{cgi}{$gateway_key}{value}.'" />'."\n"; my $subnet_value = $parameter->{$subnet_key} // $anvil->data->{cgi}{$subnet_key}{value};
my $gateway_key = $network_name."_gateway";
my $gateway_value = $parameter->{$gateway_key} // $anvil->data->{cgi}{$gateway_key}{value};
$manifest_xml .= ' <network name="'.$network_name.'" network="'.$network_value.'" subnet="'.$subnet_value.'" gateway="'.$gateway_value.'" />'."\n";
# While we're here, gather the network data for the machines. # While we're here, gather the network data for the machines.
foreach my $machine ("node1", "node2", "dr1") foreach my $machine ("node1", "node2", "dr1")
{ {
# Record the network # Record the network
my $ip_key = $machine."_".$network_name."_ip"; my $ip_key = $machine."_".$network_name."_ip";
$machines->{$machine}{network}{$network_name} = defined $anvil->data->{cgi}{$ip_key}{value} ? $anvil->data->{cgi}{$ip_key}{value} : ""; my $ip_value = ($parameter->{$ip_key} // $anvil->data->{cgi}{$ip_key}{value}) // "";
$machines->{$machine}{network}{$network_name} = $ip_value;
# On the first loop (bcn1), pull in the other information as well. # On the first loop (bcn1), pull in the other information as well.
if (($network eq "bcn") && ($i eq "1")) if (($network eq "bcn") && ($i eq "1"))
{ {
# Get the IP. # Get the IP.
my $ipmi_ip_key = $machine."_ipmi_ip"; my $ipmi_ip_key = $machine."_ipmi_ip";
$machines->{$machine}{ipmi_ip} = defined $anvil->data->{cgi}{$ipmi_ip_key}{value} ? $anvil->data->{cgi}{$ipmi_ip_key}{value} : ""; my $ipmi_ip_value = ($parameter->{$ipmi_ip_key} // $anvil->data->{cgi}{$ipmi_ip_key}{value}) // "";
$machines->{$machine}{ipmi_ip} = $ipmi_ip_value;
# Find the UPSes. # Find the UPSes.
foreach my $ups_name (sort {$a cmp $b} keys %{$anvil->data->{upses}{ups_name}}) foreach my $ups_name (sort {$a cmp $b} keys %{$anvil->data->{upses}{ups_name}})
{ {
my $ups_key = $machine."_ups_".$ups_name; my $ups_key = $machine."_ups_".$ups_name;
$anvil->data->{cgi}{$ups_key}{value} = "" if not defined $anvil->data->{cgi}{$ups_key}{value}; my $ups_value = ($parameter->{$ups_key} // $anvil->data->{cgi}{$ups_key}{value}) // "";
$machines->{$machine}{ups}{$ups_name} = $anvil->data->{cgi}{$ups_key}{value} ? "1" : "0";
$machines->{$machine}{ups}{$ups_name} = $ups_value ? "1" : "0";
} }
# Find the Fence devices. # Find the Fence devices.
foreach my $fence_name (sort {$a cmp $b} keys %{$anvil->data->{fences}{fence_name}}) foreach my $fence_name (sort {$a cmp $b} keys %{$anvil->data->{fences}{fence_name}})
{ {
my $fence_key = $machine."_fence_".$fence_name; my $fence_key = $machine."_fence_".$fence_name;
$anvil->data->{cgi}{$fence_key}{value} = "" if not defined $anvil->data->{cgi}{$fence_key}{value}; my $fence_value = ($parameter->{$fence_key} // $anvil->data->{cgi}{$fence_key}{value}) // "";
$machines->{$machine}{fence}{$fence_name} = $anvil->data->{cgi}{$fence_key}{value};
$machines->{$machine}{fence}{$fence_name} = $fence_value;
} }
} }
} }

@ -148,6 +148,22 @@ const getAnvilData = <HashType>(
spawnSyncOptions, spawnSyncOptions,
).stdout; ).stdout;
const getLocalHostName = () => {
let result: string;
try {
result = execModuleSubroutine('host_name', {
subModuleName: 'Get',
}).stdout;
} catch (subError) {
throw new Error(`Failed to get local host name; CAUSE: ${subError}`);
}
shout(`localHostName=${result}`);
return result;
};
const getLocalHostUUID = () => { const getLocalHostUUID = () => {
let result: string; let result: string;
@ -156,7 +172,7 @@ const getLocalHostUUID = () => {
subModuleName: 'Get', subModuleName: 'Get',
}).stdout; }).stdout;
} catch (subError) { } catch (subError) {
throw new Error(`Failed to get localhost UUID; CAUSE: ${subError}`); throw new Error(`Failed to get local host UUID; CAUSE: ${subError}`);
} }
shout(`localHostUUID=[${result}]`); shout(`localHostUUID=[${result}]`);
@ -201,6 +217,7 @@ export {
dbSubRefreshTimestamp, dbSubRefreshTimestamp,
dbWrite, dbWrite,
getAnvilData, getAnvilData,
getLocalHostName,
getLocalHostUUID, getLocalHostUUID,
getPeerData, getPeerData,
execModuleSubroutine as sub, execModuleSubroutine as sub,

@ -19,6 +19,7 @@ const EMPTY_SERVER_PATHS: ServerPath = {
'anvil-access-module': {}, 'anvil-access-module': {},
'anvil-configure-host': {}, 'anvil-configure-host': {},
'anvil-get-server-screenshot': {}, 'anvil-get-server-screenshot': {},
'anvil-join-anvil': {},
'anvil-manage-keys': {}, 'anvil-manage-keys': {},
'anvil-manage-power': {}, 'anvil-manage-power': {},
'anvil-provision-server': {}, 'anvil-provision-server': {},

@ -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();
};

@ -1,7 +1,7 @@
import { buildKnownIDCondition } from '../../buildCondition'; import { buildKnownIDCondition } from '../../buildCondition';
import { buildQueryResultModifier } from '../../buildQueryResultModifier'; import { buildQueryResultModifier } from '../../buildQueryResultModifier';
import { cap } from '../../cap'; import { cap } from '../../cap';
import { getShortHostName } from '../../getShortHostName'; import { getShortHostName } from '../../disassembleHostName';
import { stdout } from '../../shell'; import { stdout } from '../../shell';
type ExtractVariableKeyFunction = (parts: string[]) => string; type ExtractVariableKeyFunction = (parts: string[]) => string;

@ -4,7 +4,7 @@ import buildGetRequestHandler from '../buildGetRequestHandler';
import { buildQueryHostDetail } from './buildQueryHostDetail'; import { buildQueryHostDetail } from './buildQueryHostDetail';
import { buildQueryResultReducer } from '../../buildQueryResultModifier'; import { buildQueryResultReducer } from '../../buildQueryResultModifier';
import { toLocal } from '../../convertHostUUID'; import { toLocal } from '../../convertHostUUID';
import { getShortHostName } from '../../getShortHostName'; import { getShortHostName } from '../../disassembleHostName';
import { sanitize } from '../../sanitize'; import { sanitize } from '../../sanitize';
export const getHost = buildGetRequestHandler((request, buildQueryOptions) => { export const getHost = buildGetRequestHandler((request, buildQueryOptions) => {

@ -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);
};

@ -4,6 +4,7 @@ import {
getHostSSH, getHostSSH,
poweroffHost, poweroffHost,
rebootHost, rebootHost,
runManifest,
updateSystem, updateSystem,
} from '../lib/request_handlers/command'; } from '../lib/request_handlers/command';
@ -13,6 +14,7 @@ router
.put('/inquire-host', getHostSSH) .put('/inquire-host', getHostSSH)
.put('/poweroff-host', poweroffHost) .put('/poweroff-host', poweroffHost)
.put('/reboot-host', rebootHost) .put('/reboot-host', rebootHost)
.put('/run-manifest/:manifestUuid', runManifest)
.put('/update-system', updateSystem); .put('/update-system', updateSystem);
export default router; export default router;

@ -7,6 +7,7 @@ import fenceRouter from './fence';
import fileRouter from './file'; import fileRouter from './file';
import hostRouter from './host'; import hostRouter from './host';
import jobRouter from './job'; import jobRouter from './job';
import manifestRouter from './manifest';
import networkInterfaceRouter from './network-interface'; import networkInterfaceRouter from './network-interface';
import serverRouter from './server'; import serverRouter from './server';
import sshKeyRouter from './ssh-key'; import sshKeyRouter from './ssh-key';
@ -21,6 +22,7 @@ const routes: Readonly<Record<string, Router>> = {
file: fileRouter, file: fileRouter,
host: hostRouter, host: hostRouter,
job: jobRouter, job: jobRouter,
manifest: manifestRouter,
'network-interface': networkInterfaceRouter, 'network-interface': networkInterfaceRouter,
server: serverRouter, server: serverRouter,
'ssh-key': sshKeyRouter, 'ssh-key': sshKeyRouter,

@ -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;
};

@ -13,6 +13,96 @@ type AnvilDataDatabaseHash = {
}; };
}; };
type AnvilDataHostListHash = {
host_uuid: {
[hostUuid: string]: {
anvil_name?: string;
anvil_uuid?: string;
host_ipmi: string;
host_key: string;
host_name: string;
host_status: string;
host_type: string;
short_host_name: string;
};
};
};
type AnvilDataManifestListHash = {
manifest_uuid: {
[manifestUUID: string]: {
parsed: {
domain: string;
fences?: {
[fenceId: string]: {
uuid: string;
};
};
machine: {
[hostId: string]: {
fence?: {
[fenceName: string]: {
port: string;
};
};
ipmi_ip: string;
name: string;
network: {
[networkId: string]: {
ip: string;
};
};
ups?: {
[upsName: string]: {
used: string;
};
};
};
};
name: string;
networks: {
count: {
[networkType: string]: number;
};
dns: string;
mtu: string;
name: {
[networkId: string]: {
gateway: string;
network: string;
subnet: string;
};
};
ntp: string;
};
prefix: string;
sequence: string;
upses?: {
[upsId: string]: {
uuid: string;
};
};
};
};
};
name_to_uuid: Record<string, string>;
} & Record<
string,
{
manifest_last_ran: number;
manifest_name: string;
manifest_note: string;
manifest_xml: string;
}
>;
type AnvilDataSysHash = {
hosts?: {
by_uuid: { [hostUuid: string]: string };
by_name: { [hostName: string]: string };
};
};
type AnvilDataUPSHash = { type AnvilDataUPSHash = {
[upsName: string]: { [upsName: string]: {
agent: string; agent: string;

@ -42,7 +42,9 @@ const ConfirmDialog = forwardRef<
PaperProps: paperProps = {}, PaperProps: paperProps = {},
...restDialogProps ...restDialogProps
} = {}, } = {},
disableProceed: isDisableProceed,
formContent: isFormContent, formContent: isFormContent,
loading: isLoading = false,
loadingAction: isLoadingAction = false, loadingAction: isLoadingAction = false,
onActionAppend, onActionAppend,
onCancelAppend, onCancelAppend,
@ -59,8 +61,11 @@ const ConfirmDialog = forwardRef<
ref, ref,
) => { ) => {
const { sx: paperSx, ...restPaperProps } = paperProps; const { sx: paperSx, ...restPaperProps } = paperProps;
const { sx: proceedButtonSx, ...restProceedButtonProps } = const {
proceedButtonProps; disabled: proceedButtonDisabled = isDisableProceed,
sx: proceedButtonSx,
...restProceedButtonProps
} = proceedButtonProps;
const [isOpen, setIsOpen] = useState<boolean>(openInitially); const [isOpen, setIsOpen] = useState<boolean>(openInitially);
@ -141,6 +146,7 @@ const ConfirmDialog = forwardRef<
const proceedButtonElement = useMemo( const proceedButtonElement = useMemo(
() => ( () => (
<ContainedButton <ContainedButton
disabled={proceedButtonDisabled}
onClick={proceedButtonClickEventHandler} onClick={proceedButtonClickEventHandler}
type={proceedButtonType} type={proceedButtonType}
{...restProceedButtonProps} {...restProceedButtonProps}
@ -159,6 +165,7 @@ const ConfirmDialog = forwardRef<
[ [
actionProceedText, actionProceedText,
proceedButtonClickEventHandler, proceedButtonClickEventHandler,
proceedButtonDisabled,
proceedButtonSx, proceedButtonSx,
proceedButtonType, proceedButtonType,
proceedColour, proceedColour,
@ -196,16 +203,52 @@ const ConfirmDialog = forwardRef<
), ),
[titleText], [titleText],
); );
const combinedScrollBoxSx = useMemo<SxProps<Theme> | undefined>( const combinedScrollBoxSx = useMemo(() => {
let result: SxProps<Theme> | undefined;
if (isScrollContent) {
let overflowX: 'hidden' | undefined;
let paddingTop: string | undefined;
if (isFormContent) {
overflowX = 'hidden';
paddingTop = '.6em';
}
result = {
maxHeight: '60vh',
overflowX,
overflowY: 'scroll',
paddingRight: '.4em',
paddingTop,
...scrollBoxSx,
};
}
return result;
}, [isFormContent, isScrollContent, scrollBoxSx]);
const contentAreaElement = useMemo(
() => () =>
isScrollContent isLoading ? (
? { <Spinner />
maxHeight: '60vh', ) : (
overflowY: 'scroll', <>
...scrollBoxSx, <Box {...restScrollBoxProps} sx={combinedScrollBoxSx}>
} {contentElement}
: undefined, </Box>
[isScrollContent, scrollBoxSx], {preActionArea}
{actionAreaElement}
</>
),
[
actionAreaElement,
combinedScrollBoxSx,
contentElement,
isLoading,
preActionArea,
restScrollBoxProps,
],
); );
useImperativeHandle( useImperativeHandle(
@ -232,11 +275,7 @@ const ConfirmDialog = forwardRef<
onSubmit={contentContainerSubmitEventHandler} onSubmit={contentContainerSubmitEventHandler}
{...contentContainerProps} {...contentContainerProps}
> >
<Box {...restScrollBoxProps} sx={combinedScrollBoxSx}> {contentAreaElement}
{contentElement}
</Box>
{preActionArea}
{actionAreaElement}
</FlexBox> </FlexBox>
</MUIDialog> </MUIDialog>
); );

@ -1,6 +1,9 @@
import { import {
Add as MUIAddIcon,
Close as MUICloseIcon,
Done as MUIDoneIcon, Done as MUIDoneIcon,
Edit as MUIEditIcon, Edit as MUIEditIcon,
PlayCircle as MUIPlayCircleIcon,
Visibility as MUIVisibilityIcon, Visibility as MUIVisibilityIcon,
VisibilityOff as MUIVisibilityOffIcon, VisibilityOff as MUIVisibilityOffIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
@ -14,6 +17,7 @@ import { createElement, FC, ReactNode, useMemo } from 'react';
import { import {
BLACK, BLACK,
BLUE,
BORDER_RADIUS, BORDER_RADIUS,
DISABLED, DISABLED,
GREY, GREY,
@ -39,21 +43,36 @@ const NormalIconButton = styled(MUIIconButton)({
color: GREY, color: GREY,
}); });
const MAP_TO_VISIBILITY_ICON: IconButtonMapToStateIcon = { const MAP_TO_ADD_ICON: IconButtonMapToStateIconBundle = {
false: MUIVisibilityIcon, none: { iconType: MUIAddIcon },
true: MUIVisibilityOffIcon,
}; };
const MAP_TO_EDIT_ICON: IconButtonMapToStateIcon = { const MAP_TO_CLOSE_ICON: IconButtonMapToStateIconBundle = {
false: MUIEditIcon, none: { iconType: MUICloseIcon },
true: MUIDoneIcon, };
const MAP_TO_EDIT_ICON: IconButtonMapToStateIconBundle = {
false: { iconType: MUIEditIcon },
true: { iconType: MUIDoneIcon, iconProps: { sx: { color: BLUE } } },
};
const MAP_TO_PLAY_ICON: IconButtonMapToStateIconBundle = {
none: { iconType: MUIPlayCircleIcon },
};
const MAP_TO_VISIBILITY_ICON: IconButtonMapToStateIconBundle = {
false: { iconType: MUIVisibilityIcon },
true: { iconType: MUIVisibilityOffIcon },
}; };
const MAP_TO_MAP_PRESET: Record< const MAP_TO_MAP_PRESET: Record<
IconButtonPresetMapToStateIcon, IconButtonPresetMapToStateIconBundle,
IconButtonMapToStateIcon IconButtonMapToStateIconBundle
> = { > = {
add: MAP_TO_ADD_ICON,
close: MAP_TO_CLOSE_ICON,
edit: MAP_TO_EDIT_ICON, edit: MAP_TO_EDIT_ICON,
play: MAP_TO_PLAY_ICON,
visibility: MAP_TO_VISIBILITY_ICON, visibility: MAP_TO_VISIBILITY_ICON,
}; };
@ -68,11 +87,11 @@ const IconButton: FC<IconButtonProps> = ({
iconProps, iconProps,
mapPreset, mapPreset,
mapToIcon: externalMapToIcon, mapToIcon: externalMapToIcon,
state, state = 'none',
variant = 'contained', variant = 'contained',
...restIconButtonProps ...restIconButtonProps
}) => { }) => {
const mapToIcon = useMemo<IconButtonMapToStateIcon | undefined>( const mapToIcon = useMemo<IconButtonMapToStateIconBundle | undefined>(
() => externalMapToIcon ?? (mapPreset && MAP_TO_MAP_PRESET[mapPreset]), () => externalMapToIcon ?? (mapPreset && MAP_TO_MAP_PRESET[mapPreset]),
[externalMapToIcon, mapPreset], [externalMapToIcon, mapPreset],
); );
@ -81,19 +100,22 @@ const IconButton: FC<IconButtonProps> = ({
let result: ReactNode; let result: ReactNode;
if (mapToIcon) { if (mapToIcon) {
const iconElementType: CreatableComponent | undefined = state const { iconType, iconProps: presetIconProps } = mapToIcon[state] ?? {
? mapToIcon[state] ?? defaultIcon iconType: defaultIcon,
: defaultIcon; };
if (iconElementType) { if (iconType) {
result = createElement(iconElementType, iconProps); result = createElement(iconType, {
...presetIconProps,
...iconProps,
});
} }
} else { } else {
result = children; result = children;
} }
return result; return result;
}, [children, mapToIcon, state, defaultIcon, iconProps]); }, [mapToIcon, state, defaultIcon, iconProps, children]);
const iconButtonElementType = useMemo( const iconButtonElementType = useMemo(
() => MAP_TO_VARIANT[variant], () => MAP_TO_VARIANT[variant],
[variant], [variant],

@ -5,6 +5,7 @@ import {
forwardRef, forwardRef,
ReactElement, ReactElement,
useCallback, useCallback,
useEffect,
useImperativeHandle, useImperativeHandle,
useMemo, useMemo,
useState, useState,
@ -12,7 +13,6 @@ import {
import createInputOnChangeHandler from '../lib/createInputOnChangeHandler'; import createInputOnChangeHandler from '../lib/createInputOnChangeHandler';
import { createTestInputFunction } from '../lib/test_input'; import { createTestInputFunction } from '../lib/test_input';
import useIsFirstRender from '../hooks/useIsFirstRender';
type InputWithRefOptionalPropsWithDefault< type InputWithRefOptionalPropsWithDefault<
TypeName extends keyof MapToInputType, TypeName extends keyof MapToInputType,
@ -26,6 +26,7 @@ type InputWithRefOptionalPropsWithoutDefault<
> = { > = {
inputTestBatch?: InputTestBatch; inputTestBatch?: InputTestBatch;
onFirstRender?: InputFirstRenderFunction; onFirstRender?: InputFirstRenderFunction;
onUnmount?: () => void;
valueKey?: CreateInputOnChangeHandlerOptions<TypeName>['valueKey']; valueKey?: CreateInputOnChangeHandlerOptions<TypeName>['valueKey'];
}; };
@ -69,6 +70,7 @@ const InputWithRef = forwardRef(
input, input,
inputTestBatch, inputTestBatch,
onFirstRender, onFirstRender,
onUnmount,
required: isRequired = INPUT_WITH_REF_DEFAULT_PROPS.required, required: isRequired = INPUT_WITH_REF_DEFAULT_PROPS.required,
valueKey, valueKey,
valueType = INPUT_WITH_REF_DEFAULT_PROPS.valueType as TypeName, valueType = INPUT_WITH_REF_DEFAULT_PROPS.valueType as TypeName,
@ -96,8 +98,6 @@ const InputWithRef = forwardRef(
...restInitProps ...restInitProps
} = inputProps; } = inputProps;
const isFirstRender = useIsFirstRender();
const [inputValue, setInputValue] = const [inputValue, setInputValue] =
useState<MapToInputType[TypeName]>(initValue); useState<MapToInputType[TypeName]>(initValue);
const [isChangedByUser, setIsChangedByUser] = useState<boolean>(false); const [isChangedByUser, setIsChangedByUser] = useState<boolean>(false);
@ -166,7 +166,14 @@ const InputWithRef = forwardRef(
[initOnFocus, inputTestBatch], [initOnFocus, inputTestBatch],
); );
if (isFirstRender) { /**
* Using any setState function synchronously in the render function
* directly will trigger the 'cannot update a component while readering a
* different component' warning. This can be solved by wrapping the
* setState call(s) in a useEffect hook because it executes **after** the
* render function completes.
*/
useEffect(() => {
const isValid = const isValid =
testInput?.call(null, { testInput?.call(null, {
inputs: { [INPUT_TEST_ID]: { value: inputValue } }, inputs: { [INPUT_TEST_ID]: { value: inputValue } },
@ -174,7 +181,11 @@ const InputWithRef = forwardRef(
}) ?? false; }) ?? false;
onFirstRender?.call(null, { isValid }); onFirstRender?.call(null, { isValid });
}
return onUnmount;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useImperativeHandle( useImperativeHandle(
ref, ref,

@ -24,7 +24,6 @@ const MAP_TO_INPUT_BUILDER: MapToInputBuilder = {
input={ input={
<SwitchWithLabel <SwitchWithLabel
checked={isChecked} checked={isChecked}
flexBoxProps={{ width: '100%' }}
id={id} id={id}
label={label} label={label}
name={name} name={name}

@ -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,4 +1,4 @@
import { ReactElement, ReactNode, useMemo, useState } from 'react'; import { ReactElement, ReactNode, useEffect, useMemo, useState } from 'react';
import { BLACK } from '../../lib/consts/DEFAULT_THEME'; import { BLACK } from '../../lib/consts/DEFAULT_THEME';
@ -142,11 +142,13 @@ const AddUpsInputGroup = <
], ],
); );
if (isFirstRender) { useEffect(() => {
buildInputFirstRenderFunction(INPUT_ID_UPS_TYPE)({ if (isFirstRender) {
isValid: Boolean(inputUpsTypeIdValue), buildInputFirstRenderFunction(INPUT_ID_UPS_TYPE)({
}); isValid: Boolean(inputUpsTypeIdValue),
} });
}
}, [buildInputFirstRenderFunction, inputUpsTypeIdValue, isFirstRender]);
return content; return content;
}; };

@ -22,7 +22,7 @@ const CommonUpsInputGroup = <
formUtils: { formUtils: {
buildFinishInputTestBatchFunction, buildFinishInputTestBatchFunction,
buildInputFirstRenderFunction, buildInputFirstRenderFunction,
msgSetters, setMessage,
}, },
previous: { upsIPAddress: previousIpAddress, upsName: previousUpsName } = {}, previous: { upsIPAddress: previousIpAddress, upsName: previousUpsName } = {},
}: CommonUpsInputGroupProps<M>): ReactElement => ( }: CommonUpsInputGroupProps<M>): ReactElement => (
@ -42,16 +42,14 @@ const CommonUpsInputGroup = <
inputTestBatch={buildPeacefulStringTestBatch( inputTestBatch={buildPeacefulStringTestBatch(
INPUT_LABEL_UPS_NAME, INPUT_LABEL_UPS_NAME,
() => { () => {
msgSetters[INPUT_ID_UPS_NAME](); setMessage(INPUT_ID_UPS_NAME);
}, },
{ {
onFinishBatch: onFinishBatch:
buildFinishInputTestBatchFunction(INPUT_ID_UPS_NAME), buildFinishInputTestBatchFunction(INPUT_ID_UPS_NAME),
}, },
(message) => { (message) => {
msgSetters[INPUT_ID_UPS_NAME]({ setMessage(INPUT_ID_UPS_NAME, { children: message });
children: message,
});
}, },
)} )}
onFirstRender={buildInputFirstRenderFunction(INPUT_ID_UPS_NAME)} onFirstRender={buildInputFirstRenderFunction(INPUT_ID_UPS_NAME)}
@ -72,16 +70,14 @@ const CommonUpsInputGroup = <
inputTestBatch={buildIPAddressTestBatch( inputTestBatch={buildIPAddressTestBatch(
INPUT_LABEL_UPS_IP, INPUT_LABEL_UPS_IP,
() => { () => {
msgSetters[INPUT_ID_UPS_IP](); setMessage(INPUT_ID_UPS_IP);
}, },
{ {
onFinishBatch: onFinishBatch:
buildFinishInputTestBatchFunction(INPUT_ID_UPS_IP), buildFinishInputTestBatchFunction(INPUT_ID_UPS_IP),
}, },
(message) => { (message) => {
msgSetters[INPUT_ID_UPS_IP]({ setMessage(INPUT_ID_UPS_IP, { children: message });
children: message,
});
}, },
)} )}
onFirstRender={buildInputFirstRenderFunction(INPUT_ID_UPS_IP)} onFirstRender={buildInputFirstRenderFunction(INPUT_ID_UPS_IP)}

@ -62,20 +62,22 @@ const MessageGroup = forwardRef<
(key: string, message?: Message) => { (key: string, message?: Message) => {
let length = 0; let length = 0;
const { [key]: unused, ...rest } = messages; setMessages((previous) => {
const result: Messages = rest; const { [key]: unused, ...rest } = previous;
const result: Messages = rest;
if (message) { if (message) {
result[key] = message; result[key] = message;
} }
length = Object.keys(result).length; length = Object.keys(result).length;
onSet?.call(null, length); return result;
});
setMessages(result); onSet?.call(null, length);
}, },
[messages, onSet], [onSet],
); );
const setMessageRe = useCallback( const setMessageRe = useCallback(
(re: RegExp, message?: Message) => { (re: RegExp, message?: Message) => {
@ -87,22 +89,25 @@ const MessageGroup = forwardRef<
length += 1; length += 1;
} }
: undefined; : undefined;
const result: Messages = {};
setMessages((previous) => {
Object.keys(messages).forEach((key: string) => { const result: Messages = {};
if (re.test(key)) {
assignMessage?.call(null, result, key); Object.keys(previous).forEach((key: string) => {
} else { if (re.test(key)) {
result[key] = messages[key]; assignMessage?.call(null, result, key);
length += 1; } else {
} result[key] = previous[key];
length += 1;
}
});
return result;
}); });
onSet?.call(null, length); onSet?.call(null, length);
setMessages(result);
}, },
[messages, onSet], [onSet],
); );
const messageElements = useMemo(() => { const messageElements = useMemo(() => {

@ -34,6 +34,7 @@ import { v4 as uuidv4 } from 'uuid';
import API_BASE_URL from '../lib/consts/API_BASE_URL'; import API_BASE_URL from '../lib/consts/API_BASE_URL';
import { BLUE, GREY } from '../lib/consts/DEFAULT_THEME'; import { BLUE, GREY } from '../lib/consts/DEFAULT_THEME';
import NETWORK_TYPES from '../lib/consts/NETWORK_TYPES';
import { REP_IPV4, REP_IPV4_CSV } from '../lib/consts/REG_EXP_PATTERNS'; import { REP_IPV4, REP_IPV4_CSV } from '../lib/consts/REG_EXP_PATTERNS';
import BriefNetworkInterface from './BriefNetworkInterface'; import BriefNetworkInterface from './BriefNetworkInterface';
@ -106,17 +107,6 @@ const CLASSES = {
}; };
const INITIAL_IFACES = [undefined, undefined]; const INITIAL_IFACES = [undefined, undefined];
const NETWORK_TYPES: Record<string, string> = {
bcn: 'Back-Channel Network',
ifn: 'Internet-Facing Network',
sn: 'Storage Network',
};
const NODE_NETWORK_TYPES: Record<string, string> = {
...NETWORK_TYPES,
mn: 'Migration Network',
};
const STRIKER_REQUIRED_NETWORKS: NetworkInput[] = [ const STRIKER_REQUIRED_NETWORKS: NetworkInput[] = [
{ {
inputUUID: '30dd2ac5-8024-4a7e-83a1-6a3df7218972', inputUUID: '30dd2ac5-8024-4a7e-83a1-6a3df7218972',
@ -349,11 +339,13 @@ const NetworkForm: FC<{
!isNode && networkInterfaceCount <= 2 ? [1] : NETWORK_INTERFACE_TEMPLATE, !isNode && networkInterfaceCount <= 2 ? [1] : NETWORK_INTERFACE_TEMPLATE,
[isNode, networkInterfaceCount], [isNode, networkInterfaceCount],
); );
const netTypeList = useMemo( const netTypeList = useMemo(() => {
() => const { bcn, ifn, mn, sn } = NETWORK_TYPES;
isNode && networkInterfaceCount >= 8 ? NODE_NETWORK_TYPES : NETWORK_TYPES,
[isNode, networkInterfaceCount], return isNode && networkInterfaceCount >= 8
); ? { bcn, ifn, mn, sn }
: { bcn, ifn, sn };
}, [isNode, networkInterfaceCount]);
useEffect(() => { useEffect(() => {
const { ipAddressInputRef: ipRef, subnetMaskInputRef: maskRef } = const { ipAddressInputRef: ipRef, subnetMaskInputRef: maskRef } =

@ -6,6 +6,7 @@ import {
IconButtonProps as MUIIconButtonProps, IconButtonProps as MUIIconButtonProps,
iconButtonClasses as muiIconButtonClasses, iconButtonClasses as muiIconButtonClasses,
InputAdornment as MUIInputAdornment, InputAdornment as MUIInputAdornment,
InputBaseComponentProps as MUIInputBaseComponentProps,
} from '@mui/material'; } from '@mui/material';
import { FC, useCallback, useMemo, useState } from 'react'; import { FC, useCallback, useMemo, useState } from 'react';
@ -31,6 +32,7 @@ type OutlinedInputWithLabelOptionalPropsWithDefault = {
}; };
type OutlinedInputWithLabelOptionalPropsWithoutDefault = { type OutlinedInputWithLabelOptionalPropsWithoutDefault = {
baseInputProps?: MUIInputBaseComponentProps;
onHelp?: MUIIconButtonProps['onClick']; onHelp?: MUIIconButtonProps['onClick'];
onHelpAppend?: MUIIconButtonProps['onClick']; onHelpAppend?: MUIIconButtonProps['onClick'];
type?: string; type?: string;
@ -50,6 +52,7 @@ type OutlinedInputWithLabelProps = Pick<
const OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS: Required<OutlinedInputWithLabelOptionalPropsWithDefault> & const OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS: Required<OutlinedInputWithLabelOptionalPropsWithDefault> &
OutlinedInputWithLabelOptionalPropsWithoutDefault = { OutlinedInputWithLabelOptionalPropsWithoutDefault = {
baseInputProps: undefined,
fillRow: false, fillRow: false,
formControlProps: {}, formControlProps: {},
helpMessageBoxProps: {}, helpMessageBoxProps: {},
@ -65,6 +68,7 @@ const OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS: Required<OutlinedInputWithLabelOp
}; };
const OutlinedInputWithLabel: FC<OutlinedInputWithLabelProps> = ({ const OutlinedInputWithLabel: FC<OutlinedInputWithLabelProps> = ({
baseInputProps,
fillRow: isFillRow = OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS.fillRow, fillRow: isFillRow = OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS.fillRow,
formControlProps = OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS.formControlProps, formControlProps = OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS.formControlProps,
helpMessageBoxProps = OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS.helpMessageBoxProps, helpMessageBoxProps = OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS.helpMessageBoxProps,
@ -171,6 +175,7 @@ const OutlinedInputWithLabel: FC<OutlinedInputWithLabelProps> = ({
} }
fullWidth={formControlProps.fullWidth} fullWidth={formControlProps.fullWidth}
id={id} id={id}
inputProps={baseInputProps}
label={label} label={label}
name={name} name={name}
onBlur={onBlur} onBlur={onBlur}

@ -3,15 +3,33 @@ import { Box as MUIBox, SxProps, Theme } from '@mui/material';
import { BORDER_RADIUS, DIVIDER } from '../../lib/consts/DEFAULT_THEME'; import { BORDER_RADIUS, DIVIDER } from '../../lib/consts/DEFAULT_THEME';
const InnerPanel: FC<InnerPanelProps> = ({ sx, ...muiBoxRestProps }) => { const InnerPanel: FC<InnerPanelProps> = ({
headerMarginOffset: hmo = '.3em',
ml,
mv = '1.4em',
sx,
// Props that depend on others.
mb = mv,
mt = mv,
...muiBoxRestProps
}) => {
const marginLeft = useMemo(
() => (ml ? `calc(${ml} + ${hmo})` : hmo),
[hmo, ml],
);
const marginTop = useMemo(() => {
const resultMt = typeof mt === 'number' ? `${mt}px` : mt;
return `calc(${resultMt} + ${hmo})`;
}, [hmo, mt]);
const combinedSx = useMemo<SxProps<Theme>>( const combinedSx = useMemo<SxProps<Theme>>(
() => ({ () => ({
borderWidth: '1px', borderWidth: '1px',
borderRadius: BORDER_RADIUS, borderRadius: BORDER_RADIUS,
borderStyle: 'solid', borderStyle: 'solid',
borderColor: DIVIDER, borderColor: DIVIDER,
marginTop: '1.4em',
marginBottom: '1.4em',
paddingBottom: 0, paddingBottom: 0,
position: 'relative', position: 'relative',
@ -20,7 +38,15 @@ const InnerPanel: FC<InnerPanelProps> = ({ sx, ...muiBoxRestProps }) => {
[sx], [sx],
); );
return <MUIBox {...muiBoxRestProps} sx={combinedSx} />; return (
<MUIBox
mb={mb}
ml={marginLeft}
mt={marginTop}
{...muiBoxRestProps}
sx={combinedSx}
/>
);
}; };
export default InnerPanel; export default InnerPanel;

@ -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,55 +1,67 @@
import { Switch, SxProps, Theme } from '@mui/material'; import {
import { FC, useMemo } from 'react'; FormControlLabel as MUIFormControlLabel,
styled,
Switch as MUISwitch,
} from '@mui/material';
import { FC, ReactElement, useMemo } from 'react';
import { GREY } from '../lib/consts/DEFAULT_THEME'; import { GREY } from '../lib/consts/DEFAULT_THEME';
import FlexBox from './FlexBox';
import { BodyText } from './Text'; import { BodyText } from './Text';
const SwitchFormControlLabel = styled(MUIFormControlLabel)({
height: '3.5em',
marginLeft: 0,
width: '100%',
});
const SwitchWithLabel: FC<SwitchWithLabelProps> = ({ const SwitchWithLabel: FC<SwitchWithLabelProps> = ({
baseInputProps,
checked: isChecked, checked: isChecked,
flexBoxProps: { sx: flexBoxSx, ...restFlexBoxProps } = {}, formControlLabelProps,
id: switchId, id: switchId,
label, label,
name: switchName, name: switchName,
onChange, onChange,
switchProps, switchProps,
}) => { }) => {
const combinedFlexBoxSx = useMemo<SxProps<Theme>>( const labelElement = useMemo<ReactElement>(
() => ({
'& > :first-child': {
flexGrow: 1,
},
...flexBoxSx,
}),
[flexBoxSx],
);
const labelElement = useMemo(
() => () =>
typeof label === 'string' ? ( typeof label === 'string' ? (
<BodyText inheritColour color={`${GREY}9F`}> <BodyText inheritColour color={`${GREY}AF`}>
{label} {label}
</BodyText> </BodyText>
) : ( ) : (
label <>{label}</>
), ),
[label], [label],
); );
return ( return (
<FlexBox row {...restFlexBoxProps} sx={combinedFlexBoxSx}> <>
{labelElement} <SwitchFormControlLabel
<Switch componentsProps={{ typography: { flexGrow: 1 } }}
control={
<MUISwitch
checked={isChecked}
edge="end"
name={switchName}
onChange={onChange}
{...switchProps}
/>
}
label={labelElement}
labelPlacement="start"
{...formControlLabelProps}
/>
<input
checked={isChecked} checked={isChecked}
edge="end" hidden
id={switchId} id={switchId}
name={switchName} readOnly
onChange={onChange} {...baseInputProps}
{...switchProps}
/> />
</FlexBox> </>
); );
}; };

@ -1,7 +1,9 @@
import { MutableRefObject, useCallback, useMemo, useState } from 'react'; import { MutableRefObject, useCallback, useMemo, useState } from 'react';
import buildMapToMessageSetter from '../lib/buildMapToMessageSetter'; import buildObjectStateSetterCallback, {
import buildObjectStateSetterCallback from '../lib/buildObjectStateSetterCallback'; buildRegExpObjectStateSetterCallback,
} from '../lib/buildObjectStateSetterCallback';
import { Message } from '../components/MessageBox';
import { MessageGroupForwardedRefContent } from '../components/MessageGroup'; import { MessageGroupForwardedRefContent } from '../components/MessageGroup';
const useFormUtils = < const useFormUtils = <
@ -14,12 +16,48 @@ const useFormUtils = <
): FormUtils<M> => { ): FormUtils<M> => {
const [formValidity, setFormValidity] = useState<FormValidity<M>>({}); const [formValidity, setFormValidity] = useState<FormValidity<M>>({});
const setValidity = useCallback((key: keyof M, value: boolean) => { const setMessage = useCallback(
(key: keyof M, message?: Message) => {
messageGroupRef?.current?.setMessage?.call(null, String(key), message);
},
[messageGroupRef],
);
const setMessageRe = useCallback(
(re: RegExp, message?: Message) => {
messageGroupRef?.current?.setMessageRe?.call(null, re, message);
},
[messageGroupRef],
);
const setValidity = useCallback((key: keyof M, value?: boolean) => {
setFormValidity( setFormValidity(
buildObjectStateSetterCallback<FormValidity<M>>(key, value), buildObjectStateSetterCallback<FormValidity<M>>(key, value),
); );
}, []); }, []);
const setValidityRe = useCallback((re: RegExp, value?: boolean) => {
setFormValidity(
buildRegExpObjectStateSetterCallback<FormValidity<M>>(re, value),
);
}, []);
const unsetKey = useCallback(
(key: keyof M) => {
setMessage(key);
setValidity(key);
},
[setMessage, setValidity],
);
const unsetKeyRe = useCallback(
(re: RegExp) => {
setMessageRe(re);
setValidityRe(re);
},
[setMessageRe, setValidityRe],
);
const buildFinishInputTestBatchFunction = useCallback( const buildFinishInputTestBatchFunction = useCallback(
(key: keyof M) => (result: boolean) => { (key: keyof M) => (result: boolean) => {
setValidity(key, result); setValidity(key, result);
@ -35,24 +73,31 @@ const useFormUtils = <
[setValidity], [setValidity],
); );
const buildInputUnmountFunction = useCallback(
(key: keyof M) => () => {
unsetKey(key);
},
[unsetKey],
);
const isFormInvalid = useMemo( const isFormInvalid = useMemo(
() => Object.values(formValidity).some((isInputValid) => !isInputValid), () => Object.values(formValidity).some((isInputValid) => !isInputValid),
[formValidity], [formValidity],
); );
const msgSetters = useMemo(
() => buildMapToMessageSetter<U, I, M>(ids, messageGroupRef),
[ids, messageGroupRef],
);
return { return {
buildFinishInputTestBatchFunction, buildFinishInputTestBatchFunction,
buildInputFirstRenderFunction, buildInputFirstRenderFunction,
buildInputUnmountFunction,
formValidity, formValidity,
isFormInvalid, isFormInvalid,
msgSetters,
setFormValidity, setFormValidity,
setMessage,
setMessageRe,
setValidity, setValidity,
setValidityRe,
unsetKey,
unsetKeyRe,
}; };
}; };

@ -7,8 +7,8 @@ const buildMessageSetter = <T extends MapToInputTestID>(
messageGroupRef: MutableRefObject<MessageGroupForwardedRefContent>, messageGroupRef: MutableRefObject<MessageGroupForwardedRefContent>,
container?: MapToMessageSetter<T>, container?: MapToMessageSetter<T>,
key: string = id, key: string = id,
): MessageSetterFunction => { ): MessageSetter => {
const setter: MessageSetterFunction = (message?) => { const setter: MessageSetter = (message?) => {
messageGroupRef.current.setMessage?.call(null, id, message); messageGroupRef.current.setMessage?.call(null, id, message);
}; };
@ -47,4 +47,6 @@ const buildMapToMessageSetter = <
return result; return result;
}; };
export { buildMessageSetter };
export default buildMapToMessageSetter; export default buildMapToMessageSetter;

@ -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;

@ -13,6 +13,9 @@ const buildDomainTestBatch: BuildInputTestBatchFunction = (
isRequired, isRequired,
onFinishBatch, onFinishBatch,
tests: [ tests: [
{
test: testNotBlank,
},
{ {
onFailure: (...args) => { onFailure: (...args) => {
onDomainTestFailure( onDomainTestFailure(
@ -27,7 +30,6 @@ const buildDomainTestBatch: BuildInputTestBatchFunction = (
test: ({ compare, value }) => test: ({ compare, value }) =>
(compare[0] as boolean) || REP_DOMAIN.test(value as string), (compare[0] as boolean) || REP_DOMAIN.test(value as string),
}, },
{ test: testNotBlank },
], ],
}); });

@ -12,16 +12,18 @@ const buildIPAddressTestBatch: BuildInputTestBatchFunction = (
isRequired, isRequired,
onFinishBatch, onFinishBatch,
tests: [ tests: [
{
test: testNotBlank,
},
{ {
onFailure: (...args) => { onFailure: (...args) => {
onIPv4TestFailure( onIPv4TestFailure(
`${inputName} should be a valid IPv4 address.`, <>{inputName} should be a valid IPv4 address.</>,
...args, ...args,
); );
}, },
test: ({ value }) => REP_IPV4.test(value as string), test: ({ value }) => REP_IPV4.test(value as string),
}, },
{ test: testNotBlank },
], ],
}); });

@ -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;

@ -13,6 +13,13 @@ const buildPeacefulStringTestBatch: BuildInputTestBatchFunction = (
isRequired, isRequired,
onFinishBatch, onFinishBatch,
tests: [ tests: [
{
/**
* Not-blank test ensures no unnecessary error message is provided when
* input is not (yet) filled.
*/
test: testNotBlank,
},
{ {
onFailure: (...args) => { onFailure: (...args) => {
onTestPeacefulStringFailureAppend( onTestPeacefulStringFailureAppend(
@ -31,7 +38,6 @@ const buildPeacefulStringTestBatch: BuildInputTestBatchFunction = (
}, },
test: ({ value }) => REP_PEACEFUL_STRING.test(value as string), test: ({ value }) => REP_PEACEFUL_STRING.test(value as string),
}, },
{ test: testNotBlank },
], ],
}); });

@ -12,13 +12,15 @@ const buildUUIDTestBatch: BuildInputTestBatchFunction = (
isRequired, isRequired,
onFinishBatch, onFinishBatch,
tests: [ tests: [
{
test: testNotBlank,
},
{ {
onFailure: (...args) => { onFailure: (...args) => {
onUUIDTestFailure(`${inputName} must be a valid UUID.`, ...args); onUUIDTestFailure(<>{inputName} must be a valid UUID.</>, ...args);
}, },
test: ({ value }) => REP_UUID.test(value as string), test: ({ value }) => REP_UUID.test(value as string),
}, },
{ test: testNotBlank },
], ],
}); });

@ -1,5 +1,7 @@
import buildDomainTestBatch from './buildDomainTestBatch'; import buildDomainTestBatch from './buildDomainTestBatch';
import buildIPAddressTestBatch from './buildIPAddressTestBatch'; import buildIPAddressTestBatch from './buildIPAddressTestBatch';
import buildIpCsvTestBatch from './buildIpCsvTestBatch';
import buildNumberTestBatch from './buildNumberTestBatch';
import buildPeacefulStringTestBatch from './buildPeacefulStringTestBatch'; import buildPeacefulStringTestBatch from './buildPeacefulStringTestBatch';
import buildUUIDTestBatch from './buildUUIDTestBatch'; import buildUUIDTestBatch from './buildUUIDTestBatch';
import createTestInputFunction from './createTestInputFunction'; import createTestInputFunction from './createTestInputFunction';
@ -12,6 +14,8 @@ import testRange from './testRange';
export { export {
buildDomainTestBatch, buildDomainTestBatch,
buildIPAddressTestBatch, buildIPAddressTestBatch,
buildIpCsvTestBatch,
buildNumberTestBatch,
buildPeacefulStringTestBatch, buildPeacefulStringTestBatch,
buildUUIDTestBatch, buildUUIDTestBatch,
createTestInputFunction, createTestInputFunction,

@ -101,15 +101,17 @@ const testInput: TestInputFunction = ({
displayMin = orSet(dDisplayMin, String(min)), displayMin = orSet(dDisplayMin, String(min)),
} = testsToRun[id]; } = testsToRun[id];
if (!value && isOptional) {
return true;
}
const { cbFinishBatch, setTestCallbacks } = evalIsIgnoreOnCallbacks({ const { cbFinishBatch, setTestCallbacks } = evalIsIgnoreOnCallbacks({
isIgnoreOnCallbacks, isIgnoreOnCallbacks,
onFinishBatch, onFinishBatch,
}); });
if (!value && isOptional) {
cbFinishBatch?.call(null, true, id);
return true;
}
const runTest: (test: InputTest) => boolean = ({ const runTest: (test: InputTest) => boolean = ({
onFailure, onFailure,
onSuccess = dOnSuccess, onSuccess = dOnSuccess,

@ -8,6 +8,7 @@ import Grid from '../../components/Grid';
import handleAPIError from '../../lib/handleAPIError'; import handleAPIError from '../../lib/handleAPIError';
import Header from '../../components/Header'; import Header from '../../components/Header';
import ManageFencePanel from '../../components/ManageFence'; import ManageFencePanel from '../../components/ManageFence';
import ManageManifestPanel from '../../components/ManageManifest';
import ManageUpsPanel from '../../components/ManageUps'; import ManageUpsPanel from '../../components/ManageUps';
import { Panel } from '../../components/Panels'; import { Panel } from '../../components/Panels';
import PrepareHostForm from '../../components/PrepareHostForm'; import PrepareHostForm from '../../components/PrepareHostForm';
@ -20,12 +21,18 @@ import useIsFirstRender from '../../hooks/useIsFirstRender';
import useProtect from '../../hooks/useProtect'; import useProtect from '../../hooks/useProtect';
import useProtectedState from '../../hooks/useProtectedState'; import useProtectedState from '../../hooks/useProtectedState';
const TAB_ID_PREPARE_HOST = 'prepare-host';
const TAB_ID_PREPARE_NETWORK = 'prepare-network';
const TAB_ID_MANAGE_FENCE = 'manage-fence';
const TAB_ID_MANAGE_UPS = 'manage-ups';
const TAB_ID_MANAGE_MANIFEST = 'manage-manifest';
const MAP_TO_PAGE_TITLE: Record<string, string> = { const MAP_TO_PAGE_TITLE: Record<string, string> = {
'prepare-host': 'Prepare Host', [TAB_ID_PREPARE_HOST]: 'Prepare Host',
'prepare-network': 'Prepare Network', [TAB_ID_PREPARE_NETWORK]: 'Prepare Network',
'manage-fence': 'Manage Fence Devices', [TAB_ID_MANAGE_FENCE]: 'Manage Fence Devices',
'manage-ups': 'Manage UPSes', [TAB_ID_MANAGE_UPS]: 'Manage UPSes',
'manage-manifest': 'Manage Manifests', [TAB_ID_MANAGE_MANIFEST]: 'Manage Manifests',
}; };
const PAGE_TITLE_LOADING = 'Loading'; const PAGE_TITLE_LOADING = 'Loading';
const STEP_CONTENT_GRID_COLUMNS = { md: 8, sm: 6, xs: 1 }; const STEP_CONTENT_GRID_COLUMNS = { md: 8, sm: 6, xs: 1 };
@ -70,7 +77,7 @@ const PrepareNetworkTabContent: FC = () => {
> >
{hostOverviewPairs.map(([hostUUID, { shortHostName }]) => ( {hostOverviewPairs.map(([hostUUID, { shortHostName }]) => (
<Tab <Tab
key={`prepare-network-${hostUUID}`} key={`${TAB_ID_PREPARE_NETWORK}-${hostUUID}`}
label={shortHostName} label={shortHostName}
value={hostUUID} value={hostUUID}
/> />
@ -144,6 +151,19 @@ const ManageUpsTabContent: FC = () => (
/> />
); );
const ManageManifestContent: FC = () => (
<Grid
columns={STEP_CONTENT_GRID_COLUMNS}
layout={{
'managemanifest-left-column': {},
'managemanifest-center-column': {
children: <ManageManifestPanel />,
...STEP_CONTENT_GRID_CENTER_COLUMN,
},
}}
/>
);
const ManageElement: FC = () => { const ManageElement: FC = () => {
const { const {
isReady, isReady,
@ -156,11 +176,11 @@ const ManageElement: FC = () => {
useEffect(() => { useEffect(() => {
if (isReady) { if (isReady) {
let step = getQueryParam(rawStep, { let step = getQueryParam(rawStep, {
fallbackValue: 'prepare-host', fallbackValue: TAB_ID_PREPARE_HOST,
}); });
if (!MAP_TO_PAGE_TITLE[step]) { if (!MAP_TO_PAGE_TITLE[step]) {
step = 'prepare-host'; step = TAB_ID_PREPARE_HOST;
} }
if (pageTitle === PAGE_TITLE_LOADING) { if (pageTitle === PAGE_TITLE_LOADING) {
@ -188,24 +208,28 @@ const ManageElement: FC = () => {
orientation={{ xs: 'vertical', sm: 'horizontal' }} orientation={{ xs: 'vertical', sm: 'horizontal' }}
value={pageTabId} value={pageTabId}
> >
<Tab label="Prepare host" value="prepare-host" /> <Tab label="Prepare host" value={TAB_ID_PREPARE_HOST} />
<Tab label="Prepare network" value="prepare-network" /> <Tab label="Prepare network" value={TAB_ID_PREPARE_NETWORK} />
<Tab label="Manage fence devices" value="manage-fence" /> <Tab label="Manage fence devices" value={TAB_ID_MANAGE_FENCE} />
<Tab label="Manage UPSes" value="manage-ups" /> <Tab label="Manage UPSes" value={TAB_ID_MANAGE_UPS} />
<Tab label="Manage manifests" value={TAB_ID_MANAGE_MANIFEST} />
</Tabs> </Tabs>
</Panel> </Panel>
<TabContent changingTabId={pageTabId} tabId="prepare-host"> <TabContent changingTabId={pageTabId} tabId={TAB_ID_PREPARE_HOST}>
<PrepareHostTabContent /> <PrepareHostTabContent />
</TabContent> </TabContent>
<TabContent changingTabId={pageTabId} tabId="prepare-network"> <TabContent changingTabId={pageTabId} tabId={TAB_ID_PREPARE_NETWORK}>
<PrepareNetworkTabContent /> <PrepareNetworkTabContent />
</TabContent> </TabContent>
<TabContent changingTabId={pageTabId} tabId="manage-fence"> <TabContent changingTabId={pageTabId} tabId={TAB_ID_MANAGE_FENCE}>
<ManageFenceTabContent /> <ManageFenceTabContent />
</TabContent> </TabContent>
<TabContent changingTabId={pageTabId} tabId="manage-ups"> <TabContent changingTabId={pageTabId} tabId={TAB_ID_MANAGE_UPS}>
<ManageUpsTabContent /> <ManageUpsTabContent />
</TabContent> </TabContent>
<TabContent changingTabId={pageTabId} tabId={TAB_ID_MANAGE_MANIFEST}>
<ManageManifestContent />
</TabContent>
</> </>
); );
}; };

@ -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,14 +1,19 @@
type DivFormEventHandler = import('react').FormEventHandler<HTMLDivElement>;
type DivFormEventHandlerParameters = Parameters<DivFormEventHandler>;
type ConfirmDialogOptionalProps = { type ConfirmDialogOptionalProps = {
actionCancelText?: string; actionCancelText?: string;
closeOnProceed?: boolean; closeOnProceed?: boolean;
contentContainerProps?: import('../components/FlexBox').FlexBoxProps; contentContainerProps?: import('../components/FlexBox').FlexBoxProps;
dialogProps?: Partial<import('@mui/material').DialogProps>; dialogProps?: Partial<import('@mui/material').DialogProps>;
disableProceed?: boolean;
formContent?: boolean; formContent?: boolean;
loading?: boolean;
loadingAction?: boolean; loadingAction?: boolean;
onActionAppend?: ContainedButtonProps['onClick']; onActionAppend?: ContainedButtonProps['onClick'];
onProceedAppend?: ContainedButtonProps['onClick']; onProceedAppend?: ContainedButtonProps['onClick'];
onCancelAppend?: ContainedButtonProps['onClick']; onCancelAppend?: ContainedButtonProps['onClick'];
onSubmitAppend?: import('react').FormEventHandler<HTMLDivElement>; onSubmitAppend?: DivFormEventHandler;
openInitially?: boolean; openInitially?: boolean;
preActionArea?: import('react').ReactNode; preActionArea?: import('react').ReactNode;
proceedButtonProps?: ContainedButtonProps; proceedButtonProps?: ContainedButtonProps;

@ -14,14 +14,31 @@ type InputFirstRenderFunctionBuilder<M extends MapToInputTestID> = (
key: keyof M, key: keyof M,
) => InputFirstRenderFunction; ) => InputFirstRenderFunction;
type InputUnmountFunction = () => void;
type InputUnmountFunctionBuilder<M extends MapToInputTestID> = (
key: keyof M,
) => InputUnmountFunction;
type FormUtils<M extends MapToInputTestID> = { type FormUtils<M extends MapToInputTestID> = {
buildFinishInputTestBatchFunction: InputTestBatchFinishCallbackBuilder<M>; buildFinishInputTestBatchFunction: InputTestBatchFinishCallbackBuilder<M>;
buildInputFirstRenderFunction: InputFirstRenderFunctionBuilder<M>; buildInputFirstRenderFunction: InputFirstRenderFunctionBuilder<M>;
buildInputUnmountFunction: InputUnmountFunctionBuilder<M>;
formValidity: FormValidity<M>; formValidity: FormValidity<M>;
isFormInvalid: boolean; isFormInvalid: boolean;
msgSetters: MapToMessageSetter<M>;
setFormValidity: import('react').Dispatch< setFormValidity: import('react').Dispatch<
import('react').SetStateAction<FormValidity<M>> import('react').SetStateAction<FormValidity<M>>
>; >;
setValidity: (key: keyof M, value: boolean) => void; setMessage: (
key: keyof M,
message?: import('../components/MessageBox').Message,
) => void;
setMessageRe: (
re: RegExp,
message?: import('../components/MessageBox').Message,
) => void;
setValidity: (key: keyof M, value?: boolean) => void;
setValidityRe: (re: RegExp, value?: boolean) => void;
unsetKey: (key: keyof M) => void;
unsetKeyRe: (re: RegExp) => void;
}; };

@ -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,5 +1,5 @@
type MapToMessageSetter<T extends MapToInputTestID> = { type MapToMessageSetter<T extends MapToInputTestID> = {
[MessageSetterID in keyof T]: MessageSetterFunction; [MessageSetterID in keyof T]: MessageSetter;
}; };
type InputIds<T> = ReadonlyArray<T> | MapToInputTestID; type InputIds<T> = ReadonlyArray<T> | MapToInputTestID;

@ -1,3 +1,3 @@
type MessageSetterFunction = ( type MessageSetter = (
message?: import('../components/MessageBox').Message, message?: import('../components/MessageBox').Message,
) => void; ) => void;

@ -1,3 +1,8 @@
type SelectChangeEventHandler = Exclude<
import('@mui/material').SelectProps['onChange'],
undefined
>;
type SelectOptionalProps = { type SelectOptionalProps = {
onClearIndicatorClick?: import('@mui/material').IconButtonProps['onClick']; onClearIndicatorClick?: import('@mui/material').IconButtonProps['onClick'];
}; };

@ -1,5 +1,6 @@
type SwitchWithLabelOptionalProps = { type SwitchWithLabelOptionalProps = {
flexBoxProps?: import('../components/FlexBox').FlexBoxProps; baseInputProps?: import('@mui/material').InputBaseComponentProps;
formControlLabelProps?: import('@mui/material').FormControlLabelProps;
switchProps?: import('@mui/material').SwitchProps; switchProps?: import('@mui/material').SwitchProps;
}; };

Loading…
Cancel
Save