Merge pull request #318 from ylei-tsubame/manage-ups
Add UPS management tab
This commit is contained in:
commit
3701517b41
@ -134,10 +134,10 @@ const dbWrite = (query: string, options?: SpawnSyncOptions) => {
|
||||
return execAnvilAccessModule(['--query', query, '--mode', 'write'], options);
|
||||
};
|
||||
|
||||
const getAnvilData = (
|
||||
const getAnvilData = <HashType>(
|
||||
dataStruct: AnvilDataStruct,
|
||||
{ predata, ...spawnSyncOptions }: GetAnvilDataOptions = {},
|
||||
) =>
|
||||
): HashType =>
|
||||
execAnvilAccessModule(
|
||||
[
|
||||
'--predata',
|
||||
|
@ -6,7 +6,7 @@ export const getFenceTemplate: RequestHandler = (request, response) => {
|
||||
let rawFenceData;
|
||||
|
||||
try {
|
||||
({ fence_data: rawFenceData } = getAnvilData(
|
||||
({ fence_data: rawFenceData } = getAnvilData<{ fence_data: unknown }>(
|
||||
{ fence_data: true },
|
||||
{ predata: [['Striker->get_fence_data']] },
|
||||
));
|
||||
|
@ -154,7 +154,7 @@ export const createHostConnection: RequestHandler<
|
||||
database: {
|
||||
[localHostUUID]: { port: rawLocalDBPort },
|
||||
},
|
||||
} = getAnvilData({ database: true }) as { database: DatabaseHash };
|
||||
} = getAnvilData<{ database: AnvilDataDatabaseHash }>({ database: true });
|
||||
|
||||
localDBPort = sanitize(rawLocalDBPort, 'number');
|
||||
} catch (subError) {
|
||||
|
@ -7,7 +7,7 @@ import { stdout } from '../../shell';
|
||||
|
||||
const buildHostConnections = (
|
||||
fromHostUUID: string,
|
||||
databaseHash: DatabaseHash,
|
||||
databaseHash: AnvilDataDatabaseHash,
|
||||
{
|
||||
defaultPort = 5432,
|
||||
defaultUser = 'admin',
|
||||
@ -42,7 +42,7 @@ export const getHostConnection = buildGetRequestHandler(
|
||||
(request, buildQueryOptions) => {
|
||||
const { hostUUIDs: rawHostUUIDs } = request.query;
|
||||
|
||||
let rawDatabaseData: DatabaseHash;
|
||||
let rawDatabaseData: AnvilDataDatabaseHash;
|
||||
|
||||
const hostUUIDField = 'ip_add.ip_address_host_uuid';
|
||||
const localHostUUID: string = getLocalHostUUID();
|
||||
@ -59,7 +59,9 @@ export const getHostConnection = buildGetRequestHandler(
|
||||
stdout(`condHostUUIDs=[${condHostUUIDs}]`);
|
||||
|
||||
try {
|
||||
({ database: rawDatabaseData } = getAnvilData({ database: true }));
|
||||
({ database: rawDatabaseData } = getAnvilData<{ database: AnvilDataDatabaseHash }>(
|
||||
{ database: true },
|
||||
));
|
||||
} catch (subError) {
|
||||
throw new Error(`Failed to get anvil data; CAUSE: ${subError}`);
|
||||
}
|
||||
|
37
striker-ui-api/src/lib/request_handlers/ups/getUPS.ts
Normal file
37
striker-ui-api/src/lib/request_handlers/ups/getUPS.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { RequestHandler } from 'express';
|
||||
|
||||
import buildGetRequestHandler from '../buildGetRequestHandler';
|
||||
import { buildQueryResultReducer } from '../../buildQueryResultModifier';
|
||||
|
||||
export const getUPS: RequestHandler = buildGetRequestHandler(
|
||||
(request, buildQueryOptions) => {
|
||||
const query = `
|
||||
SELECT
|
||||
ups_uuid,
|
||||
ups_name,
|
||||
ups_agent,
|
||||
ups_ip_address
|
||||
FROM upses
|
||||
ORDER BY ups_name ASC;`;
|
||||
const afterQueryReturn: QueryResultModifierFunction | undefined =
|
||||
buildQueryResultReducer<{ [upsUUID: string]: UPSOverview }>(
|
||||
(previous, [upsUUID, upsName, upsAgent, upsIPAddress]) => {
|
||||
previous[upsUUID] = {
|
||||
upsAgent,
|
||||
upsIPAddress,
|
||||
upsName,
|
||||
upsUUID,
|
||||
};
|
||||
|
||||
return previous;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
if (buildQueryOptions) {
|
||||
buildQueryOptions.afterQueryReturn = afterQueryReturn;
|
||||
}
|
||||
|
||||
return query;
|
||||
},
|
||||
);
|
@ -0,0 +1,52 @@
|
||||
import { RequestHandler } from 'express';
|
||||
|
||||
import { getAnvilData } from '../../accessModule';
|
||||
import { stderr } from '../../shell';
|
||||
|
||||
export const getUPSTemplate: RequestHandler = (request, response) => {
|
||||
let rawUPSData: AnvilDataUPSHash;
|
||||
|
||||
try {
|
||||
({ ups_data: rawUPSData } = getAnvilData<{ ups_data: AnvilDataUPSHash }>(
|
||||
{ ups_data: true },
|
||||
{ predata: [['Striker->get_ups_data']] },
|
||||
));
|
||||
} catch (subError) {
|
||||
stderr(`Failed to get ups template; CAUSE: ${subError}`);
|
||||
|
||||
response.status(500).send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const upsData: AnvilDataUPSHash = Object.entries(
|
||||
rawUPSData,
|
||||
).reduce<UPSTemplate>((previous, [upsTypeId, value]) => {
|
||||
const { brand, description: rawDescription, ...rest } = value;
|
||||
|
||||
const matched = rawDescription.match(
|
||||
/^(.+)\s+[-]\s+[<][^>]+href=[\\"]+([^\s]+)[\\"]+.+[>]([^<]+)[<]/,
|
||||
);
|
||||
const result: UPSTemplate[string] = {
|
||||
...rest,
|
||||
brand,
|
||||
description: rawDescription,
|
||||
links: {},
|
||||
};
|
||||
|
||||
if (matched) {
|
||||
const [, description, linkHref, linkLabel] = matched;
|
||||
|
||||
result.description = description;
|
||||
result.links[0] = { linkHref, linkLabel };
|
||||
}
|
||||
|
||||
if (/apc/i.test(brand)) {
|
||||
previous[upsTypeId] = result;
|
||||
}
|
||||
|
||||
return previous;
|
||||
}, {});
|
||||
|
||||
response.status(200).send(upsData);
|
||||
};
|
2
striker-ui-api/src/lib/request_handlers/ups/index.ts
Normal file
2
striker-ui-api/src/lib/request_handlers/ups/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './getUPS';
|
||||
export * from './getUPSTemplate';
|
@ -10,6 +10,7 @@ import jobRouter from './job';
|
||||
import networkInterfaceRouter from './network-interface';
|
||||
import serverRouter from './server';
|
||||
import sshKeyRouter from './ssh-key';
|
||||
import upsRouter from './ups';
|
||||
import userRouter from './user';
|
||||
|
||||
const routes: Readonly<Record<string, Router>> = {
|
||||
@ -23,6 +24,7 @@ const routes: Readonly<Record<string, Router>> = {
|
||||
'network-interface': networkInterfaceRouter,
|
||||
server: serverRouter,
|
||||
'ssh-key': sshKeyRouter,
|
||||
ups: upsRouter,
|
||||
user: userRouter,
|
||||
};
|
||||
|
||||
|
9
striker-ui-api/src/routes/ups.ts
Normal file
9
striker-ui-api/src/routes/ups.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import express from 'express';
|
||||
|
||||
import { getUPS, getUPSTemplate } from '../lib/request_handlers/ups';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', getUPS).get('/template', getUPSTemplate);
|
||||
|
||||
export default router;
|
17
striker-ui-api/src/types/APIUPS.d.ts
vendored
Normal file
17
striker-ui-api/src/types/APIUPS.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
type UPSOverview = {
|
||||
upsAgent: string;
|
||||
upsIPAddress: string;
|
||||
upsName: string;
|
||||
upsUUID: string;
|
||||
};
|
||||
|
||||
type UPSTemplate = {
|
||||
[upsName: string]: AnvilDataUPSHash[string] & {
|
||||
links: {
|
||||
[linkId: string]: {
|
||||
linkHref: string;
|
||||
linkLabel: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
interface AnvilDataStruct {
|
||||
[key: string]: AnvilDataStruct | boolean;
|
||||
}
|
10
striker-ui-api/src/types/DatabaseHash.d.ts
vendored
10
striker-ui-api/src/types/DatabaseHash.d.ts
vendored
@ -1,10 +0,0 @@
|
||||
type DatabaseHash = {
|
||||
[hostUUID: string]: {
|
||||
host: string;
|
||||
name: string;
|
||||
password: string;
|
||||
ping: string;
|
||||
port: string;
|
||||
user: string;
|
||||
};
|
||||
};
|
26
striker-ui-api/src/types/GetAnvilDataFunction.d.ts
vendored
Normal file
26
striker-ui-api/src/types/GetAnvilDataFunction.d.ts
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
interface AnvilDataStruct {
|
||||
[key: string]: AnvilDataStruct | boolean;
|
||||
}
|
||||
|
||||
type AnvilDataDatabaseHash = {
|
||||
[hostUUID: string]: {
|
||||
host: string;
|
||||
name: string;
|
||||
password: string;
|
||||
ping: string;
|
||||
port: string;
|
||||
user: string;
|
||||
};
|
||||
};
|
||||
|
||||
type AnvilDataUPSHash = {
|
||||
[upsName: string]: {
|
||||
agent: string;
|
||||
brand: string;
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
|
||||
type GetAnvilDataOptions = import('child_process').SpawnSyncOptions & {
|
||||
predata?: Array<[string, ...unknown[]]>;
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
type GetAnvilDataOptions = import('child_process').SpawnSyncOptions & {
|
||||
predata?: Array<[string, ...unknown[]]>;
|
||||
};
|
@ -49,6 +49,7 @@ const ConfirmDialog = forwardRef<
|
||||
onProceedAppend,
|
||||
onSubmitAppend,
|
||||
openInitially = false,
|
||||
preActionArea,
|
||||
proceedButtonProps = {},
|
||||
proceedColour: proceedColourKey = 'blue',
|
||||
scrollContent: isScrollContent = false,
|
||||
@ -234,6 +235,7 @@ const ConfirmDialog = forwardRef<
|
||||
<Box {...restScrollBoxProps} sx={combinedScrollBoxSx}>
|
||||
{contentElement}
|
||||
</Box>
|
||||
{preActionArea}
|
||||
{actionAreaElement}
|
||||
</FlexBox>
|
||||
</MUIDialog>
|
||||
|
34
striker-ui/components/FormDialog.tsx
Normal file
34
striker-ui/components/FormDialog.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { forwardRef, useMemo } from 'react';
|
||||
|
||||
import ConfirmDialog from './ConfirmDialog';
|
||||
|
||||
const FormDialog = forwardRef<
|
||||
ConfirmDialogForwardedRefContent,
|
||||
ConfirmDialogProps
|
||||
>((props, ref) => {
|
||||
const { scrollContent: isScrollContent } = props;
|
||||
|
||||
const scrollBoxPaddingRight = useMemo(
|
||||
() => (isScrollContent ? '.5em' : undefined),
|
||||
[isScrollContent],
|
||||
);
|
||||
|
||||
return (
|
||||
<ConfirmDialog
|
||||
dialogProps={{
|
||||
PaperProps: { sx: { minWidth: { xs: '90%', md: '50em' } } },
|
||||
}}
|
||||
formContent
|
||||
scrollBoxProps={{
|
||||
paddingRight: scrollBoxPaddingRight,
|
||||
paddingTop: '.3em',
|
||||
}}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
FormDialog.displayName = 'FormDialog';
|
||||
|
||||
export default FormDialog;
|
@ -5,7 +5,6 @@ import {
|
||||
forwardRef,
|
||||
ReactElement,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useState,
|
||||
@ -26,7 +25,7 @@ type InputWithRefOptionalPropsWithoutDefault<
|
||||
TypeName extends keyof MapToInputType,
|
||||
> = {
|
||||
inputTestBatch?: InputTestBatch;
|
||||
onFirstRender?: (args: { isRequired: boolean }) => void;
|
||||
onFirstRender?: InputFirstRenderFunction;
|
||||
valueKey?: CreateInputOnChangeHandlerOptions<TypeName>['valueKey'];
|
||||
};
|
||||
|
||||
@ -167,11 +166,15 @@ const InputWithRef = forwardRef(
|
||||
[initOnFocus, inputTestBatch],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isFirstRender) {
|
||||
onFirstRender?.call(null, { isRequired });
|
||||
}
|
||||
}, [isFirstRender, isRequired, onFirstRender]);
|
||||
if (isFirstRender) {
|
||||
const isValid =
|
||||
testInput?.call(null, {
|
||||
inputs: { [INPUT_TEST_ID]: { value: inputValue } },
|
||||
isIgnoreOnCallbacks: true,
|
||||
}) ?? false;
|
||||
|
||||
onFirstRender?.call(null, { isValid });
|
||||
}
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
|
@ -176,7 +176,7 @@ const List = forwardRef(
|
||||
const listEmptyElement = useMemo(
|
||||
() =>
|
||||
typeof listEmpty === 'string' ? (
|
||||
<BodyText>{listEmpty}</BodyText>
|
||||
<BodyText align="center">{listEmpty}</BodyText>
|
||||
) : (
|
||||
listEmpty
|
||||
),
|
||||
|
156
striker-ui/components/ManageUps/AddUpsInputGroup.tsx
Normal file
156
striker-ui/components/ManageUps/AddUpsInputGroup.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
import { ReactElement, ReactNode, useMemo, useState } from 'react';
|
||||
|
||||
import { BLACK } from '../../lib/consts/DEFAULT_THEME';
|
||||
|
||||
import CommonUpsInputGroup, {
|
||||
INPUT_ID_UPS_IP,
|
||||
INPUT_ID_UPS_NAME,
|
||||
} from './CommonUpsInputGroup';
|
||||
import FlexBox from '../FlexBox';
|
||||
import Link from '../Link';
|
||||
import SelectWithLabel from '../SelectWithLabel';
|
||||
import Spinner from '../Spinner';
|
||||
import { BodyText } from '../Text';
|
||||
import useIsFirstRender from '../../hooks/useIsFirstRender';
|
||||
|
||||
const INPUT_ID_UPS_TYPE = 'add-ups-select-ups-type-id';
|
||||
|
||||
const INPUT_LABEL_UPS_TYPE = 'UPS type';
|
||||
|
||||
const AddUpsInputGroup = <
|
||||
M extends {
|
||||
[K in
|
||||
| typeof INPUT_ID_UPS_IP
|
||||
| typeof INPUT_ID_UPS_NAME
|
||||
| typeof INPUT_ID_UPS_TYPE]: string;
|
||||
},
|
||||
>({
|
||||
formUtils,
|
||||
loading: isExternalLoading,
|
||||
previous = {},
|
||||
upsTemplate,
|
||||
}: AddUpsInputGroupProps<M>): ReactElement => {
|
||||
const { buildInputFirstRenderFunction, setValidity } = formUtils;
|
||||
|
||||
const { upsTypeId: previousUpsTypeId = '' } = previous;
|
||||
|
||||
const isFirstRender = useIsFirstRender();
|
||||
|
||||
const [inputUpsTypeIdValue, setInputUpsTypeIdValue] =
|
||||
useState<string>(previousUpsTypeId);
|
||||
|
||||
const upsTypeOptions = useMemo<SelectItem[]>(
|
||||
() =>
|
||||
upsTemplate
|
||||
? Object.entries(upsTemplate).map<SelectItem>(
|
||||
([
|
||||
upsTypeId,
|
||||
{
|
||||
brand,
|
||||
description,
|
||||
links: { 0: link },
|
||||
},
|
||||
]) => {
|
||||
let linkElement: ReactNode;
|
||||
|
||||
if (link) {
|
||||
const { linkHref, linkLabel } = link;
|
||||
|
||||
linkElement = (
|
||||
<Link
|
||||
href={linkHref}
|
||||
onClick={(event) => {
|
||||
// Don't trigger the (parent) item selection event.
|
||||
event.stopPropagation();
|
||||
}}
|
||||
sx={{ display: 'inline-flex', color: BLACK }}
|
||||
target="_blank"
|
||||
>
|
||||
{linkLabel}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
displayValue: (
|
||||
<FlexBox spacing={0}>
|
||||
<BodyText inverted>{brand}</BodyText>
|
||||
<BodyText inverted>
|
||||
{description} ({linkElement})
|
||||
</BodyText>
|
||||
</FlexBox>
|
||||
),
|
||||
value: upsTypeId,
|
||||
};
|
||||
},
|
||||
)
|
||||
: [],
|
||||
[upsTemplate],
|
||||
);
|
||||
|
||||
const pickUpsTypeElement = useMemo(
|
||||
() =>
|
||||
upsTemplate && (
|
||||
<SelectWithLabel
|
||||
formControlProps={{ sx: { marginTop: '.3em' } }}
|
||||
id={INPUT_ID_UPS_TYPE}
|
||||
label={INPUT_LABEL_UPS_TYPE}
|
||||
onChange={({ target: { value: rawNewValue } }) => {
|
||||
const newValue = String(rawNewValue);
|
||||
|
||||
setValidity(INPUT_ID_UPS_TYPE, true);
|
||||
setInputUpsTypeIdValue(newValue);
|
||||
}}
|
||||
required
|
||||
selectItems={upsTypeOptions}
|
||||
selectProps={{
|
||||
onClearIndicatorClick: () => {
|
||||
setValidity(INPUT_ID_UPS_TYPE, false);
|
||||
setInputUpsTypeIdValue('');
|
||||
},
|
||||
renderValue: (rawValue) => {
|
||||
const upsTypeId = String(rawValue);
|
||||
const { brand } = upsTemplate[upsTypeId];
|
||||
|
||||
return brand;
|
||||
},
|
||||
}}
|
||||
value={inputUpsTypeIdValue}
|
||||
/>
|
||||
),
|
||||
[upsTemplate, upsTypeOptions, inputUpsTypeIdValue, setValidity],
|
||||
);
|
||||
|
||||
const content = useMemo<ReactElement>(
|
||||
() =>
|
||||
isExternalLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<FlexBox>
|
||||
{pickUpsTypeElement}
|
||||
{inputUpsTypeIdValue && (
|
||||
<CommonUpsInputGroup formUtils={formUtils} previous={previous} />
|
||||
)}
|
||||
</FlexBox>
|
||||
),
|
||||
[
|
||||
formUtils,
|
||||
inputUpsTypeIdValue,
|
||||
isExternalLoading,
|
||||
pickUpsTypeElement,
|
||||
previous,
|
||||
],
|
||||
);
|
||||
|
||||
if (isFirstRender) {
|
||||
buildInputFirstRenderFunction(INPUT_ID_UPS_TYPE)({
|
||||
isValid: Boolean(inputUpsTypeIdValue),
|
||||
});
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export { INPUT_ID_UPS_TYPE, INPUT_LABEL_UPS_TYPE };
|
||||
|
||||
export default AddUpsInputGroup;
|
104
striker-ui/components/ManageUps/CommonUpsInputGroup.tsx
Normal file
104
striker-ui/components/ManageUps/CommonUpsInputGroup.tsx
Normal file
@ -0,0 +1,104 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
import Grid from '../Grid';
|
||||
import InputWithRef from '../InputWithRef';
|
||||
import OutlinedInputWithLabel from '../OutlinedInputWithLabel';
|
||||
import {
|
||||
buildIPAddressTestBatch,
|
||||
buildPeacefulStringTestBatch,
|
||||
} from '../../lib/test_input';
|
||||
|
||||
const INPUT_ID_UPS_IP = 'common-ups-input-ip-address';
|
||||
const INPUT_ID_UPS_NAME = 'common-ups-input-host-name';
|
||||
|
||||
const INPUT_LABEL_UPS_IP = 'IP address';
|
||||
const INPUT_LABEL_UPS_NAME = 'Host name';
|
||||
|
||||
const CommonUpsInputGroup = <
|
||||
M extends {
|
||||
[K in typeof INPUT_ID_UPS_IP | typeof INPUT_ID_UPS_NAME]: string;
|
||||
},
|
||||
>({
|
||||
formUtils: {
|
||||
buildFinishInputTestBatchFunction,
|
||||
buildInputFirstRenderFunction,
|
||||
msgSetters,
|
||||
},
|
||||
previous: { upsIPAddress: previousIpAddress, upsName: previousUpsName } = {},
|
||||
}: CommonUpsInputGroupProps<M>): ReactElement => (
|
||||
<Grid
|
||||
columns={{ xs: 1, sm: 2 }}
|
||||
layout={{
|
||||
'common-ups-input-cell-host-name': {
|
||||
children: (
|
||||
<InputWithRef
|
||||
input={
|
||||
<OutlinedInputWithLabel
|
||||
id={INPUT_ID_UPS_NAME}
|
||||
label={INPUT_LABEL_UPS_NAME}
|
||||
value={previousUpsName}
|
||||
/>
|
||||
}
|
||||
inputTestBatch={buildPeacefulStringTestBatch(
|
||||
INPUT_LABEL_UPS_NAME,
|
||||
() => {
|
||||
msgSetters[INPUT_ID_UPS_NAME]();
|
||||
},
|
||||
{
|
||||
onFinishBatch:
|
||||
buildFinishInputTestBatchFunction(INPUT_ID_UPS_NAME),
|
||||
},
|
||||
(message) => {
|
||||
msgSetters[INPUT_ID_UPS_NAME]({
|
||||
children: message,
|
||||
});
|
||||
},
|
||||
)}
|
||||
onFirstRender={buildInputFirstRenderFunction(INPUT_ID_UPS_NAME)}
|
||||
required
|
||||
/>
|
||||
),
|
||||
},
|
||||
'common-ups-input-cell-ip-address': {
|
||||
children: (
|
||||
<InputWithRef
|
||||
input={
|
||||
<OutlinedInputWithLabel
|
||||
id={INPUT_ID_UPS_IP}
|
||||
label={INPUT_LABEL_UPS_IP}
|
||||
value={previousIpAddress}
|
||||
/>
|
||||
}
|
||||
inputTestBatch={buildIPAddressTestBatch(
|
||||
INPUT_LABEL_UPS_IP,
|
||||
() => {
|
||||
msgSetters[INPUT_ID_UPS_IP]();
|
||||
},
|
||||
{
|
||||
onFinishBatch:
|
||||
buildFinishInputTestBatchFunction(INPUT_ID_UPS_IP),
|
||||
},
|
||||
(message) => {
|
||||
msgSetters[INPUT_ID_UPS_IP]({
|
||||
children: message,
|
||||
});
|
||||
},
|
||||
)}
|
||||
onFirstRender={buildInputFirstRenderFunction(INPUT_ID_UPS_IP)}
|
||||
required
|
||||
/>
|
||||
),
|
||||
},
|
||||
}}
|
||||
spacing="1em"
|
||||
/>
|
||||
);
|
||||
|
||||
export {
|
||||
INPUT_ID_UPS_IP,
|
||||
INPUT_ID_UPS_NAME,
|
||||
INPUT_LABEL_UPS_IP,
|
||||
INPUT_LABEL_UPS_NAME,
|
||||
};
|
||||
|
||||
export default CommonUpsInputGroup;
|
45
striker-ui/components/ManageUps/EditUpsInputGroup.tsx
Normal file
45
striker-ui/components/ManageUps/EditUpsInputGroup.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { ReactElement, useMemo } from 'react';
|
||||
|
||||
import AddUpsInputGroup, { INPUT_ID_UPS_TYPE } from './AddUpsInputGroup';
|
||||
import { INPUT_ID_UPS_IP, INPUT_ID_UPS_NAME } from './CommonUpsInputGroup';
|
||||
import Spinner from '../Spinner';
|
||||
|
||||
const INPUT_ID_UPS_UUID = 'edit-ups-input-ups-uuid';
|
||||
|
||||
const EditUpsInputGroup = <
|
||||
M extends {
|
||||
[K in
|
||||
| typeof INPUT_ID_UPS_IP
|
||||
| typeof INPUT_ID_UPS_NAME
|
||||
| typeof INPUT_ID_UPS_TYPE]: string;
|
||||
},
|
||||
>({
|
||||
formUtils,
|
||||
loading: isExternalLoading,
|
||||
previous,
|
||||
upsTemplate,
|
||||
upsUUID,
|
||||
}: EditUpsInputGroupProps<M>): ReactElement => {
|
||||
const content = useMemo<ReactElement>(
|
||||
() =>
|
||||
isExternalLoading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<>
|
||||
<AddUpsInputGroup
|
||||
formUtils={formUtils}
|
||||
previous={previous}
|
||||
upsTemplate={upsTemplate}
|
||||
/>
|
||||
<input hidden id={INPUT_ID_UPS_UUID} readOnly value={upsUUID} />
|
||||
</>
|
||||
),
|
||||
[formUtils, isExternalLoading, previous, upsTemplate, upsUUID],
|
||||
);
|
||||
|
||||
return content;
|
||||
};
|
||||
|
||||
export { INPUT_ID_UPS_UUID };
|
||||
|
||||
export default EditUpsInputGroup;
|
301
striker-ui/components/ManageUps/ManageUpsPanel.tsx
Normal file
301
striker-ui/components/ManageUps/ManageUpsPanel.tsx
Normal file
@ -0,0 +1,301 @@
|
||||
import {
|
||||
FC,
|
||||
FormEventHandler,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import API_BASE_URL from '../../lib/consts/API_BASE_URL';
|
||||
|
||||
import AddUpsInputGroup, { INPUT_ID_UPS_TYPE } from './AddUpsInputGroup';
|
||||
import api from '../../lib/api';
|
||||
import { INPUT_ID_UPS_IP, INPUT_ID_UPS_NAME } from './CommonUpsInputGroup';
|
||||
import ConfirmDialog from '../ConfirmDialog';
|
||||
import EditUpsInputGroup, { INPUT_ID_UPS_UUID } from './EditUpsInputGroup';
|
||||
import FlexBox from '../FlexBox';
|
||||
import FormDialog from '../FormDialog';
|
||||
import handleAPIError from '../../lib/handleAPIError';
|
||||
import List from '../List';
|
||||
import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup';
|
||||
import { Panel, PanelHeader } from '../Panels';
|
||||
import periodicFetch from '../../lib/fetchers/periodicFetch';
|
||||
import Spinner from '../Spinner';
|
||||
import { BodyText, HeaderText, InlineMonoText, MonoText } from '../Text';
|
||||
import useConfirmDialogProps from '../../hooks/useConfirmDialogProps';
|
||||
import useFormUtils from '../../hooks/useFormUtils';
|
||||
import useIsFirstRender from '../../hooks/useIsFirstRender';
|
||||
import useProtectedState from '../../hooks/useProtectedState';
|
||||
|
||||
type UpsFormData = {
|
||||
upsAgent: string;
|
||||
upsBrand: string;
|
||||
upsIPAddress: string;
|
||||
upsName: string;
|
||||
upsTypeId: string;
|
||||
upsUUID: string;
|
||||
};
|
||||
|
||||
const getUpsFormData = (
|
||||
upsTemplate: APIUpsTemplate,
|
||||
...[{ target }]: Parameters<FormEventHandler<HTMLDivElement>>
|
||||
): UpsFormData => {
|
||||
const { elements } = target as HTMLFormElement;
|
||||
|
||||
const { value: upsName } = elements.namedItem(
|
||||
INPUT_ID_UPS_NAME,
|
||||
) as HTMLInputElement;
|
||||
const { value: upsIPAddress } = elements.namedItem(
|
||||
INPUT_ID_UPS_IP,
|
||||
) as HTMLInputElement;
|
||||
|
||||
const inputUpsTypeId = elements.namedItem(INPUT_ID_UPS_TYPE);
|
||||
|
||||
let upsAgent = '';
|
||||
let upsBrand = '';
|
||||
let upsTypeId = '';
|
||||
|
||||
if (inputUpsTypeId) {
|
||||
({ value: upsTypeId } = inputUpsTypeId as HTMLInputElement);
|
||||
({ agent: upsAgent, brand: upsBrand } = upsTemplate[upsTypeId]);
|
||||
}
|
||||
|
||||
const inputUpsUUID = elements.namedItem(INPUT_ID_UPS_UUID);
|
||||
|
||||
let upsUUID = '';
|
||||
|
||||
if (inputUpsUUID) {
|
||||
({ value: upsUUID } = inputUpsUUID as HTMLInputElement);
|
||||
}
|
||||
|
||||
return { upsAgent, upsBrand, upsIPAddress, upsName, upsTypeId, upsUUID };
|
||||
};
|
||||
|
||||
const buildConfirmUpsFormData = ({
|
||||
upsBrand,
|
||||
upsIPAddress,
|
||||
upsName,
|
||||
upsUUID,
|
||||
}: UpsFormData) => {
|
||||
const listItems: Record<string, { label: string; value: string }> = {
|
||||
'ups-brand': { label: 'Brand', value: upsBrand },
|
||||
'ups-name': { label: 'Host name', value: upsName },
|
||||
'ups-ip-address': { label: 'IP address', value: upsIPAddress },
|
||||
};
|
||||
|
||||
return (
|
||||
<List
|
||||
listItems={listItems}
|
||||
listItemProps={{ sx: { padding: 0 } }}
|
||||
renderListItem={(part, { label, value }) => (
|
||||
<FlexBox fullWidth growFirst key={`confirm-ups-${upsUUID}-${part}`} row>
|
||||
<BodyText>{label}</BodyText>
|
||||
<MonoText>{value}</MonoText>
|
||||
</FlexBox>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ManageUpsPanel: FC = () => {
|
||||
const isFirstRender = useIsFirstRender();
|
||||
|
||||
const confirmDialogRef = useRef<ConfirmDialogForwardedRefContent>({});
|
||||
const formDialogRef = useRef<ConfirmDialogForwardedRefContent>({});
|
||||
const messageGroupRef = useRef<MessageGroupForwardedRefContent>({});
|
||||
|
||||
const [confirmDialogProps, setConfirmDialogProps] = useConfirmDialogProps();
|
||||
const [formDialogProps, setFormDialogProps] = useConfirmDialogProps();
|
||||
const [isEditUpses, setIsEditUpses] = useState<boolean>(false);
|
||||
const [isLoadingUpsTemplate, setIsLoadingUpsTemplate] =
|
||||
useProtectedState<boolean>(true);
|
||||
const [upsTemplate, setUpsTemplate] = useProtectedState<
|
||||
APIUpsTemplate | undefined
|
||||
>(undefined);
|
||||
|
||||
const { data: upsOverviews, isLoading: isUpsOverviewLoading } =
|
||||
periodicFetch<APIUpsOverview>(`${API_BASE_URL}/ups`, {
|
||||
refreshInterval: 60000,
|
||||
});
|
||||
|
||||
const formUtils = useFormUtils(
|
||||
[INPUT_ID_UPS_IP, INPUT_ID_UPS_NAME, INPUT_ID_UPS_TYPE],
|
||||
messageGroupRef,
|
||||
);
|
||||
const { isFormInvalid } = formUtils;
|
||||
|
||||
const buildEditUpsFormDialogProps = useCallback<
|
||||
(args: APIUpsOverview[string]) => ConfirmDialogProps
|
||||
>(
|
||||
({ upsAgent, upsIPAddress, upsName, upsUUID }) => {
|
||||
// Determine the type of existing UPS based on its scan agent.
|
||||
// TODO: should identity an existing UPS's type in the DB.
|
||||
const upsTypeId: string =
|
||||
Object.entries(upsTemplate ?? {}).find(
|
||||
([, { agent }]) => upsAgent === agent,
|
||||
)?.[0] ?? '';
|
||||
|
||||
return {
|
||||
actionProceedText: 'Update',
|
||||
content: (
|
||||
<EditUpsInputGroup
|
||||
formUtils={formUtils}
|
||||
previous={{
|
||||
upsIPAddress,
|
||||
upsName,
|
||||
upsTypeId,
|
||||
}}
|
||||
upsTemplate={upsTemplate}
|
||||
upsUUID={upsUUID}
|
||||
/>
|
||||
),
|
||||
onSubmitAppend: (event) => {
|
||||
if (!upsTemplate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const editData = getUpsFormData(upsTemplate, event);
|
||||
const { upsName: newUpsName } = editData;
|
||||
|
||||
setConfirmDialogProps({
|
||||
actionProceedText: 'Update',
|
||||
content: buildConfirmUpsFormData(editData),
|
||||
titleText: (
|
||||
<HeaderText>
|
||||
Update{' '}
|
||||
<InlineMonoText fontSize="inherit">{newUpsName}</InlineMonoText>{' '}
|
||||
with the following data?
|
||||
</HeaderText>
|
||||
),
|
||||
});
|
||||
|
||||
confirmDialogRef.current.setOpen?.call(null, true);
|
||||
},
|
||||
titleText: (
|
||||
<HeaderText>
|
||||
Update UPS{' '}
|
||||
<InlineMonoText fontSize="inherit">{upsName}</InlineMonoText>
|
||||
</HeaderText>
|
||||
),
|
||||
};
|
||||
},
|
||||
[formUtils, setConfirmDialogProps, upsTemplate],
|
||||
);
|
||||
|
||||
const addUpsFormDialogProps = useMemo<ConfirmDialogProps>(
|
||||
() => ({
|
||||
actionProceedText: 'Add',
|
||||
content: (
|
||||
<AddUpsInputGroup formUtils={formUtils} upsTemplate={upsTemplate} />
|
||||
),
|
||||
onSubmitAppend: (event) => {
|
||||
if (!upsTemplate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const addData = getUpsFormData(upsTemplate, event);
|
||||
const { upsBrand } = addData;
|
||||
|
||||
setConfirmDialogProps({
|
||||
actionProceedText: 'Add',
|
||||
content: buildConfirmUpsFormData(addData),
|
||||
titleText: (
|
||||
<HeaderText>
|
||||
Add a{' '}
|
||||
<InlineMonoText fontSize="inherit">{upsBrand}</InlineMonoText> UPS
|
||||
with the following data?
|
||||
</HeaderText>
|
||||
),
|
||||
});
|
||||
|
||||
confirmDialogRef.current.setOpen?.call(null, true);
|
||||
},
|
||||
titleText: 'Add a UPS',
|
||||
}),
|
||||
[formUtils, setConfirmDialogProps, upsTemplate],
|
||||
);
|
||||
|
||||
const listElement = useMemo(
|
||||
() => (
|
||||
<List
|
||||
allowEdit
|
||||
allowItemButton={isEditUpses}
|
||||
edit={isEditUpses}
|
||||
header
|
||||
listEmpty="No Ups(es) registered."
|
||||
listItems={upsOverviews}
|
||||
onAdd={() => {
|
||||
setFormDialogProps(addUpsFormDialogProps);
|
||||
formDialogRef.current.setOpen?.call(null, true);
|
||||
}}
|
||||
onEdit={() => {
|
||||
setIsEditUpses((previous) => !previous);
|
||||
}}
|
||||
onItemClick={(value) => {
|
||||
setFormDialogProps(buildEditUpsFormDialogProps(value));
|
||||
formDialogRef.current.setOpen?.call(null, true);
|
||||
}}
|
||||
renderListItem={(upsUUID, { upsAgent, upsIPAddress, upsName }) => (
|
||||
<FlexBox fullWidth row>
|
||||
<BodyText>{upsName}</BodyText>
|
||||
<BodyText>agent="{upsAgent}"</BodyText>
|
||||
<BodyText>ip="{upsIPAddress}"</BodyText>
|
||||
</FlexBox>
|
||||
)}
|
||||
/>
|
||||
),
|
||||
[
|
||||
addUpsFormDialogProps,
|
||||
buildEditUpsFormDialogProps,
|
||||
isEditUpses,
|
||||
setFormDialogProps,
|
||||
upsOverviews,
|
||||
],
|
||||
);
|
||||
const panelContent = useMemo(
|
||||
() =>
|
||||
isLoadingUpsTemplate || isUpsOverviewLoading ? <Spinner /> : listElement,
|
||||
[isLoadingUpsTemplate, isUpsOverviewLoading, listElement],
|
||||
);
|
||||
|
||||
if (isFirstRender) {
|
||||
api
|
||||
.get<APIUpsTemplate>('/ups/template')
|
||||
.then(({ data }) => {
|
||||
setUpsTemplate(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
handleAPIError(error);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoadingUpsTemplate(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel>
|
||||
<PanelHeader>
|
||||
<HeaderText>Manage UPSes</HeaderText>
|
||||
</PanelHeader>
|
||||
{panelContent}
|
||||
</Panel>
|
||||
<FormDialog
|
||||
{...formDialogProps}
|
||||
ref={formDialogRef}
|
||||
preActionArea={
|
||||
<MessageGroup
|
||||
count={1}
|
||||
defaultMessageType="warning"
|
||||
ref={messageGroupRef}
|
||||
/>
|
||||
}
|
||||
proceedButtonProps={{ disabled: isFormInvalid }}
|
||||
/>
|
||||
<ConfirmDialog {...confirmDialogProps} ref={confirmDialogRef} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageUpsPanel;
|
3
striker-ui/components/ManageUps/index.tsx
Normal file
3
striker-ui/components/ManageUps/index.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
import ManageUpsPanel from './ManageUpsPanel';
|
||||
|
||||
export default ManageUpsPanel;
|
@ -23,7 +23,10 @@ const SelectWithLabel: FC<SelectWithLabelProps> = ({
|
||||
isReadOnly = false,
|
||||
messageBoxProps = {},
|
||||
name,
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
required: isRequired,
|
||||
selectProps: {
|
||||
multiple: selectMultiple,
|
||||
sx: selectSx,
|
||||
@ -71,15 +74,24 @@ const SelectWithLabel: FC<SelectWithLabelProps> = ({
|
||||
[createCheckbox, disableItem, hideItem, id],
|
||||
);
|
||||
|
||||
const inputElement = useMemo(() => <OutlinedInput label={label} />, [label]);
|
||||
const selectId = useMemo(() => `${id}-select-element`, [id]);
|
||||
|
||||
const inputElement = useMemo(
|
||||
() => <OutlinedInput id={id} label={label} />,
|
||||
[id, label],
|
||||
);
|
||||
const labelElement = useMemo(
|
||||
() =>
|
||||
label && (
|
||||
<OutlinedInputLabel htmlFor={id} {...inputLabelProps}>
|
||||
<OutlinedInputLabel
|
||||
htmlFor={selectId}
|
||||
isNotifyRequired={isRequired}
|
||||
{...inputLabelProps}
|
||||
>
|
||||
{label}
|
||||
</OutlinedInputLabel>
|
||||
),
|
||||
[id, inputLabelProps, label],
|
||||
[inputLabelProps, isRequired, label, selectId],
|
||||
);
|
||||
const menuItemElements = useMemo(
|
||||
() =>
|
||||
@ -96,11 +108,13 @@ const SelectWithLabel: FC<SelectWithLabelProps> = ({
|
||||
<MUIFormControl fullWidth {...formControlProps}>
|
||||
{labelElement}
|
||||
<Select
|
||||
id={id}
|
||||
id={selectId}
|
||||
input={inputElement}
|
||||
multiple={selectMultiple}
|
||||
name={name}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
onFocus={onFocus}
|
||||
readOnly={isReadOnly}
|
||||
value={selectValue}
|
||||
{...restSelectProps}
|
||||
|
@ -61,8 +61,8 @@ const AddPeerDialog = forwardRef<
|
||||
|
||||
const buildInputFirstRenderFunction = useCallback(
|
||||
(key: string) =>
|
||||
({ isRequired }: { isRequired: boolean }) => {
|
||||
setFormValidity(buildObjectStateSetterCallback(key, !isRequired));
|
||||
({ isValid }: InputFirstRenderFunctionArgs) => {
|
||||
setFormValidity(buildObjectStateSetterCallback(key, isValid));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
19
striker-ui/hooks/useConfirmDialogProps.ts
Normal file
19
striker-ui/hooks/useConfirmDialogProps.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
|
||||
const useConfirmDialogProps = ({
|
||||
actionProceedText = '',
|
||||
content = '',
|
||||
titleText = '',
|
||||
...restProps
|
||||
}: Partial<ConfirmDialogProps> = {}): [
|
||||
ConfirmDialogProps,
|
||||
Dispatch<SetStateAction<ConfirmDialogProps>>,
|
||||
] =>
|
||||
useState<ConfirmDialogProps>({
|
||||
actionProceedText,
|
||||
content,
|
||||
titleText,
|
||||
...restProps,
|
||||
});
|
||||
|
||||
export default useConfirmDialogProps;
|
59
striker-ui/hooks/useFormUtils.ts
Normal file
59
striker-ui/hooks/useFormUtils.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { MutableRefObject, useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import buildMapToMessageSetter from '../lib/buildMapToMessageSetter';
|
||||
import buildObjectStateSetterCallback from '../lib/buildObjectStateSetterCallback';
|
||||
import { MessageGroupForwardedRefContent } from '../components/MessageGroup';
|
||||
|
||||
const useFormUtils = <
|
||||
U extends string,
|
||||
I extends InputIds<U>,
|
||||
M extends MapToInputId<U, I>,
|
||||
>(
|
||||
ids: I,
|
||||
messageGroupRef: MutableRefObject<MessageGroupForwardedRefContent>,
|
||||
): FormUtils<M> => {
|
||||
const [formValidity, setFormValidity] = useState<FormValidity<M>>({});
|
||||
|
||||
const setValidity = useCallback((key: keyof M, value: boolean) => {
|
||||
setFormValidity(
|
||||
buildObjectStateSetterCallback<FormValidity<M>>(key, value),
|
||||
);
|
||||
}, []);
|
||||
|
||||
const buildFinishInputTestBatchFunction = useCallback(
|
||||
(key: keyof M) => (result: boolean) => {
|
||||
setValidity(key, result);
|
||||
},
|
||||
[setValidity],
|
||||
);
|
||||
|
||||
const buildInputFirstRenderFunction = useCallback(
|
||||
(key: keyof M) =>
|
||||
({ isValid }: InputFirstRenderFunctionArgs) => {
|
||||
setValidity(key, isValid);
|
||||
},
|
||||
[setValidity],
|
||||
);
|
||||
|
||||
const isFormInvalid = useMemo(
|
||||
() => Object.values(formValidity).some((isInputValid) => !isInputValid),
|
||||
[formValidity],
|
||||
);
|
||||
|
||||
const msgSetters = useMemo(
|
||||
() => buildMapToMessageSetter<U, I, M>(ids, messageGroupRef),
|
||||
[ids, messageGroupRef],
|
||||
);
|
||||
|
||||
return {
|
||||
buildFinishInputTestBatchFunction,
|
||||
buildInputFirstRenderFunction,
|
||||
formValidity,
|
||||
isFormInvalid,
|
||||
msgSetters,
|
||||
setFormValidity,
|
||||
setValidity,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFormUtils;
|
@ -2,25 +2,49 @@ import { MutableRefObject } from 'react';
|
||||
|
||||
import { MessageGroupForwardedRefContent } from '../components/MessageGroup';
|
||||
|
||||
type BuildMapToMessageSetterReturnType<T extends MapToInputTestID> = {
|
||||
[MessageSetterID in keyof T]: MessageSetterFunction;
|
||||
const buildMessageSetter = <T extends MapToInputTestID>(
|
||||
id: string,
|
||||
messageGroupRef: MutableRefObject<MessageGroupForwardedRefContent>,
|
||||
container?: MapToMessageSetter<T>,
|
||||
key: string = id,
|
||||
): MessageSetterFunction => {
|
||||
const setter: MessageSetterFunction = (message?) => {
|
||||
messageGroupRef.current.setMessage?.call(null, id, message);
|
||||
};
|
||||
|
||||
if (container) {
|
||||
container[key as keyof T] = setter;
|
||||
}
|
||||
|
||||
return setter;
|
||||
};
|
||||
|
||||
const buildMapToMessageSetter = <T extends MapToInputTestID>(
|
||||
mapToID: T,
|
||||
const buildMapToMessageSetter = <
|
||||
U extends string,
|
||||
I extends InputIds<U>,
|
||||
M extends MapToInputId<U, I>,
|
||||
>(
|
||||
ids: I,
|
||||
messageGroupRef: MutableRefObject<MessageGroupForwardedRefContent>,
|
||||
): BuildMapToMessageSetterReturnType<T> =>
|
||||
Object.entries(mapToID).reduce<BuildMapToMessageSetterReturnType<T>>(
|
||||
(previous, [key, id]) => {
|
||||
const setter: MessageSetterFunction = (message?) => {
|
||||
messageGroupRef.current.setMessage?.call(null, id, message);
|
||||
};
|
||||
|
||||
previous[key as keyof T] = setter;
|
||||
): MapToMessageSetter<M> => {
|
||||
let result: MapToMessageSetter<M> = {} as MapToMessageSetter<M>;
|
||||
|
||||
if (ids instanceof Array) {
|
||||
result = ids.reduce<MapToMessageSetter<M>>((previous, id) => {
|
||||
buildMessageSetter(id, messageGroupRef, previous);
|
||||
return previous;
|
||||
},
|
||||
{} as BuildMapToMessageSetterReturnType<T>,
|
||||
);
|
||||
}, result);
|
||||
} else {
|
||||
result = Object.entries(ids).reduce<MapToMessageSetter<M>>(
|
||||
(previous, [key, id]) => {
|
||||
buildMessageSetter(id, messageGroupRef, previous, key);
|
||||
return previous;
|
||||
},
|
||||
result,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export default buildMapToMessageSetter;
|
||||
|
@ -6,10 +6,11 @@ import { InlineMonoText } from '../../components/Text';
|
||||
const buildDomainTestBatch: BuildInputTestBatchFunction = (
|
||||
inputName,
|
||||
onSuccess,
|
||||
{ onFinishBatch, ...defaults } = {},
|
||||
{ isRequired, onFinishBatch, ...defaults } = {},
|
||||
onDomainTestFailure,
|
||||
) => ({
|
||||
defaults: { ...defaults, onSuccess },
|
||||
isRequired,
|
||||
onFinishBatch,
|
||||
tests: [
|
||||
{
|
||||
|
@ -5,10 +5,11 @@ import testNotBlank from './testNotBlank';
|
||||
const buildIPAddressTestBatch: BuildInputTestBatchFunction = (
|
||||
inputName,
|
||||
onSuccess,
|
||||
{ onFinishBatch, ...defaults } = {},
|
||||
{ isRequired, onFinishBatch, ...defaults } = {},
|
||||
onIPv4TestFailure,
|
||||
) => ({
|
||||
defaults: { ...defaults, onSuccess },
|
||||
isRequired,
|
||||
onFinishBatch,
|
||||
tests: [
|
||||
{
|
||||
|
@ -4,7 +4,7 @@ import toNumber from '../toNumber';
|
||||
const buildNumberTestBatch: BuildInputTestBatchFunction = (
|
||||
inputName,
|
||||
onSuccess,
|
||||
{ onFinishBatch, ...defaults } = {},
|
||||
{ isRequired, onFinishBatch, ...defaults } = {},
|
||||
onIntTestFailure?,
|
||||
onFloatTestFailure?,
|
||||
onRangeTestFailure?,
|
||||
@ -48,6 +48,7 @@ const buildNumberTestBatch: BuildInputTestBatchFunction = (
|
||||
|
||||
return {
|
||||
defaults: { ...defaults, onSuccess },
|
||||
isRequired,
|
||||
onFinishBatch,
|
||||
tests,
|
||||
};
|
||||
|
@ -6,10 +6,11 @@ import { InlineMonoText } from '../../components/Text';
|
||||
const buildPeacefulStringTestBatch: BuildInputTestBatchFunction = (
|
||||
inputName,
|
||||
onSuccess,
|
||||
{ onFinishBatch, ...defaults } = {},
|
||||
{ isRequired, onFinishBatch, ...defaults } = {},
|
||||
onTestPeacefulStringFailureAppend,
|
||||
) => ({
|
||||
defaults: { ...defaults, onSuccess },
|
||||
isRequired,
|
||||
onFinishBatch,
|
||||
tests: [
|
||||
{
|
||||
|
@ -5,10 +5,11 @@ import testNotBlank from './testNotBlank';
|
||||
const buildUUIDTestBatch: BuildInputTestBatchFunction = (
|
||||
inputName,
|
||||
onSuccess,
|
||||
{ onFinishBatch, ...defaults } = {},
|
||||
{ isRequired, onFinishBatch, ...defaults } = {},
|
||||
onUUIDTestFailure,
|
||||
) => ({
|
||||
defaults: { ...defaults, onSuccess },
|
||||
isRequired,
|
||||
onFinishBatch,
|
||||
tests: [
|
||||
{
|
||||
|
@ -8,6 +8,7 @@ import Grid from '../../components/Grid';
|
||||
import handleAPIError from '../../lib/handleAPIError';
|
||||
import Header from '../../components/Header';
|
||||
import ManageFencePanel from '../../components/ManageFence';
|
||||
import ManageUpsPanel from '../../components/ManageUps';
|
||||
import { Panel } from '../../components/Panels';
|
||||
import PrepareHostForm from '../../components/PrepareHostForm';
|
||||
import PrepareNetworkForm from '../../components/PrepareNetworkForm';
|
||||
@ -130,6 +131,19 @@ const ManageFenceTabContent: FC = () => (
|
||||
/>
|
||||
);
|
||||
|
||||
const ManageUpsTabContent: FC = () => (
|
||||
<Grid
|
||||
columns={STEP_CONTENT_GRID_COLUMNS}
|
||||
layout={{
|
||||
'manageups-left-column': {},
|
||||
'manageups-center-column': {
|
||||
children: <ManageUpsPanel />,
|
||||
...STEP_CONTENT_GRID_CENTER_COLUMN,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const ManageElement: FC = () => {
|
||||
const {
|
||||
isReady,
|
||||
@ -177,6 +191,7 @@ const ManageElement: FC = () => {
|
||||
<Tab label="Prepare host" value="prepare-host" />
|
||||
<Tab label="Prepare network" value="prepare-network" />
|
||||
<Tab label="Manage fence devices" value="manage-fence" />
|
||||
<Tab label="Manage UPSes" value="manage-ups" />
|
||||
</Tabs>
|
||||
</Panel>
|
||||
<TabContent changingTabId={pageTabId} tabId="prepare-host">
|
||||
@ -188,6 +203,9 @@ const ManageElement: FC = () => {
|
||||
<TabContent changingTabId={pageTabId} tabId="manage-fence">
|
||||
<ManageFenceTabContent />
|
||||
</TabContent>
|
||||
<TabContent changingTabId={pageTabId} tabId="manage-ups">
|
||||
<ManageUpsTabContent />
|
||||
</TabContent>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
22
striker-ui/types/APIUps.d.ts
vendored
Normal file
22
striker-ui/types/APIUps.d.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
type APIUpsTemplate = {
|
||||
[upsTypeId: string]: {
|
||||
agent: string;
|
||||
brand: string;
|
||||
description: string;
|
||||
links: {
|
||||
[linkId: string]: {
|
||||
linkHref: string;
|
||||
linkLabel: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type APIUpsOverview = {
|
||||
[upsUUID: string]: {
|
||||
upsAgent: string;
|
||||
upsIPAddress: string;
|
||||
upsName: string;
|
||||
upsUUID: string;
|
||||
};
|
||||
};
|
11
striker-ui/types/AddUpsInputGroup.d.ts
vendored
Normal file
11
striker-ui/types/AddUpsInputGroup.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
type AddUpsInputGroupOptionalProps = {
|
||||
loading?: boolean;
|
||||
previous?: CommonUpsInputGroupOptionalProps['previous'] & {
|
||||
upsTypeId?: string;
|
||||
};
|
||||
upsTemplate?: APIUpsTemplate;
|
||||
};
|
||||
|
||||
type AddUpsInputGroupProps<M extends MapToInputTestID> =
|
||||
AddUpsInputGroupOptionalProps &
|
||||
Pick<CommonUpsInputGroupProps<M>, 'formUtils'>;
|
11
striker-ui/types/CommonUpsInputGroup.d.ts
vendored
Normal file
11
striker-ui/types/CommonUpsInputGroup.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
type CommonUpsInputGroupOptionalProps = {
|
||||
previous?: {
|
||||
upsIPAddress?: string;
|
||||
upsName?: string;
|
||||
};
|
||||
};
|
||||
|
||||
type CommonUpsInputGroupProps<M extends MapToInputTestID> =
|
||||
CommonUpsInputGroupOptionalProps & {
|
||||
formUtils: FormUtils<M>;
|
||||
};
|
1
striker-ui/types/ConfirmDialog.d.ts
vendored
1
striker-ui/types/ConfirmDialog.d.ts
vendored
@ -10,6 +10,7 @@ type ConfirmDialogOptionalProps = {
|
||||
onCancelAppend?: ContainedButtonProps['onClick'];
|
||||
onSubmitAppend?: import('react').FormEventHandler<HTMLDivElement>;
|
||||
openInitially?: boolean;
|
||||
preActionArea?: import('react').ReactNode;
|
||||
proceedButtonProps?: ContainedButtonProps;
|
||||
proceedColour?: 'blue' | 'red';
|
||||
scrollContent?: boolean;
|
||||
|
9
striker-ui/types/EditUpsInputGroup.d.ts
vendored
Normal file
9
striker-ui/types/EditUpsInputGroup.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
type EditUpsInputGroupOptionalProps = {
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
type EditUpsInputGroupProps<M extends MapToInputTestID> =
|
||||
EditUpsInputGroupOptionalProps &
|
||||
Pick<AddUpsInputGroupProps<M>, 'formUtils' | 'previous' | 'upsTemplate'> & {
|
||||
upsUUID: string;
|
||||
};
|
27
striker-ui/types/FormUtils.d.ts
vendored
Normal file
27
striker-ui/types/FormUtils.d.ts
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
type FormValidity<T> = {
|
||||
[K in keyof T]?: boolean;
|
||||
};
|
||||
|
||||
type InputTestBatchFinishCallbackBuilder<M extends MapToInputTestID> = (
|
||||
key: keyof M,
|
||||
) => InputTestBatchFinishCallback;
|
||||
|
||||
type InputFirstRenderFunctionArgs = { isValid: boolean };
|
||||
|
||||
type InputFirstRenderFunction = (args: InputFirstRenderFunctionArgs) => void;
|
||||
|
||||
type InputFirstRenderFunctionBuilder<M extends MapToInputTestID> = (
|
||||
key: keyof M,
|
||||
) => InputFirstRenderFunction;
|
||||
|
||||
type FormUtils<M extends MapToInputTestID> = {
|
||||
buildFinishInputTestBatchFunction: InputTestBatchFinishCallbackBuilder<M>;
|
||||
buildInputFirstRenderFunction: InputFirstRenderFunctionBuilder<M>;
|
||||
formValidity: FormValidity<M>;
|
||||
isFormInvalid: boolean;
|
||||
msgSetters: MapToMessageSetter<M>;
|
||||
setFormValidity: import('react').Dispatch<
|
||||
import('react').SetStateAction<FormValidity<M>>
|
||||
>;
|
||||
setValidity: (key: keyof M, value: boolean) => void;
|
||||
};
|
16
striker-ui/types/MapToMessageSetter.d.ts
vendored
Normal file
16
striker-ui/types/MapToMessageSetter.d.ts
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
type MapToMessageSetter<T extends MapToInputTestID> = {
|
||||
[MessageSetterID in keyof T]: MessageSetterFunction;
|
||||
};
|
||||
|
||||
type InputIds<T> = ReadonlyArray<T> | MapToInputTestID;
|
||||
|
||||
/**
|
||||
* Given either:
|
||||
* 1. an array of input identifiers, or
|
||||
* 2. a key-value object of input indentifiers,
|
||||
* transform it into a key-value object of identifiers.
|
||||
*/
|
||||
type MapToInputId<
|
||||
U extends string,
|
||||
I extends InputIds<U>,
|
||||
> = I extends ReadonlyArray<U> ? { [K in I[number]]: K } : I;
|
3
striker-ui/types/SelectWithLabel.d.ts
vendored
3
striker-ui/types/SelectWithLabel.d.ts
vendored
@ -20,11 +20,12 @@ type SelectWithLabelOptionalProps = {
|
||||
>;
|
||||
label?: string;
|
||||
messageBoxProps?: Partial<import('../components/MessageBox').MessageBoxProps>;
|
||||
required?: boolean;
|
||||
selectProps?: Partial<SelectProps>;
|
||||
};
|
||||
|
||||
type SelectWithLabelProps = SelectWithLabelOptionalProps &
|
||||
Pick<SelectProps, 'name' | 'onChange' | 'value'> & {
|
||||
Pick<SelectProps, 'name' | 'onBlur' | 'onChange' | 'onFocus' | 'value'> & {
|
||||
id: string;
|
||||
selectItems: Array<SelectItem | string>;
|
||||
};
|
||||
|
3
striker-ui/types/TestInputFunction.d.ts
vendored
3
striker-ui/types/TestInputFunction.d.ts
vendored
@ -65,7 +65,8 @@ type InputTestBatch = {
|
||||
type BuildInputTestBatchFunction = (
|
||||
inputName: string,
|
||||
onSuccess: InputTestSuccessCallback,
|
||||
options?: InputTestBatch['defaults'] & Pick<InputTestBatch, 'onFinishBatch'>,
|
||||
options?: InputTestBatch['defaults'] &
|
||||
Pick<InputTestBatch, 'isRequired' | 'onFinishBatch'>,
|
||||
...onFailureAppends: InputTestFailureAppendCallback[]
|
||||
) => InputTestBatch;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user