Merge pull request #677 from ylei-tsubame/dependabot/58-59braces

Web UI: patches 650, 663, 642, 661, 670, 400, and dependabot/58, 59
main
Digimer 6 months ago committed by GitHub
commit ab79237240
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      striker-ui-api/out/index.js
  2. 28
      striker-ui-api/package-lock.json
  3. 1
      striker-ui-api/src/lib/consts/WS_GUID.ts
  4. 2
      striker-ui-api/src/lib/consts/index.ts
  5. 70
      striker-ui-api/src/lib/request_handlers/command/getHostSSH.ts
  6. 79
      striker-ui-api/src/lib/request_handlers/host/getHostConnection.ts
  7. 2
      striker-ui-api/src/lib/request_handlers/manifest/buildManifest.ts
  8. 2
      striker-ui-api/src/lib/request_handlers/ssh-key/getSSHKeyConflict.ts
  9. 66
      striker-ui-api/src/middlewares/proxyServerVnc.ts
  10. 2
      striker-ui-api/src/types/ApiCommand.d.ts
  11. 1
      striker-ui-api/src/types/ApiHost.d.ts
  12. 2
      striker-ui-api/src/types/ApiSshKey.d.ts
  13. 5
      striker-ui-api/src/types/ErrorResponse.d.ts
  14. 5
      striker-ui/components/CrudList.tsx
  15. 33
      striker-ui/components/Dialog/DialogHeader.tsx
  16. 5
      striker-ui/components/Dialog/DialogWithHeader.tsx
  17. 84
      striker-ui/components/Display/FullSize.tsx
  18. 35
      striker-ui/components/Display/VncDisplay.tsx
  19. 3
      striker-ui/components/Files/schema.ts
  20. 5
      striker-ui/components/FormSummary.tsx
  21. 11
      striker-ui/components/ManageHost/ManageHost.tsx
  22. 75
      striker-ui/components/ManageHost/TestAccessForm.tsx
  23. 10
      striker-ui/components/ManageHost/schema.ts
  24. 9
      striker-ui/components/ManageHost/testAccessSchema.ts
  25. 7
      striker-ui/components/ManageMailRecipient/schema.ts
  26. 3
      striker-ui/components/ManageMailServer/schema.ts
  27. 2
      striker-ui/components/ManageManifest/AddManifestInputGroup.tsx
  28. 11
      striker-ui/components/ManageManifest/AnHostInputGroup.tsx
  29. 44
      striker-ui/components/ManageManifest/AnNetworkConfigInputGroup.tsx
  30. 2
      striker-ui/components/ManageManifest/EditManifestInputGroup.tsx
  31. 76
      striker-ui/components/ManageManifest/ManageManifestPanel.tsx
  32. 28
      striker-ui/components/ManageManifest/RunManifestInputGroup.tsx
  33. 3
      striker-ui/components/ProvisionServerDialog.tsx
  34. 21
      striker-ui/components/StrikerConfig/ConfigPeersForm.tsx
  35. 44
      striker-ui/hooks/useCookieJar.ts
  36. 17
      striker-ui/lib/yupMatches.ts
  37. 1
      striker-ui/out/_next/static/4RB26cd2zGZyKBQyjB24P/_buildManifest.js
  38. 1
      striker-ui/out/_next/static/SxEOmK8s3UBkjPP7eOep7/_buildManifest.js
  39. 0
      striker-ui/out/_next/static/SxEOmK8s3UBkjPP7eOep7/_ssgManifest.js
  40. 1
      striker-ui/out/_next/static/chunks/16-633a4da2be332451.js
  41. 1
      striker-ui/out/_next/static/chunks/16-8f130ff153ed09e1.js
  42. 2
      striker-ui/out/_next/static/chunks/466-40a89715cb183656.js
  43. 1
      striker-ui/out/_next/static/chunks/512-56563c67ec35f070.js
  44. 1
      striker-ui/out/_next/static/chunks/724.74a0b8e0158ff12a.js
  45. 1
      striker-ui/out/_next/static/chunks/724.9b0f3b59b9f819ec.js
  46. 1
      striker-ui/out/_next/static/chunks/762-6137fd9eb5f130da.js
  47. 1
      striker-ui/out/_next/static/chunks/762-c3bdcfb38ea6ff94.js
  48. 1
      striker-ui/out/_next/static/chunks/845-b3d5dd7a156a9380.js
  49. 2
      striker-ui/out/_next/static/chunks/pages/_app-979a3ab1fd6debc5.js
  50. 1
      striker-ui/out/_next/static/chunks/pages/config-396facf1669ffe17.js
  51. 1
      striker-ui/out/_next/static/chunks/pages/config-cab528473cc20327.js
  52. 1
      striker-ui/out/_next/static/chunks/pages/file-manager-1086cc9ed94415ae.js
  53. 1
      striker-ui/out/_next/static/chunks/pages/file-manager-a836cefe1c1c7d5f.js
  54. 2
      striker-ui/out/_next/static/chunks/pages/login-9acb46ff70465046.js
  55. 1
      striker-ui/out/_next/static/chunks/pages/mail-config-4ecabfd783a4abaf.js
  56. 1
      striker-ui/out/_next/static/chunks/pages/mail-config-cc0f4d97fffbb64c.js
  57. 1
      striker-ui/out/_next/static/chunks/pages/manage-element-0a2d309344524020.js
  58. 1
      striker-ui/out/_next/static/chunks/pages/manage-element-3d0a368d3c926f1d.js
  59. 2
      striker-ui/out/_next/static/chunks/pages/server-9fd04502dddda042.js
  60. 2
      striker-ui/out/_next/static/chunks/webpack-a4ad8f3183d9bd89.js
  61. 2
      striker-ui/out/anvil.html
  62. 2
      striker-ui/out/config.html
  63. 2
      striker-ui/out/file-manager.html
  64. 2
      striker-ui/out/index.html
  65. 2
      striker-ui/out/init.html
  66. 2
      striker-ui/out/login.html
  67. 2
      striker-ui/out/mail-config.html
  68. 2
      striker-ui/out/manage-element.html
  69. 2
      striker-ui/out/server.html
  70. 28
      striker-ui/package-lock.json
  71. 1
      striker-ui/types/APICommand.d.ts
  72. 5
      striker-ui/types/APIError.d.ts
  73. 1
      striker-ui/types/APIHost.d.ts
  74. 1
      striker-ui/types/ConfigPeerForm.d.ts
  75. 3
      striker-ui/types/CrudList.d.ts
  76. 1
      striker-ui/types/Dialog.d.ts
  77. 1
      striker-ui/types/ManageHost.d.ts
  78. 2
      striker-ui/types/ManageManifest.d.ts
  79. 4
      striker-ui/types/VncDisplay.d.ts
  80. 1
      striker-ui/types/novnc__novnc.d.ts
  81. 29
      tools/anvil-manage-vnc-pipe

File diff suppressed because one or more lines are too long

@ -3022,11 +3022,11 @@
} }
}, },
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dependencies": { "dependencies": {
"fill-range": "^7.0.1" "fill-range": "^7.1.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -4144,9 +4144,9 @@
} }
}, },
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@ -8827,11 +8827,11 @@
} }
}, },
"braces": { "braces": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"requires": { "requires": {
"fill-range": "^7.0.1" "fill-range": "^7.1.1"
} }
}, },
"browserslist": { "browserslist": {
@ -9657,9 +9657,9 @@
} }
}, },
"fill-range": { "fill-range": {
"version": "7.0.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"requires": { "requires": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
} }

@ -0,0 +1 @@
export const WS_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

@ -6,7 +6,9 @@ export * from './AN_VARIABLE_NAME_LIST';
export * from './DELETED'; export * from './DELETED';
export * from './ENV'; export * from './ENV';
export * from './EXIT_CODE_LIST'; export * from './EXIT_CODE_LIST';
export * from './HOST_KEY_CHANGED_PREFIX';
export * from './LOCAL'; export * from './LOCAL';
export * from './NODE_AND_DR_RESERVED_MEMORY_SIZE'; export * from './NODE_AND_DR_RESERVED_MEMORY_SIZE';
export * from './OS_LIST'; export * from './OS_LIST';
export * from './REG_EXP_PATTERNS'; export * from './REG_EXP_PATTERNS';
export * from './WS_GUID';

@ -1,16 +1,19 @@
import assert from 'assert'; import assert from 'assert';
import { RequestHandler } from 'express'; import { RequestHandler } from 'express';
import { REP_IPV4, REP_PEACEFUL_STRING } from '../../consts'; import {
import { HOST_KEY_CHANGED_PREFIX } from '../../consts/HOST_KEY_CHANGED_PREFIX'; HOST_KEY_CHANGED_PREFIX,
REP_IPV4,
REP_PEACEFUL_STRING,
} from '../../consts';
import { getLocalHostUUID, getPeerData, query } from '../../accessModule'; import { getPeerData, query } from '../../accessModule';
import { sanitize } from '../../sanitize'; import { sanitize } from '../../sanitize';
import { perr } from '../../shell'; import { perr } from '../../shell';
export const getHostSSH: RequestHandler< export const getHostSSH: RequestHandler<
unknown, unknown,
GetHostSshResponseBody, GetHostSshResponseBody | ErrorResponseBody,
GetHostSshRequestBody GetHostSshRequestBody
> = async (request, response) => { > = async (request, response) => {
const { const {
@ -42,36 +45,61 @@ export const getHostSSH: RequestHandler<
return response.status(400).send(); return response.status(400).send();
} }
const localHostUUID = getLocalHostUUID();
let rsbody: GetHostSshResponseBody; let rsbody: GetHostSshResponseBody;
try { try {
rsbody = await getPeerData(target, { password, port }); rsbody = await getPeerData(target, { password, port });
} catch (subError) { } catch (error) {
perr(`Failed to get peer data; CAUSE: ${subError}`); const emsg = `Failed to get peer data; CAUSE: ${error}`;
perr(emsg);
const rserror: ErrorResponseBody = {
code: 'fe14fb1',
message: emsg,
name: 'AccessError',
};
return response.status(500).send(); return response.status(500).send(rserror);
} }
if (!rsbody.isConnected) { let states: [string, string][];
const rows: [stateNote: string, stateUUID: string][] = await query(`
SELECT sta.state_note, sta.state_uuid
FROM states AS sta
WHERE sta.state_host_uuid = '${localHostUUID}'
AND sta.state_name = '${HOST_KEY_CHANGED_PREFIX}${target}';`);
if (rows.length > 0) { try {
rsbody.badSSHKeys = rows.reduce<DeleteSshKeyConflictRequestBody>( states = await query<[stateUuid: string, hostUuid: string][]>(`
(previous, [, stateUUID]) => { SELECT a.state_uuid, a.state_host_uuid
previous[localHostUUID].push(stateUUID); FROM states AS a
WHERE a.state_name = '${HOST_KEY_CHANGED_PREFIX}${target}';`);
} catch (error) {
const emsg = `Failed to list SSH key conflicts; CAUSE: ${error}`;
perr(emsg);
const rserror: ErrorResponseBody = {
code: 'd5a2acf',
message: emsg,
name: 'AccessError',
};
return response.status(500).send(rserror);
}
if (states.length > 0) {
rsbody.badSshKeys = states.reduce<DeleteSshKeyConflictRequestBody>(
(previous, state) => {
const [stateUuid, hostUuid] = state;
const { [hostUuid]: list = [] } = previous;
list.push(stateUuid);
previous[hostUuid] = list;
return previous; return previous;
}, },
{ [localHostUUID]: [] }, {},
); );
} }
}
response.status(200).send(rsbody); response.status(200).send(rsbody);
}; };

@ -1,6 +1,7 @@
import { getDatabaseConfigData, getLocalHostUUID } from '../../accessModule'; import { getDatabaseConfigData, getLocalHostUUID } from '../../accessModule';
import { buildUnknownIDCondition } from '../../buildCondition'; import { buildUnknownIDCondition } from '../../buildCondition';
import buildGetRequestHandler from '../buildGetRequestHandler'; import buildGetRequestHandler from '../buildGetRequestHandler';
import { buildQueryResultReducer } from '../../buildQueryResultModifier';
import { toLocal } from '../../convertHostUUID'; import { toLocal } from '../../convertHostUUID';
import { match } from '../../match'; import { match } from '../../match';
import { pout, poutvar } from '../../shell'; import { pout, poutvar } from '../../shell';
@ -55,7 +56,7 @@ export const getHostConnection = buildGetRequestHandler(
let rawDatabaseData: AnvilDataDatabaseHash; let rawDatabaseData: AnvilDataDatabaseHash;
const hostUUIDField = 'ip_add.ip_address_host_uuid'; const hostUUIDField = 'a.ip_address_host_uuid';
const localHostUUID: string = getLocalHostUUID(); const localHostUUID: string = getLocalHostUUID();
const { after: condHostUUIDs, before: beforeBuildIDCond } = const { after: condHostUUIDs, before: beforeBuildIDCond } =
buildUnknownIDCondition(rawHostUUIDs, hostUUIDField, { buildUnknownIDCondition(rawHostUUIDs, hostUUIDField, {
@ -88,52 +89,56 @@ export const getHostConnection = buildGetRequestHandler(
poutvar(connections, 'connections='); poutvar(connections, 'connections=');
if (buildQueryOptions) { if (buildQueryOptions) {
buildQueryOptions.afterQueryReturn = (queryStdout) => { buildQueryOptions.afterQueryReturn = buildQueryResultReducer(
let result = queryStdout; (previous, row) => {
const [ipUuid, hostUuid, ip, ifaceId] = row;
if (queryStdout instanceof Array) {
queryStdout.forEach(
([ipAddressUUID, hostUUID, ipAddress, network]) => {
const [, networkType, rawNetworkNumber, rawNetworkLinkNumber] =
match(network, /^([^\s]+)(\d+)_[^\s]+(\d+)$/);
const connectionKey = getConnectionKey(hostUUID);
connections[connectionKey].inbound.ipAddress[ipAddress] = { const [, networkType, rNetworkNumber, rNetworkLinkNumber] = match(
hostUUID, ifaceId,
ipAddress, /^(.*n)(\d+)_link(\d+)$/,
ipAddressUUID, );
networkLinkNumber: Number(rawNetworkLinkNumber), const connectionKey = getConnectionKey(hostUuid);
networkNumber: Number(rawNetworkNumber),
connections[connectionKey].inbound.ipAddress[ip] = {
hostUUID: hostUuid,
ifaceId,
ipAddress: ip,
ipAddressUUID: ipUuid,
networkLinkNumber: Number(rNetworkLinkNumber),
networkNumber: Number(rNetworkNumber),
networkType, networkType,
}; };
return previous;
}, },
connections,
); );
result = connections;
}
return result;
};
} }
return `SELECT return `SELECT
ip_add.ip_address_uuid, a.ip_address_uuid,
ip_add.ip_address_host_uuid, a.ip_address_host_uuid,
ip_add.ip_address_address, a.ip_address_address,
CASE
WHEN a.ip_address_on_type = 'interface'
THEN (
CASE CASE
WHEN ip_add.ip_address_on_type = 'interface' WHEN b.network_interface_name ~* '.*n\\d+_link\\d+'
THEN net_int.network_interface_name THEN b.network_interface_name
ELSE bon.bond_active_interface ELSE b.network_interface_device
END
)
ELSE d.bond_active_interface
END AS network_name END AS network_name
FROM ip_addresses AS ip_add FROM ip_addresses AS a
LEFT JOIN network_interfaces AS net_int LEFT JOIN network_interfaces AS b
ON ip_add.ip_address_on_uuid = net_int.network_interface_uuid ON a.ip_address_on_uuid = b.network_interface_uuid
LEFT JOIN bridges AS bri LEFT JOIN bridges AS c
ON ip_add.ip_address_on_uuid = bri.bridge_uuid ON a.ip_address_on_uuid = c.bridge_uuid
LEFT JOIN bonds AS bon LEFT JOIN bonds AS d
ON bri.bridge_uuid = bon.bond_bridge_uuid ON c.bridge_uuid = d.bond_bridge_uuid
OR ip_add.ip_address_on_uuid = bon.bond_uuid OR a.ip_address_on_uuid = d.bond_uuid
WHERE ${condHostUUIDs} WHERE ${condHostUUIDs}
AND ip_add.ip_address_note != 'DELETED';`; AND a.ip_address_note != 'DELETED';`;
}, },
); );

@ -235,6 +235,8 @@ export const buildManifest = async (
`Fence name must be a peaceful string; got [${fenceName}]`, `Fence name must be a peaceful string; got [${fenceName}]`,
); );
if (!port) return;
assert( assert(
REP_PEACEFUL_STRING.test(port), REP_PEACEFUL_STRING.test(port),
`Port of ${fenceName} must be a peaceful string; got [${port}]`, `Port of ${fenceName} must be a peaceful string; got [${port}]`,

@ -1,4 +1,4 @@
import { HOST_KEY_CHANGED_PREFIX } from '../../consts/HOST_KEY_CHANGED_PREFIX'; import { HOST_KEY_CHANGED_PREFIX } from '../../consts';
import { getLocalHostUUID } from '../../accessModule'; import { getLocalHostUUID } from '../../accessModule';
import buildGetRequestHandler from '../buildGetRequestHandler'; import buildGetRequestHandler from '../buildGetRequestHandler';

@ -1,22 +1,24 @@
import { createHash } from 'crypto';
import { createProxyMiddleware } from 'http-proxy-middleware'; import { createProxyMiddleware } from 'http-proxy-middleware';
import { P_UUID } from '../lib/consts'; import { P_UUID, WS_GUID } from '../lib/consts';
import { perr, pout } from '../lib/shell';
import { getVncinfo } from '../lib/accessModule'; import { getVncinfo } from '../lib/accessModule';
import { cname } from '../lib/cname';
import { perr, pout, poutvar } from '../lib/shell';
const WS_SVR_VNC_URL_PREFIX = '/ws/server/vnc'; const WS_SVR_VNC_URL_PREFIX = '/ws/server/vnc';
const getServerUuid = (url = '') =>
url.replace(new RegExp(`^${WS_SVR_VNC_URL_PREFIX}/(${P_UUID})`), '$1');
export const proxyServerVnc = createProxyMiddleware({ export const proxyServerVnc = createProxyMiddleware({
changeOrigin: true, changeOrigin: true,
pathFilter: `${WS_SVR_VNC_URL_PREFIX}/*`, pathFilter: `${WS_SVR_VNC_URL_PREFIX}/*`,
router: async (request) => { router: async (request) => {
const { url = '' } = request; const { url } = request;
const serverUuid = url.replace( const serverUuid = getServerUuid(url);
new RegExp(`^${WS_SVR_VNC_URL_PREFIX}/(${P_UUID})`),
'$1',
);
pout(`Got param [${serverUuid}] from [${url}]`); pout(`Got param [${serverUuid}] from [${url}]`);
@ -38,17 +40,55 @@ export const proxyServerVnc = createProxyMiddleware({
error: (error, request, response) => { error: (error, request, response) => {
perr(`VNC proxy error: ${error}`); perr(`VNC proxy error: ${error}`);
let resType: string; if (!response) {
perr(`Missing response; got [${response}]`);
return;
}
const serverUuid = getServerUuid(request.url);
const errapiName = cname(`vncerror.${serverUuid}`);
const errapiObj: ErrorResponseBody = {
code: '72c969b',
message: error.message,
name: error.name,
};
const errapiStr = JSON.stringify(errapiObj);
const errapiValue = encodeURIComponent(errapiStr);
const errapiCookie = `${errapiName}=j:${errapiValue}; Path=/server; SameSite=Lax; Max-Age=3`;
poutvar({ errapiCookie }, 'Error cookie: ');
if ('writeHead' in response) { if ('writeHead' in response) {
resType = 'ServerResponse'; pout('Found ServerResponse object');
response.writeHead(500).end(); return response
} else { .writeHead(500, {
resType = 'Socket'; 'Set-Cookie': `${errapiCookie}`,
})
.end();
} }
pout(`Response type = ${resType}`); pout(`Found Socket object`);
const {
headers: { 'sec-websocket-key': wskey },
} = request;
const wsaccept = createHash('sha1')
.update(wskey + WS_GUID, 'binary')
.digest('base64');
const headers = [
'HTTP/1.1 101 Switching Protocols',
'Connection: upgrade',
`Sec-WebSocket-Accept: ${wsaccept}`,
`Set-Cookie: ${errapiCookie}`,
'Upgrade: websocket',
];
response.end(`${headers.join('\r\n')}\r\n`, 'utf-8');
}, },
}, },
ws: true, ws: true,

@ -5,7 +5,7 @@ type GetHostSshRequestBody = {
}; };
type GetHostSshResponseBody = { type GetHostSshResponseBody = {
badSSHKeys?: DeleteSshKeyConflictRequestBody; badSshKeys?: DeleteSshKeyConflictRequestBody;
hostName: string; hostName: string;
hostOS: string; hostOS: string;
hostUUID: string; hostUUID: string;

@ -19,6 +19,7 @@ type HostConnectionOverview = {
ipAddress: { ipAddress: {
[ipAddress: string]: { [ipAddress: string]: {
hostUUID: string; hostUUID: string;
ifaceId: string;
ipAddress: string; ipAddress: string;
ipAddressUUID: string; ipAddressUUID: string;
networkLinkNumber: number; networkLinkNumber: number;

@ -9,4 +9,4 @@ type SshKeyConflict = {
}; };
}; };
type DeleteSshKeyConflictRequestBody = { [hostUUID: string]: string[] }; type DeleteSshKeyConflictRequestBody = Record<string, string[]>;

@ -0,0 +1,5 @@
type ErrorResponseBody = {
code: string;
message: string;
name: string;
};

@ -25,6 +25,7 @@ const CrudList = <
addHeader: rAddHeader, addHeader: rAddHeader,
editHeader: rEditHeader, editHeader: rEditHeader,
entriesUrl, entriesUrl,
formDialogProps,
getAddLoading, getAddLoading,
getDeleteErrorMessage, getDeleteErrorMessage,
getDeleteHeader, getDeleteHeader,
@ -184,6 +185,8 @@ const CrudList = <
loading={getAddLoading?.call(null)} loading={getAddLoading?.call(null)}
ref={addDialogRef} ref={addDialogRef}
showClose showClose
{...formDialogProps?.common}
{...formDialogProps?.add}
> >
{renderAddForm(formTools)} {renderAddForm(formTools)}
</DialogWithHeader> </DialogWithHeader>
@ -192,6 +195,8 @@ const CrudList = <
loading={getEditLoading(loadingEntry)} loading={getEditLoading(loadingEntry)}
ref={editDialogRef} ref={editDialogRef}
showClose showClose
{...formDialogProps?.common}
{...formDialogProps?.edit}
> >
{renderEditForm(formTools, entry)} {renderEditForm(formTools, entry)}
</DialogWithHeader> </DialogWithHeader>

@ -1,4 +1,4 @@
import { FC, ReactNode, useContext, useMemo } from 'react'; import { FC, ReactNode, useCallback, useContext, useMemo } from 'react';
import { DialogContext } from './Dialog'; import { DialogContext } from './Dialog';
import IconButton from '../IconButton'; import IconButton from '../IconButton';
@ -7,10 +7,29 @@ import sxstring from '../../lib/sxstring';
import { HeaderText } from '../Text'; import { HeaderText } from '../Text';
const DialogHeader: FC<DialogHeaderProps> = (props) => { const DialogHeader: FC<DialogHeaderProps> = (props) => {
const { children, showClose } = props; const {
children,
onClose = ({ handlers: { base } }, ...args) => base?.call(null, ...args),
showClose,
} = props;
const dialogContext = useContext(DialogContext); const dialogContext = useContext(DialogContext);
const closeHandler = useCallback<ButtonClickEventHandler>(
(...args) =>
onClose(
{
handlers: {
base: () => {
dialogContext?.setOpen(false);
},
},
},
...args,
),
[dialogContext, onClose],
);
const title = useMemo<ReactNode>( const title = useMemo<ReactNode>(
() => sxstring(children, HeaderText), () => sxstring(children, HeaderText),
[children], [children],
@ -19,15 +38,9 @@ const DialogHeader: FC<DialogHeaderProps> = (props) => {
const close = useMemo<ReactNode>( const close = useMemo<ReactNode>(
() => () =>
showClose && ( showClose && (
<IconButton <IconButton mapPreset="close" onClick={closeHandler} size="small" />
mapPreset="close"
onClick={() => {
dialogContext?.setOpen(false);
}}
size="small"
/>
), ),
[dialogContext, showClose], [closeHandler, showClose],
); );
return ( return (

@ -18,6 +18,7 @@ const DialogWithHeader: ForwardRefExoticComponent<
dialogProps, dialogProps,
header, header,
loading, loading,
onClose,
openInitially, openInitially,
showClose, showClose,
wide, wide,
@ -31,7 +32,9 @@ const DialogWithHeader: ForwardRefExoticComponent<
ref={ref} ref={ref}
wide={wide} wide={wide}
> >
<DialogHeader showClose={showClose}>{header}</DialogHeader> <DialogHeader onClose={onClose} showClose={showClose}>
{header}
</DialogHeader>
{children} {children}
</Dialog> </Dialog>
); );

@ -15,7 +15,8 @@ import MenuItem from '../MenuItem';
import { Panel, PanelHeader } from '../Panels'; import { Panel, PanelHeader } from '../Panels';
import ServerMenu from '../ServerMenu'; import ServerMenu from '../ServerMenu';
import Spinner from '../Spinner'; import Spinner from '../Spinner';
import { HeaderText } from '../Text'; import { BodyText, HeaderText } from '../Text';
import useCookieJar from '../../hooks/useCookieJar';
import useIsFirstRender from '../../hooks/useIsFirstRender'; import useIsFirstRender from '../../hooks/useIsFirstRender';
const PREFIX = 'FullSize'; const PREFIX = 'FullSize';
@ -43,7 +44,12 @@ const StyledDiv = styled('div')(() => ({
const VncDisplay = dynamic(() => import('./VncDisplay'), { ssr: false }); const VncDisplay = dynamic(() => import('./VncDisplay'), { ssr: false });
// Unit: seconds // Unit: seconds
const DEFAULT_VNC_RECONNECT_TIMER_START = 5; const DEFAULT_VNC_RECONNECT_TIMER_START = 10;
const MAP_TO_WSCODE_MSG: Record<number, string> = {
1000: 'in-use by another process?',
1006: 'destination is down?',
};
const buildServerVncUrl = (host: string, serverUuid: string) => const buildServerVncUrl = (host: string, serverUuid: string) =>
`ws://${host}/ws/server/vnc/${serverUuid}`; `ws://${host}/ws/server/vnc/${serverUuid}`;
@ -54,6 +60,7 @@ const FullSize: FC<FullSizeProps> = ({
serverName, serverName,
vncReconnectTimerStart = DEFAULT_VNC_RECONNECT_TIMER_START, vncReconnectTimerStart = DEFAULT_VNC_RECONNECT_TIMER_START,
}): JSX.Element => { }): JSX.Element => {
const { buildCookieJar } = useCookieJar();
const isFirstRender = useIsFirstRender(); const isFirstRender = useIsFirstRender();
const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null); const [anchorEl, setAnchorEl] = useState<HTMLElement | null>(null);
@ -61,7 +68,15 @@ const FullSize: FC<FullSizeProps> = ({
Partial<RfbConnectArgs> | undefined Partial<RfbConnectArgs> | undefined
>(undefined); >(undefined);
const [vncConnecting, setVncConnecting] = useState<boolean>(false); const [vncConnecting, setVncConnecting] = useState<boolean>(false);
const [vncError, setVncError] = useState<boolean>(false); const [vncError, setVncError] = useState<boolean>(false);
const [vncWsErrorMessage, setVncWsErrorMessage] = useState<
string | undefined
>();
const [vncApiErrorMessage, setVncApiErrorMessage] = useState<
string | undefined
>();
const [vncReconnectTimer, setVncReconnectTimer] = useState<number>( const [vncReconnectTimer, setVncReconnectTimer] = useState<number>(
vncReconnectTimerStart, vncReconnectTimerStart,
); );
@ -138,17 +153,61 @@ const FullSize: FC<FullSizeProps> = ({
// 'disconnect' event emits when a connection fails, // 'disconnect' event emits when a connection fails,
// OR when a user closes the existing connection. // OR when a user closes the existing connection.
const rfbDisconnectEventHandler = useCallback( const rfbDisconnectEventHandler = useCallback(
({ detail: { clean } }) => { (event) => {
if (!clean) { const { detail } = event;
const { clean } = detail;
if (clean) return;
setVncConnecting(false); setVncConnecting(false);
setVncError(true); setVncError(true);
updateVncReconnectTimer(); updateVncReconnectTimer();
}
}, },
[updateVncReconnectTimer], [updateVncReconnectTimer],
); );
const wsCloseEventHandler = useCallback(
(event?: WebsockCloseEvent): void => {
if (!event) {
setVncWsErrorMessage(undefined);
return;
}
const { code: wscode, reason } = event;
let wsmsg = `ws: ${wscode}`;
const guess = MAP_TO_WSCODE_MSG[wscode];
if (guess) {
wsmsg += ` (${guess})`;
}
if (reason) {
wsmsg += `, ${reason}`;
}
setVncWsErrorMessage(wsmsg);
const vncerror = buildCookieJar()[
`suiapi.vncerror.${serverUUID}`
] as APIError;
if (!vncerror) {
setVncApiErrorMessage(undefined);
return;
}
const { code: apicode, message } = vncerror;
setVncApiErrorMessage(`api: ${apicode}, ${message}`);
},
[buildCookieJar, serverUUID],
);
const showScreen = useMemo( const showScreen = useMemo(
() => !vncConnecting && !vncError, () => !vncConnecting && !vncError,
[vncConnecting, vncError], [vncConnecting, vncError],
@ -281,27 +340,26 @@ const FullSize: FC<FullSizeProps> = ({
<VncDisplay <VncDisplay
onConnect={rfbConnectEventHandler} onConnect={rfbConnectEventHandler}
onDisconnect={rfbDisconnectEventHandler} onDisconnect={rfbDisconnectEventHandler}
onWsClose={wsCloseEventHandler}
rfb={rfb} rfb={rfb}
rfbConnectArgs={rfbConnectArgs} rfbConnectArgs={rfbConnectArgs}
rfbScreen={rfbScreen} rfbScreen={rfbScreen}
/> />
</Box> </Box>
{!showScreen && ( {!showScreen && (
<Box display="flex" className={classes.spinnerBox}> <Box display="flex" className={classes.spinnerBox} textAlign="center">
{vncConnecting && ( {vncConnecting && (
<> <>
<HeaderText textAlign="center"> <HeaderText>Connecting to {serverName}.</HeaderText>
Connecting to {serverName}.
</HeaderText>
<Spinner /> <Spinner />
</> </>
)} )}
{vncError && ( {vncError && (
<> <>
<HeaderText textAlign="center"> <HeaderText>Can&apos;t connect to the server.</HeaderText>
There was a problem connecting to the server. <BodyText>{vncApiErrorMessage}</BodyText>
</HeaderText> <BodyText>{vncWsErrorMessage}</BodyText>
<HeaderText textAlign="center" mt="1em"> <HeaderText mt=".5em">
Retrying in {vncReconnectTimer}. Retrying in {vncReconnectTimer}.
</HeaderText> </HeaderText>
</> </>

@ -1,4 +1,5 @@
import RFB from '@novnc/novnc/core/rfb'; import RFB from '@novnc/novnc/core/rfb';
import Websock from '@novnc/novnc/core/websock';
import { useEffect } from 'react'; import { useEffect } from 'react';
const rfbConnect: RfbConnectFunction = ({ const rfbConnect: RfbConnectFunction = ({
@ -9,6 +10,8 @@ const rfbConnect: RfbConnectFunction = ({
focusOnClick = false, focusOnClick = false,
onConnect, onConnect,
onDisconnect, onDisconnect,
onWsClose,
onWsError,
qualityLevel = 6, qualityLevel = 6,
resizeSession = true, resizeSession = true,
rfb, rfb,
@ -45,6 +48,23 @@ const rfbConnect: RfbConnectFunction = ({
if (onDisconnect) { if (onDisconnect) {
rfb.current.addEventListener('disconnect', onDisconnect); rfb.current.addEventListener('disconnect', onDisconnect);
} }
/* eslint-disable no-underscore-dangle */
const ws: typeof Websock = rfb.current._sock;
const socketClose = ws._eventHandlers.close;
const socketError = ws._eventHandlers.error;
ws.on('close', (e?: WebsockCloseEvent) => {
socketClose(e);
onWsClose?.call(null, e);
});
ws.on('error', (e: Event) => {
socketError(e);
onWsError?.call(null, e);
});
/* eslint-enable no-underscore-dangle */
}; };
const rfbDisconnect: RfbDisconnectFunction = (rfb) => { const rfbDisconnect: RfbDisconnectFunction = (rfb) => {
@ -58,6 +78,8 @@ const VncDisplay = (props: VncDisplayProps): JSX.Element => {
const { const {
onConnect, onConnect,
onDisconnect, onDisconnect,
onWsClose,
onWsError,
rfb, rfb,
rfbConnectArgs, rfbConnectArgs,
rfbScreen, rfbScreen,
@ -73,6 +95,8 @@ const VncDisplay = (props: VncDisplayProps): JSX.Element => {
const args: RfbConnectArgs = { const args: RfbConnectArgs = {
onConnect, onConnect,
onDisconnect, onDisconnect,
onWsClose,
onWsError,
rfb, rfb,
rfbScreen, rfbScreen,
url, url,
@ -83,7 +107,16 @@ const VncDisplay = (props: VncDisplayProps): JSX.Element => {
} else { } else {
rfbDisconnect(rfb); rfbDisconnect(rfb);
} }
}, [initUrl, onConnect, onDisconnect, rfb, rfbConnectArgs, rfbScreen]); }, [
initUrl,
onConnect,
onDisconnect,
onWsClose,
onWsError,
rfb,
rfbConnectArgs,
rfbScreen,
]);
useEffect( useEffect(
() => () => { () => () => {

@ -1,6 +1,7 @@
import * as yup from 'yup'; import * as yup from 'yup';
import buildYupDynamicObject from '../../lib/buildYupDynamicObject'; import buildYupDynamicObject from '../../lib/buildYupDynamicObject';
import { yupLaxUuid } from '../../lib/yupMatches';
const fileLocationSchema = yup.object({ active: yup.boolean().required() }); const fileLocationSchema = yup.object({ active: yup.boolean().required() });
@ -19,7 +20,7 @@ const fileSchema = yup.object({
}), }),
name: yup.string().required(), name: yup.string().required(),
type: yup.string().oneOf(['iso', 'other', 'script']), type: yup.string().oneOf(['iso', 'other', 'script']),
uuid: yup.string().uuid().required(), uuid: yupLaxUuid().required(),
}); });
const fileListSchema = yup.lazy((files) => const fileListSchema = yup.lazy((files) =>

@ -24,7 +24,10 @@ const renderEntryValueWithPassword: RenderFormValueFunction = (args) => {
const renderEntryValueBase: RenderFormValueFunction = (args) => { const renderEntryValueBase: RenderFormValueFunction = (args) => {
const { entry, hasPassword } = args; const { entry, hasPassword } = args;
if (['', null, undefined].some((bad) => entry === bad)) { if (
['', null, undefined].some((bad) => entry === bad) ||
Number.isNaN(entry)
) {
return <BodyText>none</BodyText>; return <BodyText>none</BodyText>;
} }

@ -16,6 +16,15 @@ const ManageHost: FC = () => {
return ( return (
<CrudList<APIHostOverview, APIHostDetail> <CrudList<APIHostOverview, APIHostDetail>
formDialogProps={{
common: {
onClose: ({ handlers: { base } }, ...args) => {
base?.call(null, ...args);
// Delay to avoid visual changes until dialog is fully closed.
setTimeout(setInquireHostResponse, 500);
},
},
}}
addHeader="Initialize host" addHeader="Initialize host"
editHeader="" editHeader=""
entriesUrl="/host?types=dr,node" entriesUrl="/host?types=dr,node"
@ -38,7 +47,7 @@ const ManageHost: FC = () => {
}} }}
renderAddForm={(tools) => ( renderAddForm={(tools) => (
<> <>
<TestAccessForm setResponse={setInquireHostResponse} /> <TestAccessForm setResponse={setInquireHostResponse} tools={tools} />
{inquireHostResponse && ( {inquireHostResponse && (
<PrepareHostForm host={inquireHostResponse} tools={tools} /> <PrepareHostForm host={inquireHostResponse} tools={tools} />
)} )}

@ -10,17 +10,19 @@ import UncontrolledInput from '../UncontrolledInput';
import useFormikUtils from '../../hooks/useFormikUtils'; import useFormikUtils from '../../hooks/useFormikUtils';
import Spinner from '../Spinner'; import Spinner from '../Spinner';
import schema from './testAccessSchema'; import schema from './testAccessSchema';
import { BodyText } from '../Text';
const TestAccessForm: FC<TestAccessFormProps> = (props) => { const TestAccessForm: FC<TestAccessFormProps> = (props) => {
const { setResponse } = props; const { setResponse, tools } = props;
const messageGroupRef = useRef<MessageGroupForwardedRefContent>(null); const messageGroupRef = useRef<MessageGroupForwardedRefContent>(null);
const [loadingInquiry, setLoadingInquiry] = useState<boolean>(false); const [loadingInquiry, setLoadingInquiry] = useState<boolean>(false);
const [moreActions, setMoreActions] = useState<ContainedButtonProps[]>([]);
const setApiMessage = useCallback( const setApiMessage = useCallback(
(message?: Message) => (message?: Message) =>
messageGroupRef?.current?.setMessage?.call(null, 'api', message), messageGroupRef.current?.setMessage?.call(null, 'api', message),
[], [],
); );
@ -33,6 +35,7 @@ const TestAccessForm: FC<TestAccessFormProps> = (props) => {
onSubmit: (values, { setSubmitting }) => { onSubmit: (values, { setSubmitting }) => {
setApiMessage(); setApiMessage();
setLoadingInquiry(true); setLoadingInquiry(true);
setMoreActions([]);
setResponse(undefined); setResponse(undefined);
const { ip, password } = values; const { ip, password } = values;
@ -43,7 +46,72 @@ const TestAccessForm: FC<TestAccessFormProps> = (props) => {
password, password,
}) })
.then(({ data }) => { .then(({ data }) => {
const { isConnected } = data; const { badSshKeys, isConnected } = data;
if (badSshKeys) {
setApiMessage({
children: (
<>
Host identification at {ip} changed. If this is valid,
please delete the conflicting SSH host key.
</>
),
type: 'warning',
});
setMoreActions([
{
background: 'red',
children: 'Delete keys',
onClick: () => {
tools.confirm.prepare({
actionProceedText: 'Delete',
content: (
<BodyText>
There&apos;s a different host key on {ip}, which could
mean a MITM attack. But if this change is expected,
you can delete the known host key(s) to resolve the
conflict.
</BodyText>
),
onProceedAppend: () => {
tools.confirm.loading(true);
api
.delete('/ssh-key/conflict', {
data: badSshKeys,
})
.then(() => {
tools.confirm.finish('Success', {
children: (
<>Started job to delete host key(s) for {ip}.</>
),
});
setMoreActions([]);
})
.catch((error) => {
const emsg = handleAPIError(error);
emsg.children = (
<>Failed to delete host key(s). {emsg.children}</>
);
tools.confirm.finish('Error', emsg);
});
},
proceedColour: 'red',
titleText: `Delete all known SSH host key(s) for ${ip}?`,
});
tools.confirm.open(true);
},
type: 'button',
},
]);
return;
}
if (!isConnected) { if (!isConnected) {
setApiMessage({ setApiMessage({
@ -141,6 +209,7 @@ const TestAccessForm: FC<TestAccessFormProps> = (props) => {
) : ( ) : (
<ActionGroup <ActionGroup
actions={[ actions={[
...moreActions,
{ {
background: 'blue', background: 'blue',
children: 'Test access', children: 'Test access',

@ -1,13 +1,11 @@
import * as yup from 'yup'; import * as yup from 'yup';
import { REP_IPV4 } from '../../lib/consts/REG_EXP_PATTERNS'; import { yupIpv4, yupLaxUuid } from '../../lib/yupMatches';
const schema = yup.object().shape( const schema = yup.object().shape(
{ {
enterpriseKey: yup.string().uuid().optional(), enterpriseKey: yupLaxUuid().optional(),
ip: yup.string().matches(REP_IPV4, { ip: yupIpv4().required(),
message: 'Expected IP address to be a valid IPv4 address.',
}),
name: yup.string().required(), name: yup.string().required(),
redhatConfirmPassword: yup redhatConfirmPassword: yup
.string() .string()
@ -27,7 +25,7 @@ const schema = yup.object().shape(
String(redhatPassword).length > 0 ? field.required() : field.optional(), String(redhatPassword).length > 0 ? field.required() : field.optional(),
), ),
type: yup.string().oneOf(['dr', 'subnode']).required(), type: yup.string().oneOf(['dr', 'subnode']).required(),
uuid: yup.string().uuid().required(), uuid: yupLaxUuid().required(),
}, },
[['redhatUsername', 'redhatPassword']], [['redhatUsername', 'redhatPassword']],
); );

@ -1,14 +1,9 @@
import * as yup from 'yup'; import * as yup from 'yup';
import { REP_IPV4 } from '../../lib/consts/REG_EXP_PATTERNS'; import { yupIpv4 } from '../../lib/yupMatches';
const schema = yup.object({ const schema = yup.object({
ip: yup ip: yupIpv4().required(),
.string()
.matches(REP_IPV4, {
message: 'Expected IP address to be a valid IPv4 address.',
})
.required(),
password: yup.string().required(), password: yup.string().required(),
}); });

@ -1,6 +1,7 @@
import * as yup from 'yup'; import * as yup from 'yup';
import buildYupDynamicObject from '../../lib/buildYupDynamicObject'; import buildYupDynamicObject from '../../lib/buildYupDynamicObject';
import { yupLaxUuid } from '../../lib/yupMatches';
const alertLevelSchema = yup.number().oneOf([0, 1, 2, 3, 4]); const alertLevelSchema = yup.number().oneOf([0, 1, 2, 3, 4]);
@ -9,9 +10,9 @@ const alertOverrideSchema = yup.object({
level: alertLevelSchema.required(), level: alertLevelSchema.required(),
target: yup.object({ target: yup.object({
type: yup.string().oneOf(['node', 'subnode']).required(), type: yup.string().oneOf(['node', 'subnode']).required(),
uuid: yup.string().uuid().required(), uuid: yupLaxUuid().required(),
}), }),
uuid: yup.string().uuid().optional(), uuid: yupLaxUuid().optional(),
}); });
const alertOverrideListSchema = yup.lazy((entries) => const alertOverrideListSchema = yup.lazy((entries) =>
@ -24,7 +25,7 @@ const mailRecipientSchema = yup.object({
language: yup.string().oneOf(['en_CA']).optional(), language: yup.string().oneOf(['en_CA']).optional(),
level: alertLevelSchema.required(), level: alertLevelSchema.required(),
name: yup.string().required(), name: yup.string().required(),
uuid: yup.string().uuid().optional(), uuid: yupLaxUuid().optional(),
}); });
const mailRecipientListSchema = yup.lazy((entries) => const mailRecipientListSchema = yup.lazy((entries) =>

@ -1,6 +1,7 @@
import * as yup from 'yup'; import * as yup from 'yup';
import buildYupDynamicObject from '../../lib/buildYupDynamicObject'; import buildYupDynamicObject from '../../lib/buildYupDynamicObject';
import { yupLaxUuid } from '../../lib/yupMatches';
const mailServerSchema = yup.object({ const mailServerSchema = yup.object({
address: yup.string().required(), address: yup.string().required(),
@ -17,7 +18,7 @@ const mailServerSchema = yup.object({
port: yup.number().required(), port: yup.number().required(),
security: yup.string().oneOf(['none', 'starttls', 'tls-ssl']), security: yup.string().oneOf(['none', 'starttls', 'tls-ssl']),
username: yup.string().optional(), username: yup.string().optional(),
uuid: yup.string().uuid().required(), uuid: yupLaxUuid().required(),
}); });
const mailServerListSchema = yup.lazy((mailServers) => const mailServerListSchema = yup.lazy((mailServers) =>

@ -8,7 +8,6 @@ import AnIdInputGroup, {
} from './AnIdInputGroup'; } from './AnIdInputGroup';
import AnNetworkConfigInputGroup, { import AnNetworkConfigInputGroup, {
INPUT_ID_ANC_DNS, INPUT_ID_ANC_DNS,
INPUT_ID_ANC_MTU,
INPUT_ID_ANC_NTP, INPUT_ID_ANC_NTP,
} from './AnNetworkConfigInputGroup'; } from './AnNetworkConfigInputGroup';
import FlexBox from '../FlexBox'; import FlexBox from '../FlexBox';
@ -41,7 +40,6 @@ const AddManifestInputGroup = <
| typeof INPUT_ID_AI_PREFIX | typeof INPUT_ID_AI_PREFIX
| typeof INPUT_ID_AI_SEQUENCE | typeof INPUT_ID_AI_SEQUENCE
| typeof INPUT_ID_ANC_DNS | typeof INPUT_ID_ANC_DNS
| typeof INPUT_ID_ANC_MTU
| typeof INPUT_ID_ANC_NTP]: string; | typeof INPUT_ID_ANC_NTP]: string;
}, },
>({ >({

@ -3,6 +3,7 @@ import { ReactElement, useMemo } from 'react';
import FlexBox from '../FlexBox'; import FlexBox from '../FlexBox';
import Grid from '../Grid'; import Grid from '../Grid';
import InputWithRef from '../InputWithRef'; import InputWithRef from '../InputWithRef';
import MessageBox from '../MessageBox';
import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; import OutlinedInputWithLabel from '../OutlinedInputWithLabel';
import { InnerPanel, InnerPanelBody, InnerPanelHeader } from '../Panels'; import { InnerPanel, InnerPanelBody, InnerPanelHeader } from '../Panels';
import SwitchWithLabel from '../SwitchWithLabel'; import SwitchWithLabel from '../SwitchWithLabel';
@ -202,7 +203,6 @@ const AnHostInputGroup = <M extends MapToInputTestID>({
}, },
)} )}
onFirstRender={buildInputFirstRenderFunction(inputId)} onFirstRender={buildInputFirstRenderFunction(inputId)}
required
/> />
), ),
}; };
@ -345,6 +345,15 @@ const AnHostInputGroup = <M extends MapToInputTestID>({
<Grid <Grid
columns={GRID_COLUMNS} columns={GRID_COLUMNS}
layout={{ layout={{
'fence-message': {
children: (
<MessageBox>
It is recommended to provide 2 fence device ports.
</MessageBox>
),
width: '100%',
xs: 0,
},
...networkListGridLayout, ...networkListGridLayout,
[inputCellIdAHIpmiIp]: { [inputCellIdAHIpmiIp]: {
children: ( children: (

@ -10,21 +10,16 @@ import Grid from '../Grid';
import IconButton from '../IconButton'; import IconButton from '../IconButton';
import InputWithRef from '../InputWithRef'; import InputWithRef from '../InputWithRef';
import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; import OutlinedInputWithLabel from '../OutlinedInputWithLabel';
import { import { buildIpCsvTestBatch } from '../../lib/test_input';
buildIpCsvTestBatch,
buildNumberTestBatch,
} from '../../lib/test_input';
const INPUT_ID_PREFIX_AN_NETWORK_CONFIG = 'an-network-config-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_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_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_ID_ANC_NTP = `${INPUT_ID_PREFIX_AN_NETWORK_CONFIG}-ntp`;
const INPUT_LABEL_ANC_DNS = 'DNS'; const INPUT_LABEL_ANC_DNS = 'DNS';
const INPUT_LABEL_ANC_MTU = 'MTU';
const INPUT_LABEL_ANC_NTP = 'NTP'; const INPUT_LABEL_ANC_NTP = 'NTP';
const DEFAULT_DNS_CSV = '8.8.8.8,8.8.4.4'; const DEFAULT_DNS_CSV = '8.8.8.8,8.8.4.4';
@ -73,17 +68,13 @@ const guessNetworkMinIp = ({
const AnNetworkConfigInputGroup = < const AnNetworkConfigInputGroup = <
M extends MapToInputTestID & { M extends MapToInputTestID & {
[K in [K in typeof INPUT_ID_ANC_DNS | typeof INPUT_ID_ANC_NTP]: string;
| typeof INPUT_ID_ANC_DNS
| typeof INPUT_ID_ANC_MTU
| typeof INPUT_ID_ANC_NTP]: string;
}, },
>({ >({
formUtils, formUtils,
networkListEntries, networkListEntries,
previous: { previous: {
dnsCsv: previousDnsCsv = DEFAULT_DNS_CSV, dnsCsv: previousDnsCsv = DEFAULT_DNS_CSV,
mtu: previousMtu,
ntpCsv: previousNtpCsv, ntpCsv: previousNtpCsv,
} = {}, } = {},
setNetworkList, setNetworkList,
@ -448,41 +439,12 @@ const AnNetworkConfigInputGroup = <
/> />
), ),
}, },
'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" spacing="1em"
/> />
); );
}; };
export { INPUT_ID_ANC_DNS, INPUT_ID_ANC_MTU, INPUT_ID_ANC_NTP }; export { INPUT_ID_ANC_DNS, INPUT_ID_ANC_NTP };
export default AnNetworkConfigInputGroup; export default AnNetworkConfigInputGroup;

@ -7,7 +7,6 @@ import {
} from './AnIdInputGroup'; } from './AnIdInputGroup';
import { import {
INPUT_ID_ANC_DNS, INPUT_ID_ANC_DNS,
INPUT_ID_ANC_MTU,
INPUT_ID_ANC_NTP, INPUT_ID_ANC_NTP,
} from './AnNetworkConfigInputGroup'; } from './AnNetworkConfigInputGroup';
import AddManifestInputGroup from './AddManifestInputGroup'; import AddManifestInputGroup from './AddManifestInputGroup';
@ -19,7 +18,6 @@ const EditManifestInputGroup = <
| typeof INPUT_ID_AI_PREFIX | typeof INPUT_ID_AI_PREFIX
| typeof INPUT_ID_AI_SEQUENCE | typeof INPUT_ID_AI_SEQUENCE
| typeof INPUT_ID_ANC_DNS | typeof INPUT_ID_ANC_DNS
| typeof INPUT_ID_ANC_MTU
| typeof INPUT_ID_ANC_NTP]: string; | typeof INPUT_ID_ANC_NTP]: string;
}, },
>({ >({

@ -18,7 +18,6 @@ import {
} from './AnNetworkInputGroup'; } from './AnNetworkInputGroup';
import { import {
INPUT_ID_ANC_DNS, INPUT_ID_ANC_DNS,
INPUT_ID_ANC_MTU,
INPUT_ID_ANC_NTP, INPUT_ID_ANC_NTP,
} from './AnNetworkConfigInputGroup'; } from './AnNetworkConfigInputGroup';
import api from '../../lib/api'; import api from '../../lib/api';
@ -30,6 +29,7 @@ import FormSummary from '../FormSummary';
import handleAPIError from '../../lib/handleAPIError'; import handleAPIError from '../../lib/handleAPIError';
import IconButton from '../IconButton'; import IconButton from '../IconButton';
import List from '../List'; import List from '../List';
import MessageBox from '../MessageBox';
import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup'; import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup';
import { Panel, PanelHeader } from '../Panels'; import { Panel, PanelHeader } from '../Panels';
import periodicFetch from '../../lib/fetchers/periodicFetch'; import periodicFetch from '../../lib/fetchers/periodicFetch';
@ -65,14 +65,10 @@ const getFormData = (
const { value: dnsCsv } = elements.namedItem( const { value: dnsCsv } = elements.namedItem(
INPUT_ID_ANC_DNS, INPUT_ID_ANC_DNS,
) as HTMLInputElement; ) as HTMLInputElement;
const { value: rawMtu } = elements.namedItem(
INPUT_ID_ANC_MTU,
) as HTMLInputElement;
const { value: ntpCsv } = elements.namedItem( const { value: ntpCsv } = elements.namedItem(
INPUT_ID_ANC_NTP, INPUT_ID_ANC_NTP,
) as HTMLInputElement; ) as HTMLInputElement;
const mtu = Number.parseInt(rawMtu, 10);
const sequence = Number.parseInt(rawSequence, 10); const sequence = Number.parseInt(rawSequence, 10);
return Object.values(elements).reduce<APIBuildManifestRequestBody>( return Object.values(elements).reduce<APIBuildManifestRequestBody>(
@ -104,7 +100,6 @@ const getFormData = (
hostConfig: { hosts: {} }, hostConfig: { hosts: {} },
networkConfig: { networkConfig: {
dnsCsv, dnsCsv,
mtu,
networks: {}, networks: {},
ntpCsv, ntpCsv,
}, },
@ -192,7 +187,6 @@ const ManageManifestPanel: FC = () => {
INPUT_ID_AI_PREFIX, INPUT_ID_AI_PREFIX,
INPUT_ID_AI_SEQUENCE, INPUT_ID_AI_SEQUENCE,
INPUT_ID_ANC_DNS, INPUT_ID_ANC_DNS,
INPUT_ID_ANC_MTU,
INPUT_ID_ANC_NTP, INPUT_ID_ANC_NTP,
], ],
messageGroupRef, messageGroupRef,
@ -243,6 +237,62 @@ const ManageManifestPanel: FC = () => {
[manifestTemplate], [manifestTemplate],
); );
const countHostFences = useCallback(
(
body: APIBuildManifestRequestBody,
): { counts: Record<string, number>; messages: React.ReactNode[] } => {
const {
hostConfig: { hosts },
} = body;
const counts = Object.values(hosts).reduce<Record<string, number>>(
(previous, host) => {
const { fences, hostType, hostNumber } = host;
const hostName = `${hostType.replace(
/node/,
'subnode',
)}${hostNumber}`;
if (!fences) {
previous[hostName] = 0;
return previous;
}
previous[hostName] = Object.values(fences).reduce<number>(
(count, fence) => {
const { fencePort } = fence;
const diff = fencePort.length ? 1 : 0;
return count + diff;
},
0,
);
return previous;
},
{},
);
const messages = Object.entries(counts).map((entry) => {
const [hostName, fenceCount] = entry;
return fenceCount ? (
<></>
) : (
<MessageBox key={`${hostName}-no-fence-port-message`}>
No fence device port specified for {hostName}.
</MessageBox>
);
});
return { counts, messages };
},
[],
);
const addManifestFormDialogProps = useMemo<ConfirmDialogProps>( const addManifestFormDialogProps = useMemo<ConfirmDialogProps>(
() => ({ () => ({
actionProceedText: 'Add', actionProceedText: 'Add',
@ -260,6 +310,7 @@ const ManageManifestPanel: FC = () => {
), ),
onSubmitAppend: (...args) => { onSubmitAppend: (...args) => {
const body = getFormData(...args); const body = getFormData(...args);
const { messages } = countHostFences(body);
setConfirmDialogProps({ setConfirmDialogProps({
actionProceedText: 'Add', actionProceedText: 'Add',
@ -276,6 +327,7 @@ const ManageManifestPanel: FC = () => {
url: '/manifest', url: '/manifest',
}); });
}, },
preActionArea: <FlexBox spacing=".3em">{messages}</FlexBox>,
titleText: `Add install manifest?`, titleText: `Add install manifest?`,
}); });
@ -284,6 +336,7 @@ const ManageManifestPanel: FC = () => {
titleText: 'Add an install manifest', titleText: 'Add an install manifest',
}), }),
[ [
countHostFences,
formUtils, formUtils,
getManifestOverviews, getManifestOverviews,
knownFences, knownFences,
@ -309,6 +362,7 @@ const ManageManifestPanel: FC = () => {
), ),
onSubmitAppend: (...args) => { onSubmitAppend: (...args) => {
const body = getFormData(...args); const body = getFormData(...args);
const { messages } = countHostFences(body);
setConfirmDialogProps({ setConfirmDialogProps({
actionProceedText: 'Edit', actionProceedText: 'Edit',
@ -325,6 +379,7 @@ const ManageManifestPanel: FC = () => {
url: `/manifest/${mdetailUuid}`, url: `/manifest/${mdetailUuid}`,
}); });
}, },
preActionArea: <FlexBox spacing=".3em">{messages}</FlexBox>,
titleText: `Update install manifest ${mdetailName}?`, titleText: `Update install manifest ${mdetailName}?`,
}); });
@ -334,16 +389,17 @@ const ManageManifestPanel: FC = () => {
titleText: `Update install manifest ${mdetailName}`, titleText: `Update install manifest ${mdetailName}`,
}), }),
[ [
countHostFences,
formUtils, formUtils,
getManifestOverviews,
isLoadingManifestDetail,
knownFences, knownFences,
knownUpses, knownUpses,
manifestDetail, manifestDetail,
isLoadingManifestDetail,
mdetailName, mdetailName,
mdetailUuid,
setConfirmDialogProps, setConfirmDialogProps,
submitForm, submitForm,
mdetailUuid,
getManifestOverviews,
], ],
); );

@ -45,12 +45,7 @@ const RunManifestInputGroup = <M extends MapToInputTestID>({
const passwordRef = useRef<InputForwardedRefContent<'string'>>({}); const passwordRef = useRef<InputForwardedRefContent<'string'>>({});
const { hosts: initHostList = {} } = hostConfig; const { hosts: initHostList = {} } = hostConfig;
const { const { dnsCsv, networks: initNetworkList = {}, ntpCsv } = networkConfig;
dnsCsv,
mtu,
networks: initNetworkList = {},
ntpCsv = MANIFEST_PARAM_NONE,
} = networkConfig;
const hostListEntries = useMemo( const hostListEntries = useMemo(
() => Object.entries(initHostList), () => Object.entries(initHostList),
@ -195,12 +190,10 @@ const RunManifestInputGroup = <M extends MapToInputTestID>({
}; };
hostListEntries.forEach(([hostId, { networks = {} }]) => { hostListEntries.forEach(([hostId, { networks = {} }]) => {
const { const { [networkId]: { networkIp: ip = '' } = {} } = networks;
[networkId]: { networkIp: ip = MANIFEST_PARAM_NONE } = {},
} = networks;
hostNetworks[`${idPrefix}-${hostId}-ip`] = { hostNetworks[`${idPrefix}-${hostId}-ip`] = {
children: <MonoText>{ip}</MonoText>, children: <MonoText>{ip || MANIFEST_PARAM_NONE}</MonoText>,
}; };
}); });
@ -237,11 +230,10 @@ const RunManifestInputGroup = <M extends MapToInputTestID>({
}; };
hostListEntries.forEach(([hostId, { fences = {} }]) => { hostListEntries.forEach(([hostId, { fences = {} }]) => {
const { [fenceName]: { fencePort = MANIFEST_PARAM_NONE } = {} } = const { [fenceName]: { fencePort = '' } = {} } = fences;
fences;
previous[`${idPrefix}-${hostId}-port`] = { previous[`${idPrefix}-${hostId}-port`] = {
children: <MonoText>{fencePort}</MonoText>, children: <MonoText>{fencePort || MANIFEST_PARAM_NONE}</MonoText>,
}; };
}); });
@ -426,19 +418,13 @@ const RunManifestInputGroup = <M extends MapToInputTestID>({
children: <BodyText>DNS</BodyText>, children: <BodyText>DNS</BodyText>,
}, },
'run-manifest-dns-csv-cell': { 'run-manifest-dns-csv-cell': {
children: <EndMono>{dnsCsv}</EndMono>, children: <EndMono>{dnsCsv || MANIFEST_PARAM_NONE}</EndMono>,
}, },
'run-manifest-ntp-csv-cell-header': { 'run-manifest-ntp-csv-cell-header': {
children: <BodyText>NTP</BodyText>, children: <BodyText>NTP</BodyText>,
}, },
'run-manifest-ntp-csv-cell': { 'run-manifest-ntp-csv-cell': {
children: <EndMono>{ntpCsv}</EndMono>, children: <EndMono>{ntpCsv || MANIFEST_PARAM_NONE}</EndMono>,
},
'run-manifest-mtu-cell-header': {
children: <BodyText>MTU</BodyText>,
},
'run-manifest-mtu-cell': {
children: <EndMono>{mtu}</EndMono>,
}, },
}} }}
spacing="0.4em" spacing="0.4em"

@ -1578,6 +1578,9 @@ const ProvisionServerDialog = ({
); );
setIsProvisionServerDataReady(true); setIsProvisionServerDataReady(true);
})
.catch(() => {
// Ignore for now; the 'no resources' message would trigger.
}); });
}, [initLimits]); }, [initLimits]);

@ -67,12 +67,16 @@ const ConfigPeersForm: FC<ConfigPeerFormProps> = ({
Object.entries(ipAddressList).reduce<InboundConnectionList>( Object.entries(ipAddressList).reduce<InboundConnectionList>(
( (
nyu, nyu,
[ipAddress, { networkLinkNumber, networkNumber, networkType }], [
ipAddress,
{ ifaceId, networkLinkNumber, networkNumber, networkType },
],
) => { ) => {
nyu[ipAddress] = { nyu[ipAddress] = {
...previous[ipAddress], ...previous[ipAddress],
dbPort, dbPort,
dbUser, dbUser,
ifaceId,
ipAddress, ipAddress,
networkLinkNumber, networkLinkNumber,
networkNumber, networkNumber,
@ -135,13 +139,20 @@ const ConfigPeersForm: FC<ConfigPeerFormProps> = ({
listItems={inboundConnections} listItems={inboundConnections}
renderListItem={( renderListItem={(
ipAddress, ipAddress,
{ dbPort, dbUser, networkNumber, networkType }, { dbPort, dbUser, ifaceId, networkNumber, networkType },
) => ( ) => {
const network: string =
NETWORK_TYPES[networkType] && networkNumber
? `${NETWORK_TYPES[networkType]} ${networkNumber}`
: `Unknown network; interface: ${ifaceId}`;
return (
<FlexBox spacing={0} sx={{ width: '100%' }}> <FlexBox spacing={0} sx={{ width: '100%' }}>
<MonoText>{`${dbUser}@${ipAddress}:${dbPort}`}</MonoText> <MonoText>{`${dbUser}@${ipAddress}:${dbPort}`}</MonoText>
<SmallText>{`${NETWORK_TYPES[networkType]} ${networkNumber}`}</SmallText> <SmallText>{network}</SmallText>
</FlexBox> </FlexBox>
)} );
}}
/> />
</Grid> </Grid>
<Grid item xs={1}> <Grid item xs={1}>

@ -4,6 +4,7 @@ import useIsFirstRender from './useIsFirstRender';
const useCookieJar = (): { const useCookieJar = (): {
cookieJar: CookieJar; cookieJar: CookieJar;
buildCookieJar: () => CookieJar;
getCookie: <T>(key: string) => T | undefined; getCookie: <T>(key: string) => T | undefined;
getSession: () => SessionCookie | undefined; getSession: () => SessionCookie | undefined;
getSessionUser: () => SessionCookieUser | undefined; getSessionUser: () => SessionCookieUser | undefined;
@ -12,25 +13,10 @@ const useCookieJar = (): {
const [cookieJar, setCookieJar] = useState<CookieJar>({}); const [cookieJar, setCookieJar] = useState<CookieJar>({});
const getCookie = useCallback( const buildCookieJar = useCallback(() => {
<T>(key: string, prefix = 'suiapi.') =>
cookieJar[`${prefix}${key}`] as T | undefined,
[cookieJar],
);
const getSession = useCallback(
() => getCookie<SessionCookie>('session'),
[getCookie],
);
const getSessionUser = useCallback(() => getSession()?.user, [getSession]);
useEffect(() => {
if (isFirstRender) {
const lines = document.cookie.split(/\s*;\s*/); const lines = document.cookie.split(/\s*;\s*/);
setCookieJar( const jar = lines.reduce<CookieJar>((previous, line) => {
lines.reduce<CookieJar>((previous, line) => {
const [key, value] = line.split('=', 2); const [key, value] = line.split('=', 2);
const decoded = decodeURIComponent(value); const decoded = decodeURIComponent(value);
@ -50,13 +36,33 @@ const useCookieJar = (): {
previous[key] = result; previous[key] = result;
return previous; return previous;
}, {}), }, {});
return jar;
}, []);
const getCookie = useCallback(
<T>(key: string, prefix = 'suiapi.') =>
cookieJar[`${prefix}${key}`] as T | undefined,
[cookieJar],
); );
const getSession = useCallback(
() => getCookie<SessionCookie>('session'),
[getCookie],
);
const getSessionUser = useCallback(() => getSession()?.user, [getSession]);
useEffect(() => {
if (isFirstRender) {
setCookieJar(buildCookieJar());
} }
}, [isFirstRender]); }, [buildCookieJar, isFirstRender]);
return { return {
cookieJar, cookieJar,
buildCookieJar,
getCookie, getCookie,
getSession, getSession,
getSessionUser, getSessionUser,

@ -0,0 +1,17 @@
import * as yup from 'yup';
import { REP_IPV4, REP_UUID } from './consts/REG_EXP_PATTERNS';
/**
* This is OK because yup uses the template string syntax internally to access
* the field name.
*/
/* eslint-disable no-template-curly-in-string */
export const yupLaxUuid = () =>
yup.string().matches(REP_UUID, { message: '${path} must be a valid UUID' });
export const yupIpv4 = () =>
yup
.string()
.matches(REP_IPV4, { message: '${path} must be a valid IPv4 address' });

@ -1 +0,0 @@
self.__BUILD_MANIFEST=function(s,c,a,e,t,i,n,f,b,u,k,h,j,g,r,d,l,_){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":[s,a,e,t,i,n,b,"static/chunks/715-31c42cd57b463d49.js",c,f,u,r,d,"static/chunks/pages/index-f7680dbed4474b4b.js"],"/_error":["static/chunks/pages/_error-8447282b6bcee29e.js"],"/anvil":[s,a,e,t,i,n,b,"static/chunks/680-2258b21ffebaf50b.js",c,f,r,"static/chunks/pages/anvil-c29ee8fc3eea3417.js"],"/config":[s,a,e,i,k,h,c,u,"static/chunks/pages/config-cab528473cc20327.js"],"/file-manager":[s,a,e,t,n,k,j,"static/chunks/579-6ba9d1157accb8a7.js",c,f,g,"static/chunks/pages/file-manager-1086cc9ed94415ae.js"],"/init":[s,a,t,i,n,b,h,l,c,f,_,"static/chunks/pages/init-afbc75b7ee36cb21.js"],"/login":[s,a,e,i,c,u,"static/chunks/pages/login-f5cfbd1de52c490d.js"],"/mail-config":[s,a,e,t,i,n,b,k,j,c,f,g,"static/chunks/pages/mail-config-cc0f4d97fffbb64c.js"],"/manage-element":[s,a,e,t,i,n,b,k,j,h,l,"static/chunks/858-f6bfa9b45bc673cc.js",c,f,u,g,_,"static/chunks/pages/manage-element-0a2d309344524020.js"],"/server":[s,e,t,c,d,"static/chunks/pages/server-5cd5f165d40a1eaa.js"],sortedPages:["/","/_app","/_error","/anvil","/config","/file-manager","/init","/login","/mail-config","/manage-element","/server"]}}("static/chunks/572-b5c29784d1349ae1.js","static/chunks/616-c4d59f8a6d39d5a4.js","static/chunks/442-b751672afa3cc53f.js","static/chunks/318-35524f40e72b9bd4.js","static/chunks/341-bdaf9b2461a83319.js","static/chunks/514-4ce501d9fa08982c.js","static/chunks/242-912372df2bb37c32.js","static/chunks/762-c3bdcfb38ea6ff94.js","static/chunks/74-9720e9bc600a2719.js","static/chunks/761-7379298625e9125e.js","static/chunks/461-8504faeaf244aab6.js","static/chunks/982-a80463e6b63f11a0.js","static/chunks/602-32dbc2a66990c0a6.js","static/chunks/845-b3d5dd7a156a9380.js","static/chunks/466-6093dd3c9e9ea062.js","static/chunks/16-633a4da2be332451.js","static/chunks/161-e5c89be90a214bca.js","static/chunks/784-0aa3ea101d582664.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

@ -0,0 +1 @@
self.__BUILD_MANIFEST=function(s,c,a,e,t,i,n,f,b,u,k,h,j,d,g,r,l,_){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":[s,a,e,t,i,n,b,"static/chunks/715-31c42cd57b463d49.js",c,f,u,g,r,"static/chunks/pages/index-f7680dbed4474b4b.js"],"/_error":["static/chunks/pages/_error-8447282b6bcee29e.js"],"/anvil":[s,a,e,t,i,n,b,"static/chunks/680-2258b21ffebaf50b.js",c,f,g,"static/chunks/pages/anvil-c29ee8fc3eea3417.js"],"/config":[s,a,e,i,k,h,c,u,"static/chunks/pages/config-396facf1669ffe17.js"],"/file-manager":[s,a,e,t,n,k,j,"static/chunks/579-6ba9d1157accb8a7.js",c,f,d,"static/chunks/pages/file-manager-a836cefe1c1c7d5f.js"],"/init":[s,a,t,i,n,b,h,l,c,f,_,"static/chunks/pages/init-afbc75b7ee36cb21.js"],"/login":[s,a,e,i,c,u,"static/chunks/pages/login-9acb46ff70465046.js"],"/mail-config":[s,a,e,t,i,n,b,k,j,c,f,d,"static/chunks/pages/mail-config-4ecabfd783a4abaf.js"],"/manage-element":[s,a,e,t,i,n,b,k,j,h,l,"static/chunks/858-f6bfa9b45bc673cc.js",c,f,u,d,_,"static/chunks/pages/manage-element-3d0a368d3c926f1d.js"],"/server":[s,e,t,c,r,"static/chunks/pages/server-9fd04502dddda042.js"],sortedPages:["/","/_app","/_error","/anvil","/config","/file-manager","/init","/login","/mail-config","/manage-element","/server"]}}("static/chunks/572-b5c29784d1349ae1.js","static/chunks/616-c4d59f8a6d39d5a4.js","static/chunks/442-b751672afa3cc53f.js","static/chunks/318-35524f40e72b9bd4.js","static/chunks/341-bdaf9b2461a83319.js","static/chunks/514-4ce501d9fa08982c.js","static/chunks/242-912372df2bb37c32.js","static/chunks/762-6137fd9eb5f130da.js","static/chunks/74-9720e9bc600a2719.js","static/chunks/761-7379298625e9125e.js","static/chunks/461-8504faeaf244aab6.js","static/chunks/982-a80463e6b63f11a0.js","static/chunks/602-32dbc2a66990c0a6.js","static/chunks/512-56563c67ec35f070.js","static/chunks/466-40a89715cb183656.js","static/chunks/16-8f130ff153ed09e1.js","static/chunks/161-e5c89be90a214bca.js","static/chunks/784-0aa3ea101d582664.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[724],{38724:function(e,r,n){n.r(r);var t=n(85893),c=n(94460),u=n(67294);let l=e=>{let{background:r="",clipViewport:n=!1,compressionLevel:t=2,dragViewport:u=!1,focusOnClick:l=!1,onConnect:o,onDisconnect:s,onWsClose:i,onWsError:a,qualityLevel:d=6,resizeSession:v=!0,rfb:f,rfbScreen:E,scaleViewport:p=!0,showDotCursor:m=!1,url:w,viewOnly:h=!1}=e;if(!(null==E?void 0:E.current)||(null==f?void 0:f.current))return;E.current.innerHTML="",f.current=new c.Z(E.current,w),f.current.background=r,f.current.clipViewport=n,f.current.compressionLevel=t,f.current.dragViewport=u,f.current.focusOnClick=l,f.current.qualityLevel=d,f.current.resizeSession=v,f.current.scaleViewport=p,f.current.showDotCursor=m,f.current.viewOnly=h,o&&f.current.addEventListener("connect",o),s&&f.current.addEventListener("disconnect",s);let k=f.current._sock,_=k._eventHandlers.close,L=k._eventHandlers.error;k.on("close",e=>{_(e),null==i||i.call(null,e)}),k.on("error",e=>{L(e),null==a||a.call(null,e)})},o=e=>{(null==e?void 0:e.current)&&(e.current.disconnect(),e.current=null)},s=e=>{let{onConnect:r,onDisconnect:n,onWsClose:c,onWsError:s,rfb:i,rfbConnectArgs:a,rfbScreen:d,url:v}=e;return(0,u.useEffect)(()=>{if(a){let{url:e=v}=a;e&&l({onConnect:r,onDisconnect:n,onWsClose:c,onWsError:s,rfb:i,rfbScreen:d,url:e,...a})}else o(i)},[v,r,n,c,s,i,a,d]),(0,u.useEffect)(()=>()=>{o(i)},[i]),(0,t.jsx)("div",{style:{width:"100%",height:"75vh"},ref:d,onMouseEnter:()=>{document.activeElement&&document.activeElement instanceof HTMLElement&&document.activeElement.blur(),(null==i?void 0:i.current)&&i.current.focus()}})};s.displayName="VncDisplay",r.default=s}}]);

@ -1 +0,0 @@
"use strict";(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[724],{38724:function(e,r,n){n.r(r);var t=n(85893),c=n(94460),u=n(67294);let l=e=>{let{background:r="",clipViewport:n=!1,compressionLevel:t=2,dragViewport:u=!1,focusOnClick:l=!1,onConnect:i,onDisconnect:s,qualityLevel:o=6,resizeSession:d=!0,rfb:a,rfbScreen:v,scaleViewport:f=!0,showDotCursor:E=!1,url:p,viewOnly:m=!1}=e;(null==v?void 0:v.current)&&(null==a||!a.current)&&(v.current.innerHTML="",a.current=new c.Z(v.current,p),a.current.background=r,a.current.clipViewport=n,a.current.compressionLevel=t,a.current.dragViewport=u,a.current.focusOnClick=l,a.current.qualityLevel=o,a.current.resizeSession=d,a.current.scaleViewport=f,a.current.showDotCursor=E,a.current.viewOnly=m,i&&a.current.addEventListener("connect",i),s&&a.current.addEventListener("disconnect",s))},i=e=>{(null==e?void 0:e.current)&&(e.current.disconnect(),e.current=null)},s=e=>{let{onConnect:r,onDisconnect:n,rfb:c,rfbConnectArgs:s,rfbScreen:o,url:d}=e;return(0,u.useEffect)(()=>{if(s){let{url:e=d}=s;e&&l({onConnect:r,onDisconnect:n,rfb:c,rfbScreen:o,url:e,...s})}else i(c)},[d,r,n,c,s,o]),(0,u.useEffect)(()=>()=>{i(c)},[c]),(0,t.jsx)("div",{style:{width:"100%",height:"75vh"},ref:o,onMouseEnter:()=>{document.activeElement&&document.activeElement instanceof HTMLElement&&document.activeElement.blur(),(null==c?void 0:c.current)&&c.current.focus()}})};s.displayName="VncDisplay",r.default=s}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -1 +1 @@
!function(){"use strict";var e,t,n,r,o,u,i,c,f,a={},l={};function d(e){var t=l[e];if(void 0!==t)return t.exports;var n=l[e]={id:e,loaded:!1,exports:{}},r=!0;try{a[e].call(n.exports,n,n.exports,d),r=!1}finally{r&&delete l[e]}return n.loaded=!0,n.exports}d.m=a,e=[],d.O=function(t,n,r,o){if(n){o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[n,r,o];return}for(var i=1/0,u=0;u<e.length;u++){for(var n=e[u][0],r=e[u][1],o=e[u][2],c=!0,f=0;f<n.length;f++)i>=o&&Object.keys(d.O).every(function(e){return d.O[e](n[f])})?n.splice(f--,1):(c=!1,o<i&&(i=o));if(c){e.splice(u--,1);var a=r();void 0!==a&&(t=a)}}return t},d.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return d.d(t,{a:t}),t},n=Object.getPrototypeOf?function(e){return Object.getPrototypeOf(e)}:function(e){return e.__proto__},d.t=function(e,r){if(1&r&&(e=this(e)),8&r||"object"==typeof e&&e&&(4&r&&e.__esModule||16&r&&"function"==typeof e.then))return e;var o=Object.create(null);d.r(o);var u={};t=t||[null,n({}),n([]),n(n)];for(var i=2&r&&e;"object"==typeof i&&!~t.indexOf(i);i=n(i))Object.getOwnPropertyNames(i).forEach(function(t){u[t]=function(){return e[t]}});return u.default=function(){return e},d.d(o,u),o},d.d=function(e,t){for(var n in t)d.o(t,n)&&!d.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},d.f={},d.e=function(e){return Promise.all(Object.keys(d.f).reduce(function(t,n){return d.f[n](e,t),t},[]))},d.u=function(e){return"static/chunks/"+e+"."+({460:"5494ba1e4d778d0d",724:"9b0f3b59b9f819ec"})[e]+".js"},d.miniCssF=function(e){},d.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),d.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r={},o="_N_E:",d.l=function(e,t,n,u){if(r[e]){r[e].push(t);return}if(void 0!==n)for(var i,c,f=document.getElementsByTagName("script"),a=0;a<f.length;a++){var l=f[a];if(l.getAttribute("src")==e||l.getAttribute("data-webpack")==o+n){i=l;break}}i||(c=!0,(i=document.createElement("script")).charset="utf-8",i.timeout=120,d.nc&&i.setAttribute("nonce",d.nc),i.setAttribute("data-webpack",o+n),i.src=d.tu(e)),r[e]=[t];var s=function(t,n){i.onerror=i.onload=null,clearTimeout(p);var o=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),o&&o.forEach(function(e){return e(n)}),t)return t(n)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=s.bind(null,i.onerror),i.onload=s.bind(null,i.onload),c&&document.head.appendChild(i)},d.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},d.nmd=function(e){return e.paths=[],e.children||(e.children=[]),e},d.tt=function(){return void 0===u&&(u={createScriptURL:function(e){return e}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(u=trustedTypes.createPolicy("nextjs#bundler",u))),u},d.tu=function(e){return d.tt().createScriptURL(e)},d.p="/_next/",i={272:0},d.f.j=function(e,t){var n=d.o(i,e)?i[e]:void 0;if(0!==n){if(n)t.push(n[2]);else if(272!=e){var r=new Promise(function(t,r){n=i[e]=[t,r]});t.push(n[2]=r);var o=d.p+d.u(e),u=Error();d.l(o,function(t){if(d.o(i,e)&&(0!==(n=i[e])&&(i[e]=void 0),n)){var r=t&&("load"===t.type?"missing":t.type),o=t&&t.target&&t.target.src;u.message="Loading chunk "+e+" failed.\n("+r+": "+o+")",u.name="ChunkLoadError",u.type=r,u.request=o,n[1](u)}},"chunk-"+e,e)}else i[e]=0}},d.O.j=function(e){return 0===i[e]},c=function(e,t){var n,r,o=t[0],u=t[1],c=t[2],f=0;if(o.some(function(e){return 0!==i[e]})){for(n in u)d.o(u,n)&&(d.m[n]=u[n]);if(c)var a=c(d)}for(e&&e(t);f<o.length;f++)r=o[f],d.o(i,r)&&i[r]&&i[r][0](),i[r]=0;return d.O(a)},(f=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(c.bind(null,0)),f.push=c.bind(null,f.push.bind(f))}(); !function(){"use strict";var e,t,n,r,o,u,i,c,f,a={},l={};function d(e){var t=l[e];if(void 0!==t)return t.exports;var n=l[e]={id:e,loaded:!1,exports:{}},r=!0;try{a[e].call(n.exports,n,n.exports,d),r=!1}finally{r&&delete l[e]}return n.loaded=!0,n.exports}d.m=a,e=[],d.O=function(t,n,r,o){if(n){o=o||0;for(var u=e.length;u>0&&e[u-1][2]>o;u--)e[u]=e[u-1];e[u]=[n,r,o];return}for(var i=1/0,u=0;u<e.length;u++){for(var n=e[u][0],r=e[u][1],o=e[u][2],c=!0,f=0;f<n.length;f++)i>=o&&Object.keys(d.O).every(function(e){return d.O[e](n[f])})?n.splice(f--,1):(c=!1,o<i&&(i=o));if(c){e.splice(u--,1);var a=r();void 0!==a&&(t=a)}}return t},d.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return d.d(t,{a:t}),t},n=Object.getPrototypeOf?function(e){return Object.getPrototypeOf(e)}:function(e){return e.__proto__},d.t=function(e,r){if(1&r&&(e=this(e)),8&r||"object"==typeof e&&e&&(4&r&&e.__esModule||16&r&&"function"==typeof e.then))return e;var o=Object.create(null);d.r(o);var u={};t=t||[null,n({}),n([]),n(n)];for(var i=2&r&&e;"object"==typeof i&&!~t.indexOf(i);i=n(i))Object.getOwnPropertyNames(i).forEach(function(t){u[t]=function(){return e[t]}});return u.default=function(){return e},d.d(o,u),o},d.d=function(e,t){for(var n in t)d.o(t,n)&&!d.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},d.f={},d.e=function(e){return Promise.all(Object.keys(d.f).reduce(function(t,n){return d.f[n](e,t),t},[]))},d.u=function(e){return"static/chunks/"+e+"."+({460:"5494ba1e4d778d0d",724:"74a0b8e0158ff12a"})[e]+".js"},d.miniCssF=function(e){},d.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||Function("return this")()}catch(e){if("object"==typeof window)return window}}(),d.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r={},o="_N_E:",d.l=function(e,t,n,u){if(r[e]){r[e].push(t);return}if(void 0!==n)for(var i,c,f=document.getElementsByTagName("script"),a=0;a<f.length;a++){var l=f[a];if(l.getAttribute("src")==e||l.getAttribute("data-webpack")==o+n){i=l;break}}i||(c=!0,(i=document.createElement("script")).charset="utf-8",i.timeout=120,d.nc&&i.setAttribute("nonce",d.nc),i.setAttribute("data-webpack",o+n),i.src=d.tu(e)),r[e]=[t];var s=function(t,n){i.onerror=i.onload=null,clearTimeout(p);var o=r[e];if(delete r[e],i.parentNode&&i.parentNode.removeChild(i),o&&o.forEach(function(e){return e(n)}),t)return t(n)},p=setTimeout(s.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=s.bind(null,i.onerror),i.onload=s.bind(null,i.onload),c&&document.head.appendChild(i)},d.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},d.nmd=function(e){return e.paths=[],e.children||(e.children=[]),e},d.tt=function(){return void 0===u&&(u={createScriptURL:function(e){return e}},"undefined"!=typeof trustedTypes&&trustedTypes.createPolicy&&(u=trustedTypes.createPolicy("nextjs#bundler",u))),u},d.tu=function(e){return d.tt().createScriptURL(e)},d.p="/_next/",i={272:0},d.f.j=function(e,t){var n=d.o(i,e)?i[e]:void 0;if(0!==n){if(n)t.push(n[2]);else if(272!=e){var r=new Promise(function(t,r){n=i[e]=[t,r]});t.push(n[2]=r);var o=d.p+d.u(e),u=Error();d.l(o,function(t){if(d.o(i,e)&&(0!==(n=i[e])&&(i[e]=void 0),n)){var r=t&&("load"===t.type?"missing":t.type),o=t&&t.target&&t.target.src;u.message="Loading chunk "+e+" failed.\n("+r+": "+o+")",u.name="ChunkLoadError",u.type=r,u.request=o,n[1](u)}},"chunk-"+e,e)}else i[e]=0}},d.O.j=function(e){return 0===i[e]},c=function(e,t){var n,r,o=t[0],u=t[1],c=t[2],f=0;if(o.some(function(e){return 0!==i[e]})){for(n in u)d.o(u,n)&&(d.m[n]=u[n]);if(c)var a=c(d)}for(e&&e(t);f<o.length;f++)r=o[f],d.o(i,r)&&i[r]&&i[r][0](),i[r]=0;return d.O(a)},(f=self.webpackChunk_N_E=self.webpackChunk_N_E||[]).forEach(c.bind(null,0)),f.push=c.bind(null,f.push.bind(f))}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -2842,12 +2842,12 @@
} }
}, },
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"fill-range": "^7.0.1" "fill-range": "^7.1.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -4149,9 +4149,9 @@
} }
}, },
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
@ -9247,12 +9247,12 @@
} }
}, },
"braces": { "braces": {
"version": "3.0.2", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dev": true, "dev": true,
"requires": { "requires": {
"fill-range": "^7.0.1" "fill-range": "^7.1.1"
} }
}, },
"browserslist": { "browserslist": {
@ -10238,9 +10238,9 @@
} }
}, },
"fill-range": { "fill-range": {
"version": "7.0.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dev": true, "dev": true,
"requires": { "requires": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"

@ -1,4 +1,5 @@
type APICommandInquireHostResponseBody = { type APICommandInquireHostResponseBody = {
badSshKeys?: Record<string, string[]>;
hostName: string; hostName: string;
hostOS: string; hostOS: string;
hostUUID: string; hostUUID: string;

@ -0,0 +1,5 @@
type APIError = {
code: string;
message: string;
name: string;
};

@ -4,6 +4,7 @@ type APIHostConnectionOverviewList = {
ipAddress: { ipAddress: {
[ipAddress: string]: { [ipAddress: string]: {
hostUUID: string; hostUUID: string;
ifaceId: string;
ipAddress: string; ipAddress: string;
ipAddressUUID: string; ipAddressUUID: string;
networkLinkNumber: number; networkLinkNumber: number;

@ -2,6 +2,7 @@ type InboundConnectionList = {
[ipAddress: string]: { [ipAddress: string]: {
dbPort: number; dbPort: number;
dbUser: string; dbUser: string;
ifaceId: string;
ipAddress: string; ipAddress: string;
networkLinkNumber: number; networkLinkNumber: number;
networkNumber: number; networkNumber: number;

@ -25,6 +25,9 @@ type DeletePromiseChainGetter<T> = (
type CrudListOptionalProps<Overview> = { type CrudListOptionalProps<Overview> = {
entryUrlPrefix?: string; entryUrlPrefix?: string;
formDialogProps?: Partial<
Record<'add' | 'common' | 'edit', Partial<DialogWithHeaderProps>>
>;
getAddLoading?: (previous?: boolean) => boolean; getAddLoading?: (previous?: boolean) => boolean;
getDeletePromiseChain?: <T>( getDeletePromiseChain?: <T>(
base: DeletePromiseChainGetter<T>, base: DeletePromiseChainGetter<T>,

@ -39,6 +39,7 @@ type DialogActionGroupProps = DialogActionGroupOptionalProps;
/** DialogHeader */ /** DialogHeader */
type DialogHeaderOptionalProps = { type DialogHeaderOptionalProps = {
onClose?: ExtendableEventHandler<ButtonClickEventHandler>;
showClose?: boolean; showClose?: boolean;
}; };

@ -14,6 +14,7 @@ type TestAccessFormProps = {
setResponse: React.Dispatch< setResponse: React.Dispatch<
React.SetStateAction<InquireHostResponse | undefined> React.SetStateAction<InquireHostResponse | undefined>
>; >;
tools: CrudListFormTools;
}; };
/** PrepareHostForm */ /** PrepareHostForm */

@ -18,8 +18,6 @@ type ManifestNetworkList = {
type ManifestNetworkConfig = { type ManifestNetworkConfig = {
dnsCsv: string; dnsCsv: string;
/** Max Transmission Unit (MTU); unit: bytes */
mtu: number;
networks: ManifestNetworkList; networks: ManifestNetworkList;
ntpCsv: string; ntpCsv: string;
}; };

@ -4,6 +4,8 @@ type RfbRef = import('react').MutableRefObject<
type RfbScreenRef = import('react').MutableRefObject<HTMLDivElement | null>; type RfbScreenRef = import('react').MutableRefObject<HTMLDivElement | null>;
type WebsockCloseEvent = Event & { code: number; reason: string };
type RfbConnectArgs = { type RfbConnectArgs = {
background?: string; background?: string;
clipViewport?: boolean; clipViewport?: boolean;
@ -12,6 +14,8 @@ type RfbConnectArgs = {
focusOnClick?: boolean; focusOnClick?: boolean;
onConnect?: () => void; onConnect?: () => void;
onDisconnect?: (event: { detail: { clean: boolean } }) => void; onDisconnect?: (event: { detail: { clean: boolean } }) => void;
onWsClose?: (event?: WebsockCloseEvent) => void;
onWsError?: (event: Event) => void;
qualityLevel?: number; qualityLevel?: number;
resizeSession?: boolean; resizeSession?: boolean;
rfb: RfbRef; rfb: RfbRef;

@ -1 +1,2 @@
declare module '@novnc/novnc/core/rfb'; declare module '@novnc/novnc/core/rfb';
declare module '@novnc/novnc/core/websock';

@ -46,7 +46,7 @@ my $server_vnc_port = $anvil->data->{switches}{'server-vnc-port'};
if (defined $server) if (defined $server)
{ {
$server_uuid //= is_uuid_v4($server) ? $server : $anvil->Get->server_uuid_from_name({ server_name => $server }); $server_uuid //= $anvil->Validate->uuid({ uuid => $server }) ? $server : $anvil->Get->server_uuid_from_name({ server_name => $server });
} }
$anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $switch_debug, list => { $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $switch_debug, list => {
@ -99,8 +99,7 @@ sub build_find_available_port_call
$anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "build_find_available_port_call" }); $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "build_find_available_port_call" });
return (1) if ( (not $step_operator =~ /^[+-]$/) return (1) if ( (not $step_operator =~ /^[+-]$/) || (not $anvil->Validate->positive_integer({ number => $step_size })) );
|| (not is_int($step_size)) || ($step_size < 1) );
my $call = "ss_output=\$($ss -ant) && port=${start} && while $grep -Eq \":\${port}[[:space:]]+[^[:space:]]+\" <<<\$ss_output; do (( port ${step_operator}= $step_size )); done && $echo \$port"; my $call = "ss_output=\$($ss -ant) && port=${start} && while $grep -Eq \":\${port}[[:space:]]+[^[:space:]]+\" <<<\$ss_output; do (( port ${step_operator}= $step_size )); done && $echo \$port";
@ -203,7 +202,7 @@ sub find_server_vnc_port
return (1) if (not defined $svr_uuid); return (1) if (not defined $svr_uuid);
return (0, $svr_vnc_port) if (is_int($svr_vnc_port)); return (0, $svr_vnc_port) if ($anvil->Validate->positive_integer({ number => $svr_vnc_port }));
# If we don't have the server's VNC port, find it in its qemu-kvm process. # If we don't have the server's VNC port, find it in its qemu-kvm process.
@ -253,16 +252,6 @@ sub find_ws_processes
return (0, $result); return (0, $result);
} }
sub is_int
{
return defined $_[0] && $_[0] =~ /^\d+$/;
}
sub is_uuid_v4
{
return defined $_[0] && $_[0] =~ /[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}/;
}
sub prettify sub prettify
{ {
my $var_value = shift; my $var_value = shift;
@ -325,7 +314,7 @@ sub set_vncinfo_variable
variable_value => "${local_host_name}:${end_port}", variable_value => "${local_host_name}:${end_port}",
}); });
return (1) if (not is_uuid_v4($variable_uuid)); return (1) if (not $anvil->Validate->uuid({ uuid => $variable_uuid }));
return (0); return (0);
} }
@ -377,7 +366,7 @@ sub start_pipe
my $svr_uuid = $parameters->{svr_uuid}; my $svr_uuid = $parameters->{svr_uuid};
my $svr_vnc_port = $parameters->{svr_vnc_port}; my $svr_vnc_port = $parameters->{svr_vnc_port};
return (1, __LINE__.": [$svr_uuid]") if (not is_uuid_v4($svr_uuid)); return (1, __LINE__.": [$svr_uuid]") if (not $anvil->Validate->uuid({ uuid => $svr_uuid }));
my $common_params = { debug => $debug }; my $common_params = { debug => $debug };
@ -416,8 +405,8 @@ sub start_ws
$anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "start_ws" }); $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "start_ws" });
return (1) if ( (not defined $ws_processes) return (1) if ( (not defined $ws_processes)
|| (not is_int($svr_vnc_port)) || (not $anvil->Validate->positive_integer({ number => $svr_vnc_port }))
|| (not is_int($ws_sport_offset)) ); || (not $anvil->Validate->positive_integer({ number => $ws_sport_offset })) );
my $existing_ws_pids = $ws_processes->{targets}{$svr_vnc_port}; my $existing_ws_pids = $ws_processes->{targets}{$svr_vnc_port};
@ -462,7 +451,7 @@ sub stop_pipe
my $svr_uuid = $parameters->{svr_uuid}; my $svr_uuid = $parameters->{svr_uuid};
my $svr_vnc_port = $parameters->{svr_vnc_port}; my $svr_vnc_port = $parameters->{svr_vnc_port};
return (1, __LINE__.": [$svr_uuid]") if (not is_uuid_v4($svr_uuid)); return (1, __LINE__.": [$svr_uuid]") if (not $anvil->Validate->uuid({ uuid => $svr_uuid }));
my $common_params = { debug => $debug }; my $common_params = { debug => $debug };
@ -511,7 +500,7 @@ sub stop_ws
$anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "stop_ws" }); $anvil->Log->variables({ source => $THIS_FILE, line => __LINE__, level => $debug, list => $parameters, prefix => "stop_ws" });
return (1) if ( (not is_int($ws_pid)) || (not defined $ws_processes) ); return (1) if ( (not $anvil->Validate->positive_integer({ number => $ws_pid })) || (not defined $ws_processes) );
call({ debug => $debug, call => "$kill $ws_pid || $kill -9 $ws_pid" }); call({ debug => $debug, call => "$kill $ws_pid || $kill -9 $ws_pid" });

Loading…
Cancel
Save