From 47a84a86e8e95a0c79b4176ee9b16cb81ee93a8c Mon Sep 17 00:00:00 2001 From: Tsu-ba-me Date: Mon, 5 Feb 2024 19:38:46 -0500 Subject: [PATCH] feat(striker-ui): add manage host components --- .../components/ManageHost/ManageHost.tsx | 47 ++++ .../components/ManageHost/PrepareHostForm.tsx | 228 ++++++++++++++++++ .../components/ManageHost/TestAccessForm.tsx | 145 +++++++++++ striker-ui/components/ManageHost/index.tsx | 4 + striker-ui/components/ManageHost/schema.ts | 35 +++ .../components/ManageHost/testAccessSchema.ts | 15 ++ striker-ui/types/ManageHost.d.ts | 38 +++ 7 files changed, 512 insertions(+) create mode 100644 striker-ui/components/ManageHost/ManageHost.tsx create mode 100644 striker-ui/components/ManageHost/PrepareHostForm.tsx create mode 100644 striker-ui/components/ManageHost/TestAccessForm.tsx create mode 100644 striker-ui/components/ManageHost/index.tsx create mode 100644 striker-ui/components/ManageHost/schema.ts create mode 100644 striker-ui/components/ManageHost/testAccessSchema.ts create mode 100644 striker-ui/types/ManageHost.d.ts diff --git a/striker-ui/components/ManageHost/ManageHost.tsx b/striker-ui/components/ManageHost/ManageHost.tsx new file mode 100644 index 00000000..cd99c4be --- /dev/null +++ b/striker-ui/components/ManageHost/ManageHost.tsx @@ -0,0 +1,47 @@ +import { FC, useState } from 'react'; + +import CrudList from '../CrudList'; +import PrepareHostForm from './PrepareHostForm'; +import TestAccessForm from './TestAccessForm'; +import { BodyText } from '../Text'; + +const ManageHost: FC = () => { + const [inquireHostResponse, setInquireHostResponse] = useState< + InquireHostResponse | undefined + >(); + + return ( + + addHeader="Initialize host" + editHeader="" + entriesUrl="/host" + getDeleteErrorMessage={(children, ...rest) => ({ + ...rest, + children: <>Failed to delete host(s). {children}, + })} + getDeleteHeader={(count) => `Delete the following ${count} host(s)?`} + getDeleteSuccessMessage={() => ({ + children: <>Successfully deleted host(s), + })} + listEmpty="No host(s) found" + listProps={{ allowAddItem: true, allowEdit: false }} + renderAddForm={(tools) => ( + <> + + {inquireHostResponse && ( + + )} + + )} + renderDeleteItem={(hosts, { key }) => { + const host = hosts?.[key]; + + return {host?.shortHostName}; + }} + renderEditForm={() => <>} + renderListItem={(uuid, { hostName }) => {hostName}} + /> + ); +}; + +export default ManageHost; diff --git a/striker-ui/components/ManageHost/PrepareHostForm.tsx b/striker-ui/components/ManageHost/PrepareHostForm.tsx new file mode 100644 index 00000000..016fd46b --- /dev/null +++ b/striker-ui/components/ManageHost/PrepareHostForm.tsx @@ -0,0 +1,228 @@ +import { Grid } from '@mui/material'; +import { FC, useMemo } from 'react'; + +import ActionGroup from '../ActionGroup'; +import api from '../../lib/api'; +import FormSummary from '../FormSummary'; +import handleAPIError from '../../lib/handleAPIError'; +import MessageGroup from '../MessageGroup'; +import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; +import RadioGroupWithLabel from '../RadioGroupWithLabel'; +import schema from './schema'; +import UncontrolledInput from '../UncontrolledInput'; +import useFormikUtils from '../../hooks/useFormikUtils'; + +const HOST_TYPE_OPTIONS: RadioItemList = { + subnode: { label: 'Subnode', value: 'subnode' }, + dr: { label: 'Disaster Recovery (DR) host', value: 'dr' }, +}; + +const PrepareHostForm: FC = (props) => { + const { host, tools } = props; + + const { disabledSubmit, formik, formikErrors, handleChange } = + useFormikUtils({ + initialValues: { + ip: host.hostIpAddress, + name: host.hostName, + password: host.hostPassword, + type: '', + uuid: host.hostUUID, + }, + onSubmit: (values, { setSubmitting }) => { + const { + enterpriseKey, + ip, + name, + password, + type, + uuid, + redhatPassword, + redhatUsername, + } = values; + + tools.confirm.prepare({ + actionProceedText: 'Prepare', + content: , + onCancelAppend: () => setSubmitting(false), + onProceedAppend: () => { + tools.confirm.loading(true); + + api + .put('/host/prepare', { + enterpriseUUID: enterpriseKey, + hostIPAddress: ip, + hostName: name, + hostPassword: password, + hostType: type, + hostUUID: uuid, + redhatPassword, + redhatUser: redhatUsername, + }) + .then(() => { + tools.confirm.finish('Success', { + children: <>Host at {ip} prepared., + }); + + tools.add.open(false); + }) + .catch((error) => { + const emsg = handleAPIError(error); + + emsg.children = ( + <> + Failed to prepare host at {ip}. {emsg.children} + + ); + + tools.confirm.finish('Error', emsg); + + setSubmitting(false); + }); + }, + titleText: `Prepare host at ${values.ip} with the following?`, + }); + + tools.confirm.open(); + }, + validationSchema: schema, + }); + + const enterpriseKeyChain = useMemo(() => 'enterpriseKey', []); + const nameChain = useMemo(() => 'name', []); + const redhatConfirmPasswordChain = useMemo( + () => 'redhatConfirmPassword', + [], + ); + const redhatPasswordChain = useMemo(() => 'redhatPassword', []); + const redhatUsernameChain = useMemo(() => 'redhatUsername', []); + const typeChain = useMemo(() => 'type', []); + + const showRedhatSection = useMemo( + () => + host.isInetConnected && /rhel/i.test(host.hostOS) && !host.isOSRegistered, + [host.hostOS, host.isInetConnected, host.isOSRegistered], + ); + + return ( + { + event.preventDefault(); + + formik.submitForm(); + }} + spacing="1em" + > + + + } + /> + + + + } + /> + + + + } + /> + + {showRedhatSection && ( + <> + + + } + /> + + + + } + /> + + + + + } + /> + + + )} + + + + + + + + ); +}; + +export default PrepareHostForm; diff --git a/striker-ui/components/ManageHost/TestAccessForm.tsx b/striker-ui/components/ManageHost/TestAccessForm.tsx new file mode 100644 index 00000000..5731fcf9 --- /dev/null +++ b/striker-ui/components/ManageHost/TestAccessForm.tsx @@ -0,0 +1,145 @@ +import { FC, useCallback, useMemo, useRef, useState } from 'react'; +import { Grid } from '@mui/material'; + +import ActionGroup from '../ActionGroup'; +import api from '../../lib/api'; +import handleAPIError from '../../lib/handleAPIError'; +import MessageGroup, { MessageGroupForwardedRefContent } from '../MessageGroup'; +import OutlinedInputWithLabel from '../OutlinedInputWithLabel'; +import UncontrolledInput from '../UncontrolledInput'; +import useFormikUtils from '../../hooks/useFormikUtils'; +import Spinner from '../Spinner'; +import schema from './testAccessSchema'; + +const TestAccessForm: FC = (props) => { + const { setResponse } = props; + + const messageGroupRef = useRef(null); + + const [loadingInquiry, setLoadingInquiry] = useState(false); + + const setApiMessage = useCallback( + (message?: Message) => + messageGroupRef?.current?.setMessage?.call(null, 'api', message), + [], + ); + + const { disabledSubmit, formik, formikErrors, handleChange } = + useFormikUtils({ + initialValues: { + ip: '', + password: '', + }, + onSubmit: (values, { setSubmitting }) => { + setLoadingInquiry(true); + setResponse(undefined); + + const { ip, password } = values; + + api + .put('/command/inquire-host', { + ipAddress: ip, + password, + }) + .then(({ data }) => { + setResponse({ + ...data, + hostIpAddress: ip, + hostPassword: password, + }); + + setApiMessage(); + }) + .catch((error) => { + const emsg = handleAPIError(error); + + emsg.children = ( + <> + Failed to access {ip}. {emsg.children} + + ); + + setApiMessage(emsg); + }) + .finally(() => { + setSubmitting(false); + setLoadingInquiry(false); + }); + }, + validationSchema: schema, + }); + + const ipChain = useMemo(() => 'ip', []); + const passwordChain = useMemo(() => 'password', []); + + return ( + { + event.preventDefault(); + + formik.submitForm(); + }} + spacing="1em" + > + + + } + /> + + + + } + /> + + {loadingInquiry ? ( + + + + ) : ( + <> + + + + + + + + )} + + ); +}; + +export default TestAccessForm; diff --git a/striker-ui/components/ManageHost/index.tsx b/striker-ui/components/ManageHost/index.tsx new file mode 100644 index 00000000..bc5c74d7 --- /dev/null +++ b/striker-ui/components/ManageHost/index.tsx @@ -0,0 +1,4 @@ +import ManageHost from './ManageHost'; +import PrepareHostForm from './PrepareHostForm'; + +export { ManageHost, PrepareHostForm }; diff --git a/striker-ui/components/ManageHost/schema.ts b/striker-ui/components/ManageHost/schema.ts new file mode 100644 index 00000000..b48749b6 --- /dev/null +++ b/striker-ui/components/ManageHost/schema.ts @@ -0,0 +1,35 @@ +import * as yup from 'yup'; + +import { REP_IPV4 } from '../../lib/consts/REG_EXP_PATTERNS'; + +const schema = yup.object().shape( + { + enterpriseKey: yup.string().uuid().optional(), + ip: yup.string().matches(REP_IPV4, { + message: 'Expected IP address to be a valid IPv4 address.', + }), + name: yup.string().required(), + redhatConfirmPassword: yup + .string() + .when('redhatPassword', (redhatPassword, field) => + String(redhatPassword).length > 0 + ? field.required().oneOf([yup.ref('redhatPassword')]) + : field.optional(), + ), + redhatPassword: yup + .string() + .when('redhatUsername', (redhatUsername, field) => + String(redhatUsername).length > 0 ? field.required() : field.optional(), + ), + redhatUsername: yup + .string() + .when('redhatPassword', (redhatPassword, field) => + String(redhatPassword).length > 0 ? field.required() : field.optional(), + ), + type: yup.string().oneOf(['dr', 'subnode']).required(), + uuid: yup.string().uuid().required(), + }, + [['redhatUsername', 'redhatPassword']], +); + +export default schema; diff --git a/striker-ui/components/ManageHost/testAccessSchema.ts b/striker-ui/components/ManageHost/testAccessSchema.ts new file mode 100644 index 00000000..196795f0 --- /dev/null +++ b/striker-ui/components/ManageHost/testAccessSchema.ts @@ -0,0 +1,15 @@ +import * as yup from 'yup'; + +import { REP_IPV4 } from '../../lib/consts/REG_EXP_PATTERNS'; + +const schema = yup.object({ + ip: yup + .string() + .matches(REP_IPV4, { + message: 'Expected IP address to be a valid IPv4 address.', + }) + .required(), + password: yup.string().required(), +}); + +export default schema; diff --git a/striker-ui/types/ManageHost.d.ts b/striker-ui/types/ManageHost.d.ts new file mode 100644 index 00000000..beb65451 --- /dev/null +++ b/striker-ui/types/ManageHost.d.ts @@ -0,0 +1,38 @@ +type InquireHostResponse = APICommandInquireHostResponseBody & { + hostIpAddress: string; + hostPassword: string; +}; + +/** TestAccessForm */ + +type TestAccessFormikValues = { + ip: string; + password: string; +}; + +type TestAccessFormProps = { + setResponse: React.Dispatch< + React.SetStateAction + >; +}; + +/** PrepareHostForm */ + +/** + * @property hostType - Type of host to prepare; note that `node` is `subnode` + * due to renaming. + */ +type PrepareHostFormikValues = TestAccessFormikValues & { + enterpriseKey?: string; + name: string; + redhatConfirmPassword?: string; + redhatPassword?: string; + redhatUsername?: string; + type: '' | 'dr' | 'subnode'; + uuid: string; +}; + +type PreapreHostFormProps = { + host: InquireHostResponse; + tools: CrudListFormTools; +};