commit
3701517b41
43 changed files with 1076 additions and 59 deletions
@ -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); |
||||||
|
}; |
@ -0,0 +1,2 @@ |
|||||||
|
export * from './getUPS'; |
||||||
|
export * from './getUPSTemplate'; |
@ -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; |
@ -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; |
|
||||||
} |
|
@ -1,10 +0,0 @@ |
|||||||
type DatabaseHash = { |
|
||||||
[hostUUID: string]: { |
|
||||||
host: string; |
|
||||||
name: string; |
|
||||||
password: string; |
|
||||||
ping: string; |
|
||||||
port: string; |
|
||||||
user: string; |
|
||||||
}; |
|
||||||
}; |
|
@ -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[]]>; |
|
||||||
}; |
|
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -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; |
@ -0,0 +1,3 @@ |
|||||||
|
import ManageUpsPanel from './ManageUpsPanel'; |
||||||
|
|
||||||
|
export default ManageUpsPanel; |
@ -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; |
@ -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; |
@ -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; |
||||||
|
}; |
||||||
|
}; |
@ -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'>; |
@ -0,0 +1,11 @@ |
|||||||
|
type CommonUpsInputGroupOptionalProps = { |
||||||
|
previous?: { |
||||||
|
upsIPAddress?: string; |
||||||
|
upsName?: string; |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
type CommonUpsInputGroupProps<M extends MapToInputTestID> = |
||||||
|
CommonUpsInputGroupOptionalProps & { |
||||||
|
formUtils: FormUtils<M>; |
||||||
|
}; |
@ -0,0 +1,9 @@ |
|||||||
|
type EditUpsInputGroupOptionalProps = { |
||||||
|
loading?: boolean; |
||||||
|
}; |
||||||
|
|
||||||
|
type EditUpsInputGroupProps<M extends MapToInputTestID> = |
||||||
|
EditUpsInputGroupOptionalProps & |
||||||
|
Pick<AddUpsInputGroupProps<M>, 'formUtils' | 'previous' | 'upsTemplate'> & { |
||||||
|
upsUUID: string; |
||||||
|
}; |
@ -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; |
||||||
|
}; |
@ -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; |
Loading…
Reference in new issue