Local modifications to ClusterLabs/Anvil by Alteeve
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

685 lines
22 KiB

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';
import useProtect from '../hooks/useProtect';
import useProtectedState from '../hooks/useProtectedState';
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 { protect } = useProtect();
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] = useProtectedState<
string | undefined
>(undefined, protect);
const [connectedHostUUID, setConnectedHostUUID] = useProtectedState<string>(
'',
protect,
);
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
formContainer={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={(
{ getValue: getIdentifier },
{ getValue: getPassphrase },
setMessage,
setIsSubmitting,
) => {
const identifierValue = getIdentifier?.call(null);
const passphraseValue = getPassphrase?.call(null);
api
.put<{
hostName: string;
hostOS: string;
hostUUID: string;
isConnected: boolean;
isInetConnected: boolean;
isOSRegistered: boolean;
}>('/command/inquire-host', {
ipAddress: identifierValue,
password: passphraseValue,
})
.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(identifierValue);
setConnectedHostPassword(passphraseValue);
setConnectedHostUUID(hostUUID);
setIsShowAccessSubmit(false);
setIsShowOptionalSection(true);
} else {
setMessage?.call(null, {
children: `Failed to establish a connection with the given host credentials.`,
type: 'error',
});
}
},
)
.catch((apiError) => {
const emsg = handleAPIError(apiError);
setMessage?.call(null, emsg);
})
.finally(() => {
setIsSubmitting(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: 'Node', 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)' : 'Node'}
</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;