Merge pull request #588 from ylei-tsubame/issues/414-reconnect-init-host

Web UI: replace prepare host step in manage anvil node(s)
main
Digimer 10 months ago committed by GitHub
commit 77b7f2bb37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 35
      striker-ui/components/CrudList.tsx
  2. 29
      striker-ui/components/FormSummary.tsx
  3. 48
      striker-ui/components/ManageHost/ManageHost.tsx
  4. 228
      striker-ui/components/ManageHost/PrepareHostForm.tsx
  5. 149
      striker-ui/components/ManageHost/TestAccessForm.tsx
  6. 4
      striker-ui/components/ManageHost/index.tsx
  7. 35
      striker-ui/components/ManageHost/schema.ts
  8. 15
      striker-ui/components/ManageHost/testAccessSchema.ts
  9. 31
      striker-ui/components/ManageMailServer/AddMailServerForm.tsx
  10. 42
      striker-ui/components/OutlinedInput/OutlinedInput.tsx
  11. 4
      striker-ui/components/OutlinedInputWithLabel.tsx
  12. 671
      striker-ui/components/PrepareHostForm.tsx
  13. 15
      striker-ui/components/RadioGroupWithLabel.tsx
  14. 11
      striker-ui/hooks/useFetch.tsx
  15. 13
      striker-ui/hooks/useFormikUtils.ts
  16. 9
      striker-ui/lib/disassembleCamel.ts
  17. 8
      striker-ui/lib/getFormikErrorMessages.ts
  18. 1
      striker-ui/out/_next/static/BQQcSBBTtNN9dIUZ9m3Ow/_buildManifest.js
  19. 1
      striker-ui/out/_next/static/JM2ldb5hOFExU7LFXSU9o/_buildManifest.js
  20. 0
      striker-ui/out/_next/static/JM2ldb5hOFExU7LFXSU9o/_ssgManifest.js
  21. 1
      striker-ui/out/_next/static/chunks/17-0593dc8fdd9c512e.js
  22. 2
      striker-ui/out/_next/static/chunks/264-683b93ad6e70a8fb.js
  23. 1
      striker-ui/out/_next/static/chunks/270-56592f453c639f63.js
  24. 1
      striker-ui/out/_next/static/chunks/270-9058c1049a825f7d.js
  25. 1
      striker-ui/out/_next/static/chunks/380-0eff6addb79bd61f.js
  26. 2
      striker-ui/out/_next/static/chunks/486-45903b907cd7ece3.js
  27. 1
      striker-ui/out/_next/static/chunks/556-0c0cace3593e5307.js
  28. 1
      striker-ui/out/_next/static/chunks/556-dbf62d8622405edc.js
  29. 1
      striker-ui/out/_next/static/chunks/569-fa9b9ac8a7639d2d.js
  30. 2
      striker-ui/out/_next/static/chunks/675-9a50fb0ae255b835.js
  31. 2
      striker-ui/out/_next/static/chunks/750-9f873f4e10dbcacd.js
  32. 1
      striker-ui/out/_next/static/chunks/814-6420b976d086fe20.js
  33. 1
      striker-ui/out/_next/static/chunks/pages/config-144ed56943e89e8c.js
  34. 1
      striker-ui/out/_next/static/chunks/pages/config-1c39d13147dfe819.js
  35. 1
      striker-ui/out/_next/static/chunks/pages/file-manager-8a23bb0baebac7f6.js
  36. 1
      striker-ui/out/_next/static/chunks/pages/file-manager-c8a2ce2c02dc39fc.js
  37. 1
      striker-ui/out/_next/static/chunks/pages/index-0e23f6af1e089a97.js
  38. 1
      striker-ui/out/_next/static/chunks/pages/index-6febd0ab3b8c828c.js
  39. 1
      striker-ui/out/_next/static/chunks/pages/login-1d03dfaf2cb572f8.js
  40. 1
      striker-ui/out/_next/static/chunks/pages/login-5fd1d7a2717b59af.js
  41. 1
      striker-ui/out/_next/static/chunks/pages/mail-config-14cdc2dd46514057.js
  42. 1
      striker-ui/out/_next/static/chunks/pages/mail-config-77c70d16ef879e90.js
  43. 1
      striker-ui/out/_next/static/chunks/pages/manage-element-766bd9ef38ccbfa4.js
  44. 1
      striker-ui/out/_next/static/chunks/pages/manage-element-7ac129e45d98ff58.js
  45. 2
      striker-ui/out/anvil.html
  46. 2
      striker-ui/out/config.html
  47. 2
      striker-ui/out/file-manager.html
  48. 2
      striker-ui/out/index.html
  49. 2
      striker-ui/out/init.html
  50. 2
      striker-ui/out/login.html
  51. 2
      striker-ui/out/mail-config.html
  52. 2
      striker-ui/out/manage-element.html
  53. 2
      striker-ui/out/server.html
  54. 14
      striker-ui/pages/manage-element/index.tsx
  55. 1
      striker-ui/types/CrudList.d.ts
  56. 6
      striker-ui/types/FormSummary.d.ts
  57. 5
      striker-ui/types/FormikUtils.d.ts
  58. 38
      striker-ui/types/ManageHost.d.ts
  59. 11
      striker-ui/types/RadioGroupWithLabel.d.ts

@ -39,6 +39,8 @@ const CrudList = <
renderDeleteItem, renderDeleteItem,
renderEditForm, renderEditForm,
renderListItem, renderListItem,
// Dependents
entryUrlPrefix = entriesUrl,
} = props; } = props;
const addDialogRef = useRef<DialogForwardedRefContent>(null); const addDialogRef = useRef<DialogForwardedRefContent>(null);
@ -54,25 +56,16 @@ const CrudList = <
const [edit, setEdit] = useState<boolean>(false); const [edit, setEdit] = useState<boolean>(false);
const [entry, setEntry] = useState<Detail | undefined>(); const [entry, setEntry] = useState<Detail | undefined>();
const [entries, setEntries] = useState<OverviewList | undefined>();
const { loading: loadingEntriesPeriodic } = useFetch<OverviewList>( const {
entriesUrl, data: entries,
{ mutate: refreshEntries,
onSuccess: (data) => setEntries(data), loading: loadingEntries,
refreshInterval, } = useFetch<OverviewList>(entriesUrl, { refreshInterval });
},
);
const { fetch: getEntries, loading: loadingEntriesActive } =
useActiveFetch<OverviewList>({
onData: (data) => setEntries(data),
url: entriesUrl,
});
const { fetch: getEntry, loading: loadingEntry } = useActiveFetch<Detail>({ const { fetch: getEntry, loading: loadingEntry } = useActiveFetch<Detail>({
onData: (data) => setEntry(data), onData: (data) => setEntry(data),
url: entriesUrl, url: entryUrlPrefix,
}); });
const addHeader = useMemo<React.ReactNode>( const addHeader = useMemo<React.ReactNode>(
@ -108,11 +101,6 @@ const CrudList = <
], ],
); );
const loadingEntries = useMemo<boolean>(
() => loadingEntriesPeriodic || loadingEntriesActive,
[loadingEntriesActive, loadingEntriesPeriodic],
);
const { const {
buildDeleteDialogProps, buildDeleteDialogProps,
checks, checks,
@ -162,15 +150,16 @@ const CrudList = <
.then(() => { .then(() => {
finishConfirm('Success', getDeleteSuccessMessage()); finishConfirm('Success', getDeleteSuccessMessage());
getEntries(); refreshEntries();
}) })
.catch((error) => { .catch((error) => {
const emsg = handleAPIError(error); const emsg = handleAPIError(error);
finishConfirm('Error', getDeleteErrorMessage(emsg)); finishConfirm('Error', getDeleteErrorMessage(emsg));
}); })
.finally(() => {
resetChecks(); resetChecks();
});
}, },
getConfirmDialogTitle: getDeleteHeader, getConfirmDialogTitle: getDeleteHeader,
renderEntry: (...args) => renderDeleteItem(entries, ...args), renderEntry: (...args) => renderDeleteItem(entries, ...args),

@ -1,16 +1,9 @@
import { Box, List as MUIList, ListItem as MUIListItem } from '@mui/material'; import { Box, List as MUIList, ListItem as MUIListItem } from '@mui/material';
import { capitalize } from 'lodash';
import { FC, ReactElement } from 'react'; import { FC, ReactElement } from 'react';
import FlexBox from './FlexBox'; import FlexBox from './FlexBox';
import { BodyText, MonoText, SensitiveText } from './Text'; import { BodyText, MonoText, SensitiveText } from './Text';
import disassembleCamel from '../lib/disassembleCamel';
const capEntryLabel: CapFormEntryLabel = (value) => {
const spaced = value.replace(/([a-z\d])([A-Z])/g, '$1 $2');
const lcased = spaced.toLowerCase();
return capitalize(lcased);
};
const renderEntryValueWithMono: RenderFormValueFunction = ({ entry }) => ( const renderEntryValueWithMono: RenderFormValueFunction = ({ entry }) => (
<MonoText whiteSpace="nowrap">{String(entry)}</MonoText> <MonoText whiteSpace="nowrap">{String(entry)}</MonoText>
@ -42,6 +35,7 @@ const buildEntryList = ({
maxDepth, maxDepth,
renderEntry, renderEntry,
renderEntryValue, renderEntryValue,
skip,
}: { }: {
depth?: number; depth?: number;
entries: FormEntries; entries: FormEntries;
@ -52,6 +46,7 @@ const buildEntryList = ({
maxDepth: number; maxDepth: number;
renderEntry: RenderFormEntryFunction; renderEntry: RenderFormEntryFunction;
renderEntryValue: RenderFormValueFunction; renderEntryValue: RenderFormValueFunction;
skip: Exclude<FormSummaryOptionalProps['skip'], undefined>;
}): ReactElement => { }): ReactElement => {
const result: ReactElement[] = []; const result: ReactElement[] = [];
@ -59,13 +54,21 @@ const buildEntryList = ({
const itemId = `form-summary-entry-${itemKey}`; const itemId = `form-summary-entry-${itemKey}`;
const nest = entry !== null && typeof entry === 'object'; const nest = entry !== null && typeof entry === 'object';
const value = nest ? null : entry; const value = nest ? null : entry;
const fnArgs: CommonFormEntryHandlerArgs = {
depth,
entry: value,
key: itemKey,
};
if (skip(({ key }) => !/confirm/i.test(key), fnArgs)) {
result.push( result.push(
<MUIListItem <MUIListItem
key={itemId} key={itemId}
sx={{ paddingLeft: `${depth}em` }} sx={{ paddingLeft: `${depth}em` }}
{...getListItemProps?.call(null, { depth, entry: value, key: itemKey })} {...getListItemProps?.call(null, fnArgs)}
> >
{renderEntry({ {renderEntry({
depth, depth,
@ -77,6 +80,7 @@ const buildEntryList = ({
})} })}
</MUIListItem>, </MUIListItem>,
); );
}
if (nest && depth < maxDepth) { if (nest && depth < maxDepth) {
result.push( result.push(
@ -88,6 +92,7 @@ const buildEntryList = ({
maxDepth, maxDepth,
renderEntry, renderEntry,
renderEntryValue, renderEntryValue,
skip,
}), }),
); );
} }
@ -116,7 +121,9 @@ const FormSummary = <T extends FormEntries>({
maxDepth = 3, maxDepth = 3,
renderEntry = ({ depth, entry, getLabel, key, nest, renderValue }) => ( renderEntry = ({ depth, entry, getLabel, key, nest, renderValue }) => (
<FlexBox fullWidth growFirst row maxWidth="100%"> <FlexBox fullWidth growFirst row maxWidth="100%">
<BodyText>{getLabel({ cap: capEntryLabel, depth, entry, key })}</BodyText> <BodyText>
{getLabel({ cap: disassembleCamel, depth, entry, key })}
</BodyText>
<Box sx={{ maxWidth: '100%', overflowX: 'scroll' }}> <Box sx={{ maxWidth: '100%', overflowX: 'scroll' }}>
{!nest && renderValue({ depth, entry, key })} {!nest && renderValue({ depth, entry, key })}
</Box> </Box>
@ -134,6 +141,7 @@ const FormSummary = <T extends FormEntries>({
? renderEntryValueWithPassword(args) ? renderEntryValueWithPassword(args)
: renderEntryValueWithMono(args); : renderEntryValueWithMono(args);
}, },
skip = (base, ...args) => base(...args),
}: FormSummaryProps<T>): ReturnType<FC<FormSummaryProps<T>>> => }: FormSummaryProps<T>): ReturnType<FC<FormSummaryProps<T>>> =>
buildEntryList({ buildEntryList({
entries, entries,
@ -143,6 +151,7 @@ const FormSummary = <T extends FormEntries>({
maxDepth, maxDepth,
renderEntry, renderEntry,
renderEntryValue, renderEntryValue,
skip,
}); });
export default FormSummary; export default FormSummary;

@ -0,0 +1,48 @@
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 (
<CrudList<APIHostOverview, APIHostDetail>
addHeader="Initialize host"
editHeader=""
entriesUrl="/host?types=dr,node"
entryUrlPrefix="/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) => (
<>
<TestAccessForm setResponse={setInquireHostResponse} />
{inquireHostResponse && (
<PrepareHostForm host={inquireHostResponse} tools={tools} />
)}
</>
)}
renderDeleteItem={(hosts, { key }) => {
const host = hosts?.[key];
return <BodyText>{host?.shortHostName}</BodyText>;
}}
renderEditForm={() => <></>}
renderListItem={(uuid, { hostName }) => <BodyText>{hostName}</BodyText>}
/>
);
};
export default ManageHost;

@ -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<PreapreHostFormProps> = (props) => {
const { host, tools } = props;
const { disabledSubmit, formik, formikErrors, handleChange } =
useFormikUtils<PrepareHostFormikValues>({
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: <FormSummary entries={values} hasPassword />,
onCancelAppend: () => setSubmitting(false),
onProceedAppend: () => {
tools.confirm.loading(true);
api
.put('/host/prepare', {
enterpriseUUID: enterpriseKey,
hostIPAddress: ip,
hostName: name,
hostPassword: password,
hostType: type === 'subnode' ? 'node' : type,
hostUUID: uuid,
redhatPassword,
redhatUser: redhatUsername,
})
.then(() => {
tools.confirm.finish('Success', {
children: <>Started job to prepare host at {ip}.</>,
});
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<string>(() => 'enterpriseKey', []);
const nameChain = useMemo<string>(() => 'name', []);
const redhatConfirmPasswordChain = useMemo<string>(
() => 'redhatConfirmPassword',
[],
);
const redhatPasswordChain = useMemo<string>(() => 'redhatPassword', []);
const redhatUsernameChain = useMemo<string>(() => 'redhatUsername', []);
const typeChain = useMemo<string>(() => 'type', []);
const showRedhatSection = useMemo<boolean>(
() =>
host.isInetConnected && /rhel/i.test(host.hostOS) && !host.isOSRegistered,
[host.hostOS, host.isInetConnected, host.isOSRegistered],
);
return (
<Grid
columns={{ xs: 1, sm: 2 }}
component="form"
container
onSubmit={(event) => {
event.preventDefault();
formik.submitForm();
}}
spacing="1em"
>
<Grid item width="100%">
<UncontrolledInput
input={
<RadioGroupWithLabel
id={typeChain}
label="Host type"
name={typeChain}
onChange={handleChange}
radioItems={HOST_TYPE_OPTIONS}
value={formik.values.type}
/>
}
/>
</Grid>
<Grid item xs={1}>
<UncontrolledInput
input={
<OutlinedInputWithLabel
id={nameChain}
label="Host name"
name={nameChain}
onChange={handleChange}
required
value={formik.values.name}
/>
}
/>
</Grid>
<Grid item xs={1}>
<UncontrolledInput
input={
<OutlinedInputWithLabel
id={enterpriseKeyChain}
label="Alteeve enterprise key"
name={enterpriseKeyChain}
onChange={handleChange}
value={formik.values.enterpriseKey}
/>
}
/>
</Grid>
{showRedhatSection && (
<>
<Grid item xs={1}>
<UncontrolledInput
input={
<OutlinedInputWithLabel
disableAutofill
id={redhatUsernameChain}
label="RedHat username"
name={redhatUsernameChain}
onChange={handleChange}
value={formik.values.redhatUsername}
/>
}
/>
</Grid>
<Grid item xs={1}>
<UncontrolledInput
input={
<OutlinedInputWithLabel
disableAutofill
id={redhatPasswordChain}
label="RedHat password"
name={redhatPasswordChain}
onChange={handleChange}
type="password"
value={formik.values.redhatPassword}
/>
}
/>
</Grid>
<Grid display={{ xs: 'none', sm: 'initial' }} item sm={1} />
<Grid item xs={1}>
<UncontrolledInput
input={
<OutlinedInputWithLabel
disableAutofill
id={redhatConfirmPasswordChain}
label="Confirm RedHat password"
name={redhatConfirmPasswordChain}
onChange={handleChange}
type="password"
value={formik.values.redhatConfirmPassword}
/>
}
/>
</Grid>
</>
)}
<Grid item width="100%">
<MessageGroup count={1} messages={formikErrors} />
</Grid>
<Grid item width="100%">
<ActionGroup
actions={[
{
background: 'blue',
children: 'Prepare host',
disabled: disabledSubmit,
type: 'submit',
},
]}
/>
</Grid>
</Grid>
);
};
export default PrepareHostForm;

@ -0,0 +1,149 @@
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<TestAccessFormProps> = (props) => {
const { setResponse } = props;
const messageGroupRef = useRef<MessageGroupForwardedRefContent>(null);
const [loadingInquiry, setLoadingInquiry] = useState<boolean>(false);
const setApiMessage = useCallback(
(message?: Message) =>
messageGroupRef?.current?.setMessage?.call(null, 'api', message),
[],
);
const { disabledSubmit, formik, formikErrors, handleChange } =
useFormikUtils<TestAccessFormikValues>({
initialValues: {
ip: '',
password: '',
},
onSubmit: (values, { setSubmitting }) => {
setLoadingInquiry(true);
setResponse(undefined);
const { ip, password } = values;
api
.put<APICommandInquireHostResponseBody>('/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<string>(() => 'ip', []);
const passwordChain = useMemo<string>(() => 'password', []);
return (
<Grid
component="form"
container
columns={{ xs: 1, sm: 2 }}
onSubmit={(event) => {
event.preventDefault();
formik.submitForm();
}}
spacing="1em"
>
<Grid item xs={1}>
<UncontrolledInput
input={
<OutlinedInputWithLabel
disableAutofill
id={ipChain}
label="IP address"
name={ipChain}
onChange={handleChange}
required
value={formik.values.ip}
/>
}
/>
</Grid>
<Grid item xs={1}>
<UncontrolledInput
input={
<OutlinedInputWithLabel
disableAutofill
id={passwordChain}
label="Password"
name={passwordChain}
onChange={handleChange}
required
type="password"
value={formik.values.password}
/>
}
/>
</Grid>
{loadingInquiry ? (
<Grid item width="100%">
<Spinner />
</Grid>
) : (
<>
<Grid item width="100%">
<MessageGroup
count={1}
messages={formikErrors}
ref={messageGroupRef}
/>
</Grid>
<Grid item width="100%">
<ActionGroup
actions={[
{
background: 'blue',
children: 'Test access',
disabled: disabledSubmit,
type: 'submit',
},
]}
/>
</Grid>
</>
)}
</Grid>
);
};
export default TestAccessForm;

@ -0,0 +1,4 @@
import ManageHost from './ManageHost';
import PrepareHostForm from './PrepareHostForm';
export { ManageHost, PrepareHostForm };

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

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

@ -26,13 +26,8 @@ const AddMailServerForm: FC<AddMailServerFormProps> = (props) => {
[mailServerUuid], [mailServerUuid],
); );
const { const { disabledSubmit, formik, formikErrors, handleChange } =
disableAutocomplete, useFormikUtils<MailServerFormikValues>({
disabledSubmit,
formik,
formikErrors,
handleChange,
} = useFormikUtils<MailServerFormikValues>({
initialValues: previousFormikValues ?? { initialValues: previousFormikValues ?? {
[msUuid]: { [msUuid]: {
address: '', address: '',
@ -136,7 +131,6 @@ const AddMailServerForm: FC<AddMailServerFormProps> = (props) => {
id={addressChain} id={addressChain}
label="Server address" label="Server address"
name={addressChain} name={addressChain}
onBlur={formik.handleBlur}
onChange={handleChange} onChange={handleChange}
required required
value={formik.values[msUuid].address} value={formik.values[msUuid].address}
@ -151,7 +145,6 @@ const AddMailServerForm: FC<AddMailServerFormProps> = (props) => {
id={portChain} id={portChain}
label="Server port" label="Server port"
name={portChain} name={portChain}
onBlur={formik.handleBlur}
onChange={handleChange} onChange={handleChange}
required required
type="number" type="number"
@ -160,14 +153,13 @@ const AddMailServerForm: FC<AddMailServerFormProps> = (props) => {
} }
/> />
</Grid> </Grid>
<Grid item sm={2} xs={1}> <Grid item width="100%">
<UncontrolledInput <UncontrolledInput
input={ input={
<SelectWithLabel <SelectWithLabel
id={securityChain} id={securityChain}
label="Server security type" label="Server security type"
name={securityChain} name={securityChain}
onBlur={formik.handleBlur}
onChange={handleChange} onChange={handleChange}
required required
selectItems={['none', 'starttls', 'tls-ssl']} selectItems={['none', 'starttls', 'tls-ssl']}
@ -176,14 +168,13 @@ const AddMailServerForm: FC<AddMailServerFormProps> = (props) => {
} }
/> />
</Grid> </Grid>
<Grid item sm={2} xs={1}> <Grid item width="100%">
<UncontrolledInput <UncontrolledInput
input={ input={
<SelectWithLabel <SelectWithLabel
id={authenticationChain} id={authenticationChain}
label="Server authentication method" label="Server authentication method"
name={authenticationChain} name={authenticationChain}
onBlur={formik.handleBlur}
onChange={handleChange} onChange={handleChange}
required required
selectItems={['none', 'plain-text', 'encrypted']} selectItems={['none', 'plain-text', 'encrypted']}
@ -192,14 +183,13 @@ const AddMailServerForm: FC<AddMailServerFormProps> = (props) => {
} }
/> />
</Grid> </Grid>
<Grid item sm={2} xs={1}> <Grid item width="100%">
<UncontrolledInput <UncontrolledInput
input={ input={
<OutlinedInputWithLabel <OutlinedInputWithLabel
id={heloDomainChain} id={heloDomainChain}
label="HELO domain" label="HELO domain"
name={heloDomainChain} name={heloDomainChain}
onBlur={formik.handleBlur}
onChange={handleChange} onChange={handleChange}
required required
value={formik.values[msUuid].heloDomain} value={formik.values[msUuid].heloDomain}
@ -211,11 +201,10 @@ const AddMailServerForm: FC<AddMailServerFormProps> = (props) => {
<UncontrolledInput <UncontrolledInput
input={ input={
<OutlinedInputWithLabel <OutlinedInputWithLabel
disableAutofill
id={usernameChain} id={usernameChain}
inputProps={disableAutocomplete()}
label="Server username" label="Server username"
name={usernameChain} name={usernameChain}
onBlur={formik.handleBlur}
onChange={handleChange} onChange={handleChange}
value={formik.values[msUuid].username} value={formik.values[msUuid].username}
/> />
@ -226,11 +215,10 @@ const AddMailServerForm: FC<AddMailServerFormProps> = (props) => {
<UncontrolledInput <UncontrolledInput
input={ input={
<OutlinedInputWithLabel <OutlinedInputWithLabel
disableAutofill
id={passwordChain} id={passwordChain}
inputProps={disableAutocomplete()}
label="Server password" label="Server password"
name={passwordChain} name={passwordChain}
onBlur={formik.handleBlur}
onChange={handleChange} onChange={handleChange}
type="password" type="password"
value={formik.values[msUuid].password} value={formik.values[msUuid].password}
@ -238,16 +226,15 @@ const AddMailServerForm: FC<AddMailServerFormProps> = (props) => {
} }
/> />
</Grid> </Grid>
<Grid item xs={1} /> <Grid display={{ xs: 'none', sm: 'initial' }} item sm={1} />
<Grid item xs={1}> <Grid item xs={1}>
<UncontrolledInput <UncontrolledInput
input={ input={
<OutlinedInputWithLabel <OutlinedInputWithLabel
disableAutofill
id={confirmPasswordChain} id={confirmPasswordChain}
inputProps={disableAutocomplete()}
label="Confirm password" label="Confirm password"
name={confirmPasswordChain} name={confirmPasswordChain}
onBlur={formik.handleBlur}
onChange={handleChange} onChange={handleChange}
type="password" type="password"
value={formik.values[msUuid].confirmPassword} value={formik.values[msUuid].confirmPassword}

@ -15,6 +15,7 @@ import { GREY, TEXT, UNSELECTED } from '../../lib/consts/DEFAULT_THEME';
import INPUT_TYPES from '../../lib/consts/INPUT_TYPES'; import INPUT_TYPES from '../../lib/consts/INPUT_TYPES';
type OutlinedInputOptionalProps = { type OutlinedInputOptionalProps = {
disableAutofill?: boolean;
onPasswordVisibilityAppend?: ( onPasswordVisibilityAppend?: (
inputType: string, inputType: string,
...restArgs: Parameters<Exclude<MUIIconButtonProps['onClick'], undefined>> ...restArgs: Parameters<Exclude<MUIIconButtonProps['onClick'], undefined>>
@ -25,13 +26,15 @@ type OutlinedInputProps = MUIOutlinedInputProps & OutlinedInputOptionalProps;
const OUTLINED_INPUT_DEFAULT_PROPS: Pick< const OUTLINED_INPUT_DEFAULT_PROPS: Pick<
OutlinedInputOptionalProps, OutlinedInputOptionalProps,
'onPasswordVisibilityAppend' 'disableAutofill' | 'onPasswordVisibilityAppend'
> = { > = {
disableAutofill: false,
onPasswordVisibilityAppend: undefined, onPasswordVisibilityAppend: undefined,
}; };
const OutlinedInput: FC<OutlinedInputProps> = (outlinedInputProps) => { const OutlinedInput: FC<OutlinedInputProps> = (outlinedInputProps) => {
const { const {
disableAutofill = false,
endAdornment, endAdornment,
label, label,
onPasswordVisibilityAppend, onPasswordVisibilityAppend,
@ -68,6 +71,7 @@ const OutlinedInput: FC<OutlinedInputProps> = (outlinedInputProps) => {
</> </>
); );
}, [initialType, onPasswordVisibilityAppend, type]); }, [initialType, onPasswordVisibilityAppend, type]);
const combinedSx = useMemo( const combinedSx = useMemo(
() => ({ () => ({
color: GREY, color: GREY,
@ -102,6 +106,7 @@ const OutlinedInput: FC<OutlinedInputProps> = (outlinedInputProps) => {
}), }),
[label, sx], [label, sx],
); );
const combinedEndAdornment = useMemo(() => { const combinedEndAdornment = useMemo(() => {
let result; let result;
@ -125,18 +130,33 @@ const OutlinedInput: FC<OutlinedInputProps> = (outlinedInputProps) => {
return result; return result;
}, [passwordVisibilityButton, endAdornment]); }, [passwordVisibilityButton, endAdornment]);
const autofillLock = useMemo<
Pick<MUIOutlinedInputProps, 'onFocus' | 'readOnly'> | undefined
>(
() =>
disableAutofill
? {
onFocus: (...args) => {
const [event] = args;
event.target.readOnly = false;
outlinedInputRestProps?.onFocus?.call(null, ...args);
},
readOnly: true,
}
: undefined,
[disableAutofill, outlinedInputRestProps?.onFocus],
);
return ( return (
<MUIOutlinedInput <MUIOutlinedInput
{...{ endAdornment={combinedEndAdornment}
endAdornment: combinedEndAdornment, label={label}
label, inputProps={{ type, ...inputRestProps }}
inputProps: { {...outlinedInputRestProps}
type, {...autofillLock}
...inputRestProps, sx={combinedSx}
},
...outlinedInputRestProps,
sx: combinedSx,
}}
/> />
); );
}; };

@ -44,7 +44,7 @@ type OutlinedInputWithLabelOptionalProps =
type OutlinedInputWithLabelProps = Pick< type OutlinedInputWithLabelProps = Pick<
OutlinedInputProps, OutlinedInputProps,
'name' | 'onBlur' | 'onChange' | 'onFocus' 'disableAutofill' | 'name' | 'onBlur' | 'onChange' | 'onFocus'
> & > &
OutlinedInputWithLabelOptionalProps & { OutlinedInputWithLabelOptionalProps & {
label: string; label: string;
@ -69,6 +69,7 @@ const OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS: Required<OutlinedInputWithLabelOp
const OutlinedInputWithLabel: FC<OutlinedInputWithLabelProps> = ({ const OutlinedInputWithLabel: FC<OutlinedInputWithLabelProps> = ({
baseInputProps, baseInputProps,
disableAutofill,
fillRow: isFillRow = OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS.fillRow, fillRow: isFillRow = OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS.fillRow,
formControlProps = OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS.formControlProps, formControlProps = OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS.formControlProps,
helpMessageBoxProps = OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS.helpMessageBoxProps, helpMessageBoxProps = OUTLINED_INPUT_WITH_LABEL_DEFAULT_PROPS.helpMessageBoxProps,
@ -148,6 +149,7 @@ const OutlinedInputWithLabel: FC<OutlinedInputWithLabelProps> = ({
{label} {label}
</OutlinedInputLabel> </OutlinedInputLabel>
<OutlinedInput <OutlinedInput
disableAutofill={disableAutofill}
endAdornment={ endAdornment={
<MUIInputAdornment <MUIInputAdornment
position="end" position="end"

@ -1,671 +0,0 @@
import {
Visibility as MUIVisibilityIcon,
VisibilityOff as MUIVisibilityOffIcon,
} from '@mui/icons-material';
import { Box as MUIBox, IconButton as MUIIconButton } from '@mui/material';
import { FC, useCallback, useMemo, useRef, useState } from 'react';
import { GREY } from '../lib/consts/DEFAULT_THEME';
import INPUT_TYPES from '../lib/consts/INPUT_TYPES';
import api from '../lib/api';
import handleAPIError from '../lib/handleAPIError';
import {
buildDomainTestBatch,
buildIPAddressTestBatch,
buildPeacefulStringTestBatch,
buildUUIDTestBatch,
createTestInputFunction,
} from '../lib/test_input';
import ConfirmDialog from './ConfirmDialog';
import ContainedButton from './ContainedButton';
import FlexBox from './FlexBox';
import GateForm from './GateForm';
import Grid from './Grid';
import InputWithRef, { InputForwardedRefContent } from './InputWithRef';
import { Message } from './MessageBox';
import MessageGroup, { MessageGroupForwardedRefContent } from './MessageGroup';
import OutlinedInputWithLabel from './OutlinedInputWithLabel';
import { Panel, PanelHeader } from './Panels';
import RadioGroupWithLabel from './RadioGroupWithLabel';
import Spinner from './Spinner';
import { BodyText, HeaderText, MonoText } from './Text';
const ENTERPRISE_KEY_LABEL = 'Alteeve enterprise key';
const HOST_IP_LABEL = 'Host IP address';
const HOST_NAME_LABEL = 'Host name';
const REDHAT_PASSWORD_LABEL = 'RedHat password';
const REDHAT_USER_LABEL = 'RedHat user';
const SUCCESS_MESSAGE_TIMEOUT = 5000;
const IT_IDS = {
enterpriseKey: 'enterpriseKey',
hostName: 'hostName',
redhatPassword: 'redhatPassword',
redhatUser: 'redhatUser',
};
const GRID_COLUMNS: Exclude<GridProps['columns'], undefined> = {
xs: 1,
sm: 2,
};
const GRID_SPACING: Exclude<GridProps['spacing'], undefined> = '1em';
const PrepareHostForm: FC = () => {
const confirmDialogRef = useRef<ConfirmDialogForwardedRefContent>({});
const inputEnterpriseKeyRef = useRef<InputForwardedRefContent<'string'>>({});
const inputHostNameRef = useRef<InputForwardedRefContent<'string'>>({});
const inputRedhatPassword = useRef<InputForwardedRefContent<'string'>>({});
const inputRedhatUser = useRef<InputForwardedRefContent<'string'>>({});
const messageGroupRef = useRef<MessageGroupForwardedRefContent>({});
const [confirmValues, setConfirmValues] = useState<
| {
enterpriseKey: string;
hostName: string;
redhatPassword: string;
redhatPasswordHidden: string;
redhatUser: string;
}
| undefined
>();
const [connectedHostIPAddress, setConnectedHostIPAddress] = useState<
string | undefined
>();
const [connectedHostPassword, setConnectedHostPassword] = useState<
string | undefined
>();
const [connectedHostUUID, setConnectedHostUUID] = useState<string>('');
const [inputHostType, setInputHostType] = useState<string>('');
const [isInputEnterpriseKeyValid, setIsInputEnterpriseKeyValid] =
useState<boolean>(true);
const [isInputHostNameValid, setIsInputHostNameValid] =
useState<boolean>(false);
const [isInputRedhatPasswordValid, setIsInputRedhatPasswordValid] =
useState<boolean>(true);
const [isInputRedhatUserValid, setIsInputRedhatUserValid] =
useState<boolean>(true);
const [isShowAccessSection, setIsShowAccessSection] =
useState<boolean>(false);
const [isShowAccessSubmit, setIsShowAccessSubmit] = useState<boolean>(true);
const [isShowOptionalSection, setIsShowOptionalSection] =
useState<boolean>(false);
const [isShowRedhatPassword, setIsShowRedhatPassword] =
useState<boolean>(false);
const [isShowRedhatSection, setIsShowRedhatSection] =
useState<boolean>(false);
const [isSubmittingPrepareHost, setIsSubmittingPrepareHost] =
useState<boolean>(false);
const setHostNameInputMessage = useCallback((message?: Message) => {
messageGroupRef.current.setMessage?.call(null, IT_IDS.hostName, message);
}, []);
const setEnterpriseKeyInputMessage = useCallback((message?: Message) => {
messageGroupRef.current.setMessage?.call(
null,
IT_IDS.enterpriseKey,
message,
);
}, []);
const setRedhatPasswordInputMessage = useCallback((message?: Message) => {
messageGroupRef.current.setMessage?.call(
null,
IT_IDS.redhatPassword,
message,
);
}, []);
const setRedhatUserInputMessage = useCallback((message?: Message) => {
messageGroupRef.current.setMessage?.call(null, IT_IDS.redhatUser, message);
}, []);
const setSubmitPrepareHostMessage = useCallback(
(message?: Message) =>
messageGroupRef.current.setMessage?.call(
null,
'submitPrepareHost',
message,
),
[],
);
const inputTests = useMemo(
() => ({
[IT_IDS.enterpriseKey]: buildUUIDTestBatch(
ENTERPRISE_KEY_LABEL,
() => {
setEnterpriseKeyInputMessage();
},
undefined,
(message) => {
setEnterpriseKeyInputMessage({ children: message, type: 'warning' });
},
),
[IT_IDS.hostName]: buildDomainTestBatch(
HOST_NAME_LABEL,
() => {
setHostNameInputMessage();
},
undefined,
(message) => {
setHostNameInputMessage({ children: message, type: 'warning' });
},
),
[IT_IDS.redhatPassword]: buildPeacefulStringTestBatch(
REDHAT_PASSWORD_LABEL,
() => {
setRedhatPasswordInputMessage();
},
undefined,
(message) => {
setRedhatPasswordInputMessage({ children: message, type: 'warning' });
},
),
[IT_IDS.redhatUser]: buildPeacefulStringTestBatch(
REDHAT_USER_LABEL,
() => {
setRedhatUserInputMessage();
},
undefined,
(message) => {
setRedhatUserInputMessage({ children: message, type: 'warning' });
},
),
}),
[
setEnterpriseKeyInputMessage,
setHostNameInputMessage,
setRedhatPasswordInputMessage,
setRedhatUserInputMessage,
],
);
const testInput = useMemo(
() => createTestInputFunction(inputTests),
[inputTests],
);
const redhatElementSxDisplay = useMemo(
() => (isShowRedhatSection ? undefined : 'none'),
[isShowRedhatSection],
);
const accessSection = useMemo(
() => (
<GateForm
allowSubmit={isShowAccessSubmit}
gridProps={{
wrapperBoxProps: {
sx: {
display: isShowAccessSection ? 'flex' : 'none',
},
},
}}
identifierInputTestBatchBuilder={buildIPAddressTestBatch}
identifierLabel={HOST_IP_LABEL}
onIdentifierBlurAppend={({ target: { value } }) => {
if (connectedHostIPAddress) {
const isIdentifierChanged = value !== connectedHostIPAddress;
setIsShowAccessSubmit(isIdentifierChanged);
setIsShowOptionalSection(!isIdentifierChanged);
setIsShowRedhatSection(!isIdentifierChanged);
}
}}
onSubmitAppend={(
ipAddress,
password,
setGateMessage,
setGateIsSubmitting,
) => {
const body = { ipAddress, password };
api
.put<APICommandInquireHostResponseBody>(
'/command/inquire-host',
body,
)
.then(
({
data: {
hostName,
hostOS,
hostUUID,
isConnected,
isInetConnected,
isOSRegistered,
},
}) => {
if (isConnected) {
inputHostNameRef.current.setValue?.call(null, hostName);
const valid = testInput({
inputs: { [IT_IDS.hostName]: { value: hostName } },
});
setIsInputHostNameValid(valid);
if (
isInetConnected &&
/rhel/i.test(hostOS) &&
!isOSRegistered
) {
setIsShowRedhatSection(true);
}
setConnectedHostIPAddress(ipAddress);
setConnectedHostPassword(password);
setConnectedHostUUID(hostUUID);
setIsShowAccessSubmit(false);
setIsShowOptionalSection(true);
} else {
setGateMessage({
children: `Failed to establish a connection with the given host credentials.`,
type: 'error',
});
}
},
)
.catch((apiError) => {
const emsg = handleAPIError(apiError);
setGateMessage?.call(null, emsg);
})
.finally(() => {
setGateIsSubmitting(false);
});
}}
passphraseLabel="Host root password"
submitLabel="Test access"
/>
),
[
isShowAccessSection,
isShowAccessSubmit,
connectedHostIPAddress,
setConnectedHostPassword,
setConnectedHostUUID,
testInput,
],
);
const optionalSection = useMemo(
() => (
<Grid
columns={GRID_COLUMNS}
layout={{
'preparehost-host-name': {
children: (
<InputWithRef
input={
<OutlinedInputWithLabel
formControlProps={{ sx: { width: '100%' } }}
id="preparehost-host-name-input"
inputProps={{
onBlur: ({ target: { value } }) => {
const valid = testInput({
inputs: { [IT_IDS.hostName]: { value } },
});
setIsInputHostNameValid(valid);
},
onFocus: () => {
setHostNameInputMessage();
},
}}
label={HOST_NAME_LABEL}
/>
}
ref={inputHostNameRef}
/>
),
},
'preparehost-enterprise-key': {
children: (
<InputWithRef
input={
<OutlinedInputWithLabel
formControlProps={{ sx: { width: '100%' } }}
id="preparehost-enterprise-key-input"
inputProps={{
onBlur: ({ target: { value } }) => {
if (value) {
const valid = testInput({
inputs: { [IT_IDS.enterpriseKey]: { value } },
});
setIsInputEnterpriseKeyValid(valid);
}
},
onFocus: () => {
setEnterpriseKeyInputMessage();
},
}}
label={ENTERPRISE_KEY_LABEL}
/>
}
ref={inputEnterpriseKeyRef}
/>
),
},
}}
spacing={GRID_SPACING}
wrapperBoxProps={{
sx: { display: isShowOptionalSection ? undefined : 'none' },
}}
/>
),
[
isShowOptionalSection,
setEnterpriseKeyInputMessage,
setHostNameInputMessage,
testInput,
],
);
const redhatSection = useMemo(
() => (
<Grid
columns={GRID_COLUMNS}
layout={{
'preparehost-redhat-user': {
children: (
<InputWithRef
input={
<OutlinedInputWithLabel
formControlProps={{ sx: { width: '100%' } }}
id="preparehost-redhat-user-input"
inputProps={{
onBlur: ({ target: { value } }) => {
if (value) {
const valid = testInput({
inputs: { [IT_IDS.redhatUser]: { value } },
});
setIsInputRedhatUserValid(valid);
}
},
onFocus: () => {
setRedhatUserInputMessage();
},
}}
label={REDHAT_USER_LABEL}
/>
}
ref={inputRedhatUser}
/>
),
},
'preparehost-redhat-password': {
children: (
<InputWithRef
input={
<OutlinedInputWithLabel
formControlProps={{ sx: { width: '100%' } }}
id="preparehost-redhat-password-input"
inputProps={{
onBlur: ({ target: { value } }) => {
if (value) {
const valid = testInput({
inputs: { [IT_IDS.redhatPassword]: { value } },
});
setIsInputRedhatPasswordValid(valid);
}
},
onFocus: () => {
setRedhatPasswordInputMessage();
},
onPasswordVisibilityAppend: (type) => {
setIsShowRedhatPassword(type !== INPUT_TYPES.password);
},
type: INPUT_TYPES.password,
}}
label={REDHAT_PASSWORD_LABEL}
/>
}
ref={inputRedhatPassword}
/>
),
},
}}
spacing={GRID_SPACING}
wrapperBoxProps={{
sx: { display: redhatElementSxDisplay },
}}
/>
),
[
redhatElementSxDisplay,
setRedhatPasswordInputMessage,
setRedhatUserInputMessage,
testInput,
],
);
const messageSection = useMemo(
() => (
<MUIBox sx={{ display: isShowOptionalSection ? undefined : 'none' }}>
<MessageGroup count={1} ref={messageGroupRef} />
</MUIBox>
),
[isShowOptionalSection],
);
const submitSection = useMemo(
() =>
isSubmittingPrepareHost ? (
<Spinner mt={0} />
) : (
<FlexBox
row
sx={{
display: isShowOptionalSection ? 'flex' : 'none',
justifyContent: 'flex-end',
}}
>
<ContainedButton
disabled={
!isInputHostNameValid ||
!isInputEnterpriseKeyValid ||
!isInputRedhatUserValid ||
!isInputRedhatPasswordValid
}
onClick={() => {
const redhatPasswordInputValue =
inputRedhatPassword.current.getValue?.call(null);
setConfirmValues({
enterpriseKey:
inputEnterpriseKeyRef.current.getValue?.call(null) ||
'none; using community version',
hostName: inputHostNameRef.current.getValue?.call(null) || '',
redhatPassword: redhatPasswordInputValue || 'none',
redhatPasswordHidden:
redhatPasswordInputValue?.replace(/./g, '*') || 'none',
redhatUser:
inputRedhatUser.current.getValue?.call(null) || 'none',
});
setSubmitPrepareHostMessage();
confirmDialogRef.current.setOpen?.call(null, true);
}}
>
Prepare host
</ContainedButton>
</FlexBox>
),
[
isInputEnterpriseKeyValid,
isInputHostNameValid,
isInputRedhatPasswordValid,
isInputRedhatUserValid,
isShowOptionalSection,
isSubmittingPrepareHost,
setSubmitPrepareHostMessage,
],
);
return (
<>
<Panel>
<PanelHeader>
<HeaderText>Prepare a host to include in Anvil!</HeaderText>
</PanelHeader>
<FlexBox>
<RadioGroupWithLabel
id="preparehost-host-type"
label="Host type"
onChange={(event, value) => {
setInputHostType(value);
setIsShowAccessSection(true);
}}
radioItems={{
node: { label: 'Subnode', value: 'node' },
dr: { label: 'Disaster Recovery (DR) host', value: 'dr' },
}}
/>
{accessSection}
{optionalSection}
{redhatSection}
{messageSection}
{submitSection}
</FlexBox>
</Panel>
<ConfirmDialog
actionProceedText="Prepare"
closeOnProceed
content={
<Grid
calculateItemBreakpoints={(index) => ({
xs: index % 2 === 0 ? 1 : 2,
})}
columns={3}
layout={{
'preparehost-confirm-host-type-label': {
children: <BodyText>Host type</BodyText>,
},
'preparehost-confirm-host-type-value': {
children: (
<MonoText>
{inputHostType === 'dr'
? 'Disaster Recovery (DR)'
: 'Subnode'}
</MonoText>
),
},
'preparehost-confirm-host-name-label': {
children: <BodyText>Host name</BodyText>,
},
'preparehost-confirm-host-name-value': {
children: <MonoText>{confirmValues?.hostName}</MonoText>,
},
'preparehost-confirm-enterprise-key-label': {
children: <BodyText>Alteeve enterprise key</BodyText>,
},
'preparehost-confirm-enterprise-key-value': {
children: <MonoText>{confirmValues?.enterpriseKey}</MonoText>,
},
'preparehost-confirm-redhat-user-label': {
children: <BodyText>RedHat user</BodyText>,
sx: { display: redhatElementSxDisplay },
},
'preparehost-confirm-redhat-user-value': {
children: <MonoText>{confirmValues?.redhatUser}</MonoText>,
sx: { display: redhatElementSxDisplay },
},
'preparehost-confirm-redhat-password-label': {
children: <BodyText>RedHat password</BodyText>,
sx: { display: redhatElementSxDisplay },
},
'preparehost-confirm-redhat-password-value': {
children: (
<FlexBox
row
sx={{
height: '100%',
maxWidth: '100%',
}}
>
<MonoText
sx={{
flexGrow: 1,
maxWidth: 'calc(100% - 3em)',
overflowX: 'scroll',
}}
>
{isShowRedhatPassword
? confirmValues?.redhatPassword
: confirmValues?.redhatPasswordHidden}
</MonoText>
<MUIIconButton
onClick={() => {
setIsShowRedhatPassword((previous) => !previous);
}}
sx={{ color: GREY, padding: 0 }}
>
{isShowRedhatPassword ? (
<MUIVisibilityOffIcon />
) : (
<MUIVisibilityIcon />
)}
</MUIIconButton>
</FlexBox>
),
sx: { display: redhatElementSxDisplay },
},
}}
spacing=".6em"
/>
}
onCancelAppend={() => {
setIsShowRedhatPassword(false);
}}
onProceedAppend={() => {
setIsSubmittingPrepareHost(true);
api
.put('/host/prepare', {
enterpriseUUID:
inputEnterpriseKeyRef.current.getValue?.call(null),
hostIPAddress: connectedHostIPAddress,
hostName: inputHostNameRef.current.getValue?.call(null),
hostPassword: connectedHostPassword,
hostType: inputHostType,
hostUUID: connectedHostUUID,
redhatPassword: inputRedhatPassword.current.getValue?.call(null),
redhatUser: inputRedhatUser.current.getValue?.call(null),
})
.then(() => {
setSubmitPrepareHostMessage({
children: `Successfully initiated prepare host.`,
});
setTimeout(() => {
setSubmitPrepareHostMessage();
}, SUCCESS_MESSAGE_TIMEOUT);
})
.catch((error) => {
const errorMessage = handleAPIError(error, {
onResponseErrorAppend: ({ status }) => {
let result: Message | undefined;
if (status === 400) {
result = {
children: `The API found invalid values. Did you forget to fill in one of the RedHat fields?`,
type: 'warning',
};
}
return result;
},
});
setSubmitPrepareHostMessage(errorMessage);
})
.finally(() => {
setIsSubmittingPrepareHost(false);
});
}}
ref={confirmDialogRef}
titleText="Confirm host preparation"
/>
</>
);
};
export default PrepareHostForm;

@ -18,10 +18,12 @@ const RadioGroupWithLabel: FC<RadioGroupWithLabelProps> = ({
formLabelProps, formLabelProps,
id, id,
label, label,
name,
onChange: onRadioGroupChange, onChange: onRadioGroupChange,
radioItems, radioItems,
radioProps: { sx: radioSx, ...restRadioProps } = {}, radioProps: { sx: radioSx, ...restRadioProps } = {},
radioGroupProps, radioGroupProps,
value,
}) => { }) => {
const labelElement = useMemo( const labelElement = useMemo(
() => (typeof label === 'string' ? <BodyText>{label}</BodyText> : label), () => (typeof label === 'string' ? <BodyText>{label}</BodyText> : label),
@ -30,7 +32,7 @@ const RadioGroupWithLabel: FC<RadioGroupWithLabelProps> = ({
const itemElements = useMemo(() => { const itemElements = useMemo(() => {
const items = Object.entries(radioItems); const items = Object.entries(radioItems);
return items.map(([itemId, { label: itemLabel, value }]) => { return items.map(([itemId, { label: itemLabel, value: itemValue }]) => {
const itemLabelElement = const itemLabelElement =
typeof itemLabel === 'string' ? ( typeof itemLabel === 'string' ? (
<BodyText>{itemLabel}</BodyText> <BodyText>{itemLabel}</BodyText>
@ -53,7 +55,7 @@ const RadioGroupWithLabel: FC<RadioGroupWithLabelProps> = ({
/> />
} }
key={`${id}-${itemId}`} key={`${id}-${itemId}`}
value={value} value={itemValue}
label={itemLabelElement} label={itemLabelElement}
{...formControlLabelProps} {...formControlLabelProps}
/> />
@ -64,7 +66,14 @@ const RadioGroupWithLabel: FC<RadioGroupWithLabelProps> = ({
return ( return (
<MUIFormControl {...formControlProps}> <MUIFormControl {...formControlProps}>
<MUIFormLabel {...formLabelProps}>{labelElement}</MUIFormLabel> <MUIFormLabel {...formLabelProps}>{labelElement}</MUIFormLabel>
<MUIRadioGroup onChange={onRadioGroupChange} row {...radioGroupProps}> <MUIRadioGroup
id={id}
name={name}
onChange={onRadioGroupChange}
row
value={value}
{...radioGroupProps}
>
{itemElements} {itemElements}
</MUIRadioGroup> </MUIRadioGroup>
</MUIFormControl> </MUIFormControl>

@ -1,5 +1,5 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import useSWR, { BareFetcher, SWRConfiguration } from 'swr'; import useSWR, { BareFetcher, KeyedMutator, SWRConfiguration } from 'swr';
import API_BASE_URL from '../lib/consts/API_BASE_URL'; import API_BASE_URL from '../lib/consts/API_BASE_URL';
@ -8,6 +8,7 @@ import fetchJSON from '../lib/fetchers/fetchJSON';
type FetchHookResponse<D, E extends Error = Error> = { type FetchHookResponse<D, E extends Error = Error> = {
data?: D; data?: D;
error?: E; error?: E;
mutate: KeyedMutator<D>;
loading: boolean; loading: boolean;
}; };
@ -26,7 +27,11 @@ const useFetch = <Data, Alt = Data>(
...config ...config
} = options; } = options;
const { data, error } = useSWR<Data>(`${baseUrl}${url}`, fetcher, config); const { data, error, mutate } = useSWR<Data>(
`${baseUrl}${url}`,
fetcher,
config,
);
const altData = useMemo<Alt | undefined>( const altData = useMemo<Alt | undefined>(
() => mod && data && mod(data), () => mod && data && mod(data),
@ -35,7 +40,7 @@ const useFetch = <Data, Alt = Data>(
const loading = !error && !data; const loading = !error && !data;
return { altData, data, error, loading }; return { altData, data, error, mutate, loading };
}; };
export default useFetch; export default useFetch;

@ -1,4 +1,3 @@
import { OutlinedInputProps } from '@mui/material';
import { FormikConfig, FormikValues, useFormik } from 'formik'; import { FormikConfig, FormikValues, useFormik } from 'formik';
import { isEqual, isObject } from 'lodash'; import { isEqual, isObject } from 'lodash';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
@ -41,17 +40,6 @@ const useFormikUtils = <Values extends FormikValues = FormikValues>(
[formik.initialValues, formik.values], [formik.initialValues, formik.values],
); );
const disableAutocomplete = useCallback(
(overwrite?: Partial<OutlinedInputProps>): OutlinedInputProps => ({
readOnly: true,
onFocus: (event) => {
event.target.readOnly = false;
},
...overwrite,
}),
[],
);
const debounceHandleChange = useMemo( const debounceHandleChange = useMemo(
() => debounce(formik.handleChange), () => debounce(formik.handleChange),
[formik.handleChange], [formik.handleChange],
@ -75,7 +63,6 @@ const useFormikUtils = <Values extends FormikValues = FormikValues>(
); );
return { return {
disableAutocomplete,
disabledSubmit, disabledSubmit,
formik, formik,
formikErrors, formikErrors,

@ -0,0 +1,9 @@
import { capitalize } from 'lodash';
const disassembleCamel = (value: string) => {
const spaced = value.replace(/([a-z\d])([A-Z])/g, '$1 $2');
return capitalize(spaced);
};
export default disassembleCamel;

@ -1,4 +1,4 @@
import { capitalize } from 'lodash'; import disassembleCamel from './disassembleCamel';
const getFormikErrorMessages = ( const getFormikErrorMessages = (
errors: object, errors: object,
@ -7,7 +7,11 @@ const getFormikErrorMessages = (
let children = error; let children = error;
if (typeof children === 'string') { if (typeof children === 'string') {
children = capitalize(children.replace(/^[^\s]+\.([^.]+)/, '$1')); const [first, ...rest] = children.split(/\s+/);
const name = disassembleCamel(first.replace(/^[^\s]+\.([^.]+)/, '$1'));
children = [name, ...rest].join(' ');
} }
return { children, type: 'warning' }; return { children, type: 'warning' };

@ -1 +0,0 @@
self.__BUILD_MANIFEST=function(s,c,a,t,e,i,n,f,b,u,k,h,j,d,g,r,l,_,o,m){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":[s,a,t,i,n,f,u,"static/chunks/6-dbef3ba2a090cb05.js",c,e,b,k,g,r,"static/chunks/pages/index-6febd0ab3b8c828c.js"],"/_error":["static/chunks/pages/_error-a9572f84d60f21da.js"],"/anvil":[s,a,t,i,n,f,u,"static/chunks/924-2a2fdb45d3e02493.js",c,e,b,k,g,"static/chunks/pages/anvil-38307a04a51f8094.js"],"/config":[s,a,t,n,h,d,c,e,j,"static/chunks/pages/config-1c39d13147dfe819.js"],"/file-manager":[s,a,t,i,f,h,l,"static/chunks/486-1480d7483e28c6f3.js",c,e,b,_,"static/chunks/pages/file-manager-c8a2ce2c02dc39fc.js"],"/init":[s,a,i,n,f,u,d,o,c,e,b,j,m,"static/chunks/pages/init-210f96453904f447.js"],"/login":[s,a,t,n,c,e,j,"static/chunks/pages/login-5fd1d7a2717b59af.js"],"/mail-config":[s,a,t,i,n,f,u,h,l,c,e,b,k,_,"static/chunks/pages/mail-config-14cdc2dd46514057.js"],"/manage-element":[s,a,t,i,n,f,u,h,d,o,"static/chunks/569-fa9b9ac8a7639d2d.js",c,e,b,k,j,m,"static/chunks/pages/manage-element-766bd9ef38ccbfa4.js"],"/server":[s,t,i,c,r,"static/chunks/pages/server-d81577dd0b817ba2.js"],sortedPages:["/","/_app","/_error","/anvil","/config","/file-manager","/init","/login","/mail-config","/manage-element","/server"]}}("static/chunks/494-413067ecdf8ec8f0.js","static/chunks/775-3f1c58f77437bd5d.js","static/chunks/804-a6d43595270ed0d2.js","static/chunks/416-b31c470a96d10e58.js","static/chunks/675-235890fb4812bd16.js","static/chunks/50-af452066db73e3df.js","static/chunks/263-5784adae0d1d8513.js","static/chunks/213-67c4f0768a44e039.js","static/chunks/633-900b9341a6a3bc53.js","static/chunks/310-4edb13985847ab25.js","static/chunks/733-a945bbb3c5f55f74.js","static/chunks/461-c4e18a515455805e.js","static/chunks/556-dbf62d8622405edc.js","static/chunks/203-ea1ab9b7c3c7694b.js","static/chunks/750-b9b6c5fdabc264a0.js","static/chunks/302-6490e226661e8e00.js","static/chunks/264-1be1a496ee1255c6.js","static/chunks/380-0eff6addb79bd61f.js","static/chunks/197-c291e38a27168218.js","static/chunks/270-56592f453c639f63.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,d,k,h,j,g,r,l,_,o,m){return{__rewrites:{afterFiles:[],beforeFiles:[],fallback:[]},"/":[s,a,e,i,n,f,u,"static/chunks/6-dbef3ba2a090cb05.js",c,t,b,d,r,l,"static/chunks/pages/index-0e23f6af1e089a97.js"],"/_error":["static/chunks/pages/_error-a9572f84d60f21da.js"],"/anvil":[s,a,e,i,n,f,u,"static/chunks/924-2a2fdb45d3e02493.js",c,t,b,d,r,"static/chunks/pages/anvil-38307a04a51f8094.js"],"/config":[s,a,e,n,k,j,c,t,h,"static/chunks/pages/config-144ed56943e89e8c.js"],"/file-manager":[s,a,e,i,f,k,g,"static/chunks/486-45903b907cd7ece3.js",c,t,b,"static/chunks/pages/file-manager-8a23bb0baebac7f6.js"],"/init":[s,a,i,n,f,u,j,_,c,t,b,h,o,"static/chunks/pages/init-210f96453904f447.js"],"/login":[s,a,e,n,c,t,h,"static/chunks/pages/login-1d03dfaf2cb572f8.js"],"/mail-config":[s,a,e,i,n,f,u,k,g,c,t,b,d,m,"static/chunks/pages/mail-config-77c70d16ef879e90.js"],"/manage-element":[s,a,e,i,n,f,u,k,g,j,_,"static/chunks/814-6420b976d086fe20.js",c,t,b,d,h,o,m,"static/chunks/pages/manage-element-7ac129e45d98ff58.js"],"/server":[s,e,i,c,l,"static/chunks/pages/server-d81577dd0b817ba2.js"],sortedPages:["/","/_app","/_error","/anvil","/config","/file-manager","/init","/login","/mail-config","/manage-element","/server"]}}("static/chunks/494-413067ecdf8ec8f0.js","static/chunks/775-3f1c58f77437bd5d.js","static/chunks/804-a6d43595270ed0d2.js","static/chunks/416-b31c470a96d10e58.js","static/chunks/675-9a50fb0ae255b835.js","static/chunks/50-af452066db73e3df.js","static/chunks/263-5784adae0d1d8513.js","static/chunks/213-67c4f0768a44e039.js","static/chunks/633-900b9341a6a3bc53.js","static/chunks/310-4edb13985847ab25.js","static/chunks/733-a945bbb3c5f55f74.js","static/chunks/461-c4e18a515455805e.js","static/chunks/556-0c0cace3593e5307.js","static/chunks/203-ea1ab9b7c3c7694b.js","static/chunks/264-683b93ad6e70a8fb.js","static/chunks/750-9f873f4e10dbcacd.js","static/chunks/302-6490e226661e8e00.js","static/chunks/197-c291e38a27168218.js","static/chunks/270-9058c1049a825f7d.js","static/chunks/17-0593dc8fdd9c512e.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

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

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

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

@ -8,15 +8,16 @@ import Grid from '../../components/Grid';
import handleAPIError from '../../lib/handleAPIError'; import handleAPIError from '../../lib/handleAPIError';
import Header from '../../components/Header'; import Header from '../../components/Header';
import ManageFencePanel from '../../components/ManageFence'; import ManageFencePanel from '../../components/ManageFence';
import { ManageHost } from '../../components/ManageHost';
import ManageManifestPanel from '../../components/ManageManifest'; import ManageManifestPanel from '../../components/ManageManifest';
import ManageUpsPanel from '../../components/ManageUps'; import ManageUpsPanel from '../../components/ManageUps';
import { Panel } from '../../components/Panels'; import { Panel, PanelHeader } from '../../components/Panels';
import PrepareHostForm from '../../components/PrepareHostForm';
import PrepareNetworkForm from '../../components/PrepareNetworkForm'; import PrepareNetworkForm from '../../components/PrepareNetworkForm';
import Spinner from '../../components/Spinner'; import Spinner from '../../components/Spinner';
import Tab from '../../components/Tab'; import Tab from '../../components/Tab';
import TabContent from '../../components/TabContent'; import TabContent from '../../components/TabContent';
import Tabs from '../../components/Tabs'; import Tabs from '../../components/Tabs';
import { HeaderText } from '../../components/Text';
import useIsFirstRender from '../../hooks/useIsFirstRender'; import useIsFirstRender from '../../hooks/useIsFirstRender';
const TAB_ID_PREPARE_HOST = 'prepare-host'; const TAB_ID_PREPARE_HOST = 'prepare-host';
@ -42,7 +43,14 @@ const PrepareHostTabContent: FC = () => (
layout={{ layout={{
'preparehost-left-column': {}, 'preparehost-left-column': {},
'preparehost-center-column': { 'preparehost-center-column': {
children: <PrepareHostForm />, children: (
<Panel>
<PanelHeader>
<HeaderText>Hosts</HeaderText>
</PanelHeader>
<ManageHost />
</Panel>
),
...STEP_CONTENT_GRID_CENTER_COLUMN, ...STEP_CONTENT_GRID_CENTER_COLUMN,
}, },
}} }}

@ -24,6 +24,7 @@ type DeletePromiseChainGetter<T> = (
) => Promise<T>[]; ) => Promise<T>[];
type CrudListOptionalProps<Overview> = { type CrudListOptionalProps<Overview> = {
entryUrlPrefix?: string;
getAddLoading?: (previous?: boolean) => boolean; getAddLoading?: (previous?: boolean) => boolean;
getDeletePromiseChain?: <T>( getDeletePromiseChain?: <T>(
base: DeletePromiseChainGetter<T>, base: DeletePromiseChainGetter<T>,

@ -40,6 +40,8 @@ type RenderFormEntryFunction = (
}, },
) => import('react').ReactElement; ) => import('react').ReactElement;
type SkipFormEntryFunction = (args: CommonFormEntryHandlerArgs) => boolean;
type FormSummaryOptionalProps = { type FormSummaryOptionalProps = {
getEntryLabel?: GetFormEntryLabelFunction; getEntryLabel?: GetFormEntryLabelFunction;
getListProps?: GetFormEntriesPropsFunction; getListProps?: GetFormEntriesPropsFunction;
@ -48,6 +50,10 @@ type FormSummaryOptionalProps = {
maxDepth?: number; maxDepth?: number;
renderEntry?: RenderFormEntryFunction; renderEntry?: RenderFormEntryFunction;
renderEntryValue?: RenderFormValueFunction; renderEntryValue?: RenderFormValueFunction;
skip?: (
base: SkipFormEntryFunction,
...args: Parameters<SkipFormEntryFunction>
) => ReturnType<SkipFormEntryFunction>;
}; };
type FormSummaryProps<T extends FormEntries> = FormSummaryOptionalProps & { type FormSummaryProps<T extends FormEntries> = FormSummaryOptionalProps & {

@ -12,11 +12,6 @@ type FormikSubmitHandler<Values extends FormikValues> =
import('formik').FormikConfig<Values>['onSubmit']; import('formik').FormikConfig<Values>['onSubmit'];
type FormikUtils<Values extends FormikValues> = { type FormikUtils<Values extends FormikValues> = {
disableAutocomplete: (
overwrite?: Partial<
import('../components/OutlinedInput').OutlinedInputProps
>,
) => import('../components/OutlinedInput').OutlinedInputProps;
disabledSubmit: boolean; disabledSubmit: boolean;
formik: Formik<Values>; formik: Formik<Values>;
formikErrors: Messages; formikErrors: Messages;

@ -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<InquireHostResponse | undefined>
>;
};
/** 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;
};

@ -3,18 +3,23 @@ type RadioItem<RadioItemValue> = {
value: RadioItemValue; value: RadioItemValue;
}; };
type RadioItemList<Value = string> = Record<string, RadioItem<Value>>;
type RadioGroupWithLabelOptionalProps = { type RadioGroupWithLabelOptionalProps = {
formControlProps?: import('@mui/material').FormControlProps; formControlProps?: import('@mui/material').FormControlProps;
formControlLabelProps?: import('@mui/material').FormControlLabelProps; formControlLabelProps?: import('@mui/material').FormControlLabelProps;
formLabelProps?: import('@mui/material').FormLabelProps; formLabelProps?: import('@mui/material').FormLabelProps;
label?: import('react').ReactNode; label?: import('react').ReactNode;
onChange?: import('@mui/material').RadioGroupProps['onChange'];
radioProps?: import('@mui/material').RadioProps; radioProps?: import('@mui/material').RadioProps;
radioGroupProps?: import('@mui/material').RadioGroupProps; radioGroupProps?: import('@mui/material').RadioGroupProps;
}; };
type RadioGroupWithLabelProps<RadioItemValue = string> = type RadioGroupWithLabelProps<RadioItemValue = string> =
RadioGroupWithLabelOptionalProps & { RadioGroupWithLabelOptionalProps &
Pick<
import('@mui/material').RadioGroupProps,
'name' | 'onChange' | 'value'
> & {
id: string; id: string;
radioItems: { [id: string]: RadioItem<RadioItemValue> }; radioItems: RadioItemList<RadioItemValue>;
}; };

Loading…
Cancel
Save