Wizard: add AAP step
This commit is contained in:
parent
11e352440f
commit
04adcc133c
16 changed files with 776 additions and 14 deletions
214
playwright/Customizations/AAP.spec.ts
Normal file
214
playwright/Customizations/AAP.spec.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
import { expect } from '@playwright/test';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { test } from '../fixtures/customizations';
|
||||
import { isHosted } from '../helpers/helpers';
|
||||
import { ensureAuthenticated } from '../helpers/login';
|
||||
import {
|
||||
ibFrame,
|
||||
navigateToLandingPage,
|
||||
navigateToOptionalSteps,
|
||||
} from '../helpers/navHelpers';
|
||||
import {
|
||||
createBlueprint,
|
||||
deleteBlueprint,
|
||||
exportBlueprint,
|
||||
fillInDetails,
|
||||
fillInImageOutputGuest,
|
||||
importBlueprint,
|
||||
registerLater,
|
||||
} from '../helpers/wizardHelpers';
|
||||
|
||||
const validCallbackUrl =
|
||||
'https://controller.url/api/controller/v2/job_templates/9/callback/';
|
||||
const validHttpCallbackUrl =
|
||||
'http://controller.url/api/controller/v2/job_templates/9/callback/';
|
||||
const validHostConfigKey = 'hostconfigkey';
|
||||
const validCertificate = `-----BEGIN CERTIFICATE-----
|
||||
MIIDXTCCAkWgAwIBAgIJAOEzx5ezZ9EIMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
|
||||
BAYTAklOMQswCQYDVQQIDAJLUjEMMAoGA1UEBwwDS1JHMRAwDgYDVQQKDAdUZXN0
|
||||
IENBMB4XDTI1MDUxNTEyMDAwMFoXDTI2MDUxNTEyMDAwMFowRTELMAkGA1UEBhMC
|
||||
SU4xCzAJBgNVBAgMAktSMQwwCgYDVQQHDANSR0sxEDAOBgNVBAoMB1Rlc3QgQ0Ew
|
||||
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+R4gfN5pyJQo5qBTTtN+7
|
||||
eE9CSXZJ8SVVaE3U54IgqQoqsSoBY5QtExy7v5C6l6mW4E6dzK/JecmvTTO/BvlG
|
||||
A5k2hxB6bOQxtxYwfgElH+RFWN9P4xxhtEiQgHoG1rDfnXuDJk1U3YEkCQELUebz
|
||||
fF3EIDU1yR0Sz2bA+Sl2VXe8og1MEZfytq8VZUVltxtn2PfW7zI5gOllBR2sKeUc
|
||||
K6h8HXN7qMgfEvsLIXxTw7fU/zA3ibcxfRCl3m6QhF8hwRh6F9Wtz2s8hCzGegV5
|
||||
z0M39nY7X8C3GZQ4Ly8v8DdY+FbEix7K3SSBRbWtdPfAHRFlX9Er2Wf8DAr7O2hH
|
||||
AgMBAAGjUDBOMB0GA1UdDgQWBBTXXz2eIDgK+BhzDUAGzptn0OMcpDAfBgNVHSME
|
||||
GDAWgBTXXz2eIDgK+BhzDUAGzptn0OMcpDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3
|
||||
DQEBCwUAA4IBAQAoUgY4jsuBMB3el9cc7JS2rcOhhJzn47Hj2UANfJq52g5lbjo7
|
||||
XDc7Wb3VDcV+1LzjdzayT1qO1WzHb6FDPW9L9f6h4s8lj6MvJ+xhOWgD11srdIt3
|
||||
vbQaQW4zDfeVRcKXzqbcUX8BLXAdzJPqVwZ+Z4EDjYrJ7lF9k+IqfZm0MsYX7el9
|
||||
kvdRHbLuF4Q0sZ05CXMFkhM0Ulhu4MZ+1FcsQa7nWfZzTmbjHOuWJPB4z5WwrB7z
|
||||
U8YYvWJ3qxToWGbATqJxkRKGGqLrNrmwcfzgPqkpuCRYi0Kky6gJ1RvL+DRopY9x
|
||||
uD+ckf3oH2wYAB6RpPRMkfVxe7lGMvq/yEZ6
|
||||
-----END CERTIFICATE-----`;
|
||||
const invalidCertificate = `-----BEGIN CERTIFICATE-----
|
||||
ThisIs*Not+Valid/Base64==
|
||||
-----END CERTIFICATE-----`;
|
||||
|
||||
test('Create a blueprint with AAP registration customization', async ({
|
||||
page,
|
||||
cleanup,
|
||||
}) => {
|
||||
const blueprintName = 'test-' + uuidv4();
|
||||
|
||||
// Skip entirely in Cockpit/on-premise where AAP customization is unavailable
|
||||
test.skip(!isHosted(), 'AAP customization is not available in the plugin');
|
||||
|
||||
// Delete the blueprint after the run fixture
|
||||
await cleanup.add(() => deleteBlueprint(page, blueprintName));
|
||||
await ensureAuthenticated(page);
|
||||
|
||||
// Navigate to IB landing page and get the frame
|
||||
await navigateToLandingPage(page);
|
||||
const frame = await ibFrame(page);
|
||||
|
||||
await test.step('Navigate to optional steps in Wizard', async () => {
|
||||
await navigateToOptionalSteps(frame);
|
||||
await registerLater(frame);
|
||||
});
|
||||
|
||||
await test.step('Select and fill the AAP step with valid configuration', async () => {
|
||||
await frame
|
||||
.getByRole('button', { name: 'Ansible Automation Platform' })
|
||||
.click();
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'ansible callback url' })
|
||||
.fill(validCallbackUrl);
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'host config key' })
|
||||
.fill(validHostConfigKey);
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'File upload' })
|
||||
.fill(validCertificate);
|
||||
await expect(frame.getByRole('button', { name: 'Next' })).toBeEnabled();
|
||||
});
|
||||
|
||||
await test.step('Test TLS confirmation checkbox for HTTPS URLs', async () => {
|
||||
// TLS confirmation checkbox should appear for HTTPS URLs
|
||||
await expect(
|
||||
frame.getByRole('checkbox', {
|
||||
name: 'Insecure',
|
||||
}),
|
||||
).toBeVisible();
|
||||
|
||||
// Check TLS confirmation and verify CA input is hidden
|
||||
await frame
|
||||
.getByRole('checkbox', {
|
||||
name: 'Insecure',
|
||||
})
|
||||
.check();
|
||||
await expect(
|
||||
frame.getByRole('textbox', { name: 'File upload' }),
|
||||
).toBeHidden();
|
||||
|
||||
await frame
|
||||
.getByRole('checkbox', {
|
||||
name: 'Insecure',
|
||||
})
|
||||
.uncheck();
|
||||
|
||||
await expect(
|
||||
frame.getByRole('textbox', { name: 'File upload' }),
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Test certificate validation', async () => {
|
||||
await frame.getByRole('textbox', { name: 'File upload' }).clear();
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'File upload' })
|
||||
.fill(invalidCertificate);
|
||||
await expect(frame.getByText(/Certificate.*is not valid/)).toBeVisible();
|
||||
|
||||
await frame.getByRole('textbox', { name: 'File upload' }).clear();
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'File upload' })
|
||||
.fill(validCertificate);
|
||||
|
||||
await expect(frame.getByText('Certificate was uploaded')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step('Test HTTP URL behavior', async () => {
|
||||
await frame.getByRole('textbox', { name: 'ansible callback url' }).clear();
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'ansible callback url' })
|
||||
.fill(validHttpCallbackUrl);
|
||||
|
||||
// TLS confirmation checkbox should NOT appear for HTTP URLs
|
||||
await expect(
|
||||
frame.getByRole('checkbox', {
|
||||
name: 'Insecure',
|
||||
}),
|
||||
).toBeHidden();
|
||||
await expect(
|
||||
frame.getByRole('textbox', { name: 'File upload' }),
|
||||
).toBeVisible();
|
||||
|
||||
await frame.getByRole('textbox', { name: 'ansible callback url' }).clear();
|
||||
await frame
|
||||
.getByRole('textbox', { name: 'ansible callback url' })
|
||||
.fill(validCallbackUrl);
|
||||
});
|
||||
|
||||
await test.step('Complete AAP configuration and proceed to review', async () => {
|
||||
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
||||
});
|
||||
|
||||
await test.step('Fill the BP details', async () => {
|
||||
await fillInDetails(frame, blueprintName);
|
||||
});
|
||||
|
||||
await test.step('Create BP', async () => {
|
||||
await createBlueprint(frame, blueprintName);
|
||||
});
|
||||
|
||||
await test.step('Edit BP and verify AAP configuration persists', async () => {
|
||||
await frame.getByRole('button', { name: 'Edit blueprint' }).click();
|
||||
await frame.getByLabel('Revisit Ansible Automation Platform step').click();
|
||||
|
||||
await expect(
|
||||
frame.getByRole('textbox', { name: 'ansible callback url' }),
|
||||
).toHaveValue(validCallbackUrl);
|
||||
await expect(
|
||||
frame.getByRole('textbox', { name: 'host config key' }),
|
||||
).toHaveValue(validHostConfigKey);
|
||||
await expect(
|
||||
frame.getByRole('textbox', { name: 'File upload' }),
|
||||
).toHaveValue(validCertificate);
|
||||
|
||||
await frame.getByRole('button', { name: 'Review and finish' }).click();
|
||||
await frame
|
||||
.getByRole('button', { name: 'Save changes to blueprint' })
|
||||
.click();
|
||||
});
|
||||
// This is for hosted service only as these features are not available in cockpit plugin
|
||||
await test.step('Export BP', async (step) => {
|
||||
step.skip(!isHosted(), 'Exporting is not available in the plugin');
|
||||
await exportBlueprint(page, blueprintName);
|
||||
});
|
||||
|
||||
await test.step('Import BP', async (step) => {
|
||||
step.skip(!isHosted(), 'Importing is not available in the plugin');
|
||||
await importBlueprint(page, blueprintName);
|
||||
});
|
||||
|
||||
await test.step('Review imported BP', async (step) => {
|
||||
step.skip(!isHosted(), 'Importing is not available in the plugin');
|
||||
await fillInImageOutputGuest(page);
|
||||
await page
|
||||
.getByRole('button', { name: 'Ansible Automation Platform' })
|
||||
.click();
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'ansible callback url' }),
|
||||
).toHaveValue(validCallbackUrl);
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'host config key' }),
|
||||
).toBeEmpty();
|
||||
await expect(
|
||||
page.getByRole('textbox', { name: 'File upload' }),
|
||||
).toHaveValue(validCertificate);
|
||||
await page.getByRole('button', { name: 'Cancel' }).click();
|
||||
});
|
||||
});
|
||||
|
|
@ -72,6 +72,11 @@ test.describe.serial('test', () => {
|
|||
frame.getByRole('heading', { name: 'Systemd services' });
|
||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
|
||||
if (isHosted()) {
|
||||
frame.getByRole('heading', { name: 'Ansible Automation Platform' });
|
||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
}
|
||||
|
||||
if (isHosted()) {
|
||||
frame.getByRole('heading', { name: 'First boot configuration' });
|
||||
await frame.getByRole('button', { name: 'Next', exact: true }).click();
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { WizardStepType } from '@patternfly/react-core/dist/esm/components/Wizar
|
|||
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import AAPStep from './steps/AAP';
|
||||
import DetailsStep from './steps/Details';
|
||||
import FileSystemStep from './steps/FileSystem';
|
||||
import { FileSystemContext } from './steps/FileSystem/components/FileSystemTable';
|
||||
|
|
@ -40,6 +41,7 @@ import UsersStep from './steps/Users';
|
|||
import { getHostArch, getHostDistro } from './utilities/getHostInfo';
|
||||
import { useHasSpecificTargetOnly } from './utilities/hasSpecificTargetOnly';
|
||||
import {
|
||||
useAAPValidation,
|
||||
useDetailsValidation,
|
||||
useFilesystemValidation,
|
||||
useFirewallValidation,
|
||||
|
|
@ -197,6 +199,7 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
|
||||
// Feature flags
|
||||
const complianceEnabled = useFlag('image-builder.compliance.enabled');
|
||||
const isAAPRegistrationEnabled = useFlag('image-builder.aap.enabled');
|
||||
|
||||
// IMPORTANT: Ensure the wizard starts with a fresh initial state
|
||||
useEffect(() => {
|
||||
|
|
@ -283,6 +286,8 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
const firewallValidation = useFirewallValidation();
|
||||
// Services
|
||||
const servicesValidation = useServicesValidation();
|
||||
// AAP
|
||||
const aapValidation = useAAPValidation();
|
||||
// Firstboot
|
||||
const firstBootValidation = useFirstBootValidation();
|
||||
// Details
|
||||
|
|
@ -293,8 +298,10 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
const hasWslTargetOnly = useHasSpecificTargetOnly('wsl');
|
||||
|
||||
let startIndex = 1; // default index
|
||||
const JUMP_TO_REVIEW_STEP = 23;
|
||||
|
||||
if (isEdit) {
|
||||
startIndex = 22;
|
||||
startIndex = JUMP_TO_REVIEW_STEP;
|
||||
}
|
||||
|
||||
const [wasRegisterVisited, setWasRegisterVisited] = useState(false);
|
||||
|
|
@ -655,6 +662,22 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
|
|||
>
|
||||
<ServicesStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='Ansible Automation Platform'
|
||||
id='wizard-aap'
|
||||
isHidden={!isAAPRegistrationEnabled}
|
||||
key='wizard-aap'
|
||||
navItem={CustomStatusNavItem}
|
||||
status={aapValidation.disabledNext ? 'error' : 'default'}
|
||||
footer={
|
||||
<CustomWizardFooter
|
||||
disableNext={aapValidation.disabledNext}
|
||||
optional={true}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<AAPStep />
|
||||
</WizardStep>,
|
||||
<WizardStep
|
||||
name='First boot script configuration'
|
||||
id='wizard-first-boot'
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ type ValidatedTextInputPropTypes = TextInputProps & {
|
|||
type ValidationInputProp = TextInputProps &
|
||||
TextAreaProps & {
|
||||
value: string;
|
||||
placeholder: string;
|
||||
placeholder?: string;
|
||||
stepValidation: StepValidation;
|
||||
dataTestId?: string;
|
||||
fieldName: string;
|
||||
|
|
@ -91,7 +91,7 @@ export const ValidatedInputAndTextArea = ({
|
|||
onChange={onChange}
|
||||
validated={validated}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder}
|
||||
placeholder={placeholder || ''}
|
||||
aria-label={ariaLabel}
|
||||
data-testid={dataTestId}
|
||||
/>
|
||||
|
|
@ -138,6 +138,7 @@ export const ValidatedInput = ({
|
|||
value,
|
||||
placeholder,
|
||||
onChange,
|
||||
...props
|
||||
}: ValidatedTextInputPropTypes) => {
|
||||
const [isPristine, setIsPristine] = useState(!value ? true : false);
|
||||
|
||||
|
|
@ -164,6 +165,7 @@ export const ValidatedInput = ({
|
|||
aria-label={ariaLabel || ''}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder || ''}
|
||||
{...props}
|
||||
/>
|
||||
{!isPristine && !validator(value) && (
|
||||
<HelperText>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
import React from 'react';
|
||||
|
||||
import {
|
||||
Checkbox,
|
||||
DropEvent,
|
||||
FileUpload,
|
||||
FormGroup,
|
||||
FormHelperText,
|
||||
HelperText,
|
||||
HelperTextItem,
|
||||
} from '@patternfly/react-core';
|
||||
|
||||
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
|
||||
import {
|
||||
changeAapCallbackUrl,
|
||||
changeAapHostConfigKey,
|
||||
changeAapTlsCertificateAuthority,
|
||||
changeAapTlsConfirmation,
|
||||
selectAapCallbackUrl,
|
||||
selectAapHostConfigKey,
|
||||
selectAapTlsCertificateAuthority,
|
||||
selectAapTlsConfirmation,
|
||||
} from '../../../../../store/wizardSlice';
|
||||
import { useAAPValidation } from '../../../utilities/useValidation';
|
||||
import { ValidatedInputAndTextArea } from '../../../ValidatedInput';
|
||||
import { validateMultipleCertificates } from '../../../validators';
|
||||
|
||||
const AAPRegistration = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const callbackUrl = useAppSelector(selectAapCallbackUrl);
|
||||
const hostConfigKey = useAppSelector(selectAapHostConfigKey);
|
||||
const tlsCertificateAuthority = useAppSelector(
|
||||
selectAapTlsCertificateAuthority,
|
||||
);
|
||||
const tlsConfirmation = useAppSelector(selectAapTlsConfirmation);
|
||||
const [isRejected, setIsRejected] = React.useState(false);
|
||||
const stepValidation = useAAPValidation();
|
||||
|
||||
const isHttpsUrl = callbackUrl?.toLowerCase().startsWith('https://') || false;
|
||||
const shouldShowCaInput = !isHttpsUrl || (isHttpsUrl && !tlsConfirmation);
|
||||
|
||||
const validated = stepValidation.errors['certificate']
|
||||
? 'error'
|
||||
: stepValidation.errors['certificate'] === undefined &&
|
||||
tlsCertificateAuthority &&
|
||||
validateMultipleCertificates(tlsCertificateAuthority).validCertificates
|
||||
.length > 0
|
||||
? 'success'
|
||||
: 'default';
|
||||
|
||||
const handleCallbackUrlChange = (value: string) => {
|
||||
dispatch(changeAapCallbackUrl(value));
|
||||
};
|
||||
|
||||
const handleHostConfigKeyChange = (value: string) => {
|
||||
dispatch(changeAapHostConfigKey(value));
|
||||
};
|
||||
|
||||
const handleClear = () => {
|
||||
dispatch(changeAapTlsCertificateAuthority(''));
|
||||
};
|
||||
|
||||
const handleTextChange = (
|
||||
_event: React.ChangeEvent<HTMLTextAreaElement>,
|
||||
value: string,
|
||||
) => {
|
||||
dispatch(changeAapTlsCertificateAuthority(value));
|
||||
setIsRejected(false);
|
||||
};
|
||||
|
||||
const handleDataChange = (_: DropEvent, value: string) => {
|
||||
dispatch(changeAapTlsCertificateAuthority(value));
|
||||
setIsRejected(false);
|
||||
};
|
||||
|
||||
const handleFileRejected = () => {
|
||||
dispatch(changeAapTlsCertificateAuthority(''));
|
||||
setIsRejected(true);
|
||||
};
|
||||
|
||||
const handleTlsConfirmationChange = (checked: boolean) => {
|
||||
dispatch(changeAapTlsConfirmation(checked));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormGroup label='Ansible Callback URL' isRequired>
|
||||
<ValidatedInputAndTextArea
|
||||
value={callbackUrl || ''}
|
||||
onChange={(_event, value) => handleCallbackUrlChange(value.trim())}
|
||||
ariaLabel='ansible callback url'
|
||||
isRequired
|
||||
stepValidation={stepValidation}
|
||||
fieldName='callbackUrl'
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup label='Host Config Key' isRequired>
|
||||
<ValidatedInputAndTextArea
|
||||
value={hostConfigKey || ''}
|
||||
onChange={(_event, value) => handleHostConfigKeyChange(value.trim())}
|
||||
ariaLabel='host config key'
|
||||
isRequired
|
||||
stepValidation={stepValidation}
|
||||
fieldName='hostConfigKey'
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{shouldShowCaInput && (
|
||||
<FormGroup label='Certificate authority (CA) for Ansible Controller'>
|
||||
<FileUpload
|
||||
id='aap-certificate-upload'
|
||||
type='text'
|
||||
value={tlsCertificateAuthority || ''}
|
||||
filename={tlsCertificateAuthority ? 'CA detected' : ''}
|
||||
onDataChange={handleDataChange}
|
||||
onTextChange={handleTextChange}
|
||||
onClearClick={handleClear}
|
||||
dropzoneProps={{
|
||||
accept: {
|
||||
'application/x-pem-file': ['.pem'],
|
||||
'application/x-x509-ca-cert': ['.cer', '.crt'],
|
||||
'application/pkix-cert': ['.der'],
|
||||
},
|
||||
maxSize: 512000,
|
||||
onDropRejected: handleFileRejected,
|
||||
}}
|
||||
validated={isRejected ? 'error' : validated}
|
||||
browseButtonText='Upload'
|
||||
allowEditingUploadedText={true}
|
||||
/>
|
||||
<FormHelperText>
|
||||
<HelperText>
|
||||
<HelperTextItem
|
||||
variant={
|
||||
isRejected || validated === 'error'
|
||||
? 'error'
|
||||
: validated === 'success'
|
||||
? 'success'
|
||||
: 'default'
|
||||
}
|
||||
>
|
||||
{isRejected
|
||||
? 'Must be a .PEM/.CER/.CRT file'
|
||||
: validated === 'error'
|
||||
? stepValidation.errors['certificate']
|
||||
: validated === 'success'
|
||||
? 'Certificate was uploaded'
|
||||
: 'Drag and drop a valid certificate file or upload one'}
|
||||
</HelperTextItem>
|
||||
</HelperText>
|
||||
</FormHelperText>
|
||||
</FormGroup>
|
||||
)}
|
||||
{isHttpsUrl && (
|
||||
<FormGroup>
|
||||
<Checkbox
|
||||
id='tls-confirmation-checkbox'
|
||||
label='Insecure'
|
||||
isChecked={tlsConfirmation || false}
|
||||
onChange={(_event, checked) => handleTlsConfirmationChange(checked)}
|
||||
/>
|
||||
{stepValidation.errors['tlsConfirmation'] && (
|
||||
<FormHelperText>
|
||||
<HelperText>
|
||||
<HelperTextItem variant='error'>
|
||||
{stepValidation.errors['tlsConfirmation']}
|
||||
</HelperTextItem>
|
||||
</HelperText>
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormGroup>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AAPRegistration;
|
||||
18
src/Components/CreateImageWizard/steps/AAP/index.tsx
Normal file
18
src/Components/CreateImageWizard/steps/AAP/index.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
|
||||
import { Form, Title } from '@patternfly/react-core';
|
||||
|
||||
import AAPRegistration from './components/AAPRegistration';
|
||||
|
||||
const AAPStep = () => {
|
||||
return (
|
||||
<Form>
|
||||
<Title headingLevel='h1' size='xl'>
|
||||
Ansible Automation Platform
|
||||
</Title>
|
||||
<AAPRegistration />
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AAPStep;
|
||||
|
|
@ -25,6 +25,7 @@ import {
|
|||
KernelList,
|
||||
LocaleList,
|
||||
OscapList,
|
||||
RegisterAapList,
|
||||
RegisterLaterList,
|
||||
RegisterNowList,
|
||||
RegisterSatelliteList,
|
||||
|
|
@ -42,6 +43,7 @@ import isRhel from '../../../../../src/Utilities/isRhel';
|
|||
import { targetOptions } from '../../../../constants';
|
||||
import { useAppSelector } from '../../../../store/hooks';
|
||||
import {
|
||||
selectAapRegistration,
|
||||
selectBlueprintDescription,
|
||||
selectBlueprintName,
|
||||
selectCompliancePolicyID,
|
||||
|
|
@ -65,6 +67,7 @@ import { useHasSpecificTargetOnly } from '../../utilities/hasSpecificTargetOnly'
|
|||
const Review = () => {
|
||||
const { goToStepById } = useWizardContext();
|
||||
|
||||
const aapRegistration = useAppSelector(selectAapRegistration);
|
||||
const blueprintName = useAppSelector(selectBlueprintName);
|
||||
const blueprintDescription = useAppSelector(selectBlueprintDescription);
|
||||
const distribution = useAppSelector(selectDistribution);
|
||||
|
|
@ -83,6 +86,7 @@ const Review = () => {
|
|||
const users = useAppSelector(selectUsers);
|
||||
const kernel = useAppSelector(selectKernel);
|
||||
|
||||
const [isExpandedAap, setIsExpandedAap] = useState(true);
|
||||
const [isExpandedImageOutput, setIsExpandedImageOutput] = useState(true);
|
||||
const [isExpandedTargetEnvs, setIsExpandedTargetEnvs] = useState(true);
|
||||
const [isExpandedFSC, setIsExpandedFSC] = useState(true);
|
||||
|
|
@ -101,6 +105,8 @@ const Review = () => {
|
|||
const [isExpandableFirstBoot, setIsExpandedFirstBoot] = useState(true);
|
||||
const [isExpandedUsers, setIsExpandedUsers] = useState(true);
|
||||
|
||||
const onToggleAap = (isExpandedAap: boolean) =>
|
||||
setIsExpandedAap(isExpandedAap);
|
||||
const onToggleImageOutput = (isExpandedImageOutput: boolean) =>
|
||||
setIsExpandedImageOutput(isExpandedImageOutput);
|
||||
const onToggleTargetEnvs = (isExpandedTargetEnvs: boolean) =>
|
||||
|
|
@ -499,6 +505,21 @@ const Review = () => {
|
|||
<ServicesList />
|
||||
</ExpandableSection>
|
||||
)}
|
||||
{aapRegistration.callbackUrl && (
|
||||
<ExpandableSection
|
||||
toggleContent={composeExpandable(
|
||||
'Ansible Automation Platform',
|
||||
'revisit-aap',
|
||||
'wizard-aap',
|
||||
)}
|
||||
onToggle={(_event, isExpandableAap) => onToggleAap(isExpandableAap)}
|
||||
isExpanded={isExpandedAap}
|
||||
isIndented
|
||||
data-testid='aap-expandable'
|
||||
>
|
||||
<RegisterAapList />
|
||||
</ExpandableSection>
|
||||
)}
|
||||
{!process.env.IS_ON_PREMISE && (
|
||||
<ExpandableSection
|
||||
toggleContent={composeExpandable(
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ import { useAppSelector } from '../../../../store/hooks';
|
|||
import { useGetSourceListQuery } from '../../../../store/provisioningApi';
|
||||
import { useShowActivationKeyQuery } from '../../../../store/rhsmApi';
|
||||
import {
|
||||
selectAapCallbackUrl,
|
||||
selectAapHostConfigKey,
|
||||
selectAapTlsCertificateAuthority,
|
||||
selectAapTlsConfirmation,
|
||||
selectActivationKey,
|
||||
selectArchitecture,
|
||||
selectAwsAccountId,
|
||||
|
|
@ -660,6 +664,45 @@ export const RegisterSatelliteList = () => {
|
|||
);
|
||||
};
|
||||
|
||||
export const RegisterAapList = () => {
|
||||
const callbackUrl = useAppSelector(selectAapCallbackUrl);
|
||||
const hostConfigKey = useAppSelector(selectAapHostConfigKey);
|
||||
const tlsCertificateAuthority = useAppSelector(
|
||||
selectAapTlsCertificateAuthority,
|
||||
);
|
||||
const skipTlsVerification = useAppSelector(selectAapTlsConfirmation);
|
||||
|
||||
const getTlsStatus = () => {
|
||||
if (skipTlsVerification) {
|
||||
return 'Insecure (TLS verification skipped)';
|
||||
}
|
||||
return tlsCertificateAuthority ? 'Configured' : 'None';
|
||||
};
|
||||
|
||||
return (
|
||||
<Content>
|
||||
<Content component={ContentVariants.dl} className='review-step-dl'>
|
||||
<Content component={ContentVariants.dt} className='pf-v6-u-min-width'>
|
||||
Ansible Callback URL
|
||||
</Content>
|
||||
<Content component={ContentVariants.dd}>
|
||||
{callbackUrl || 'None'}
|
||||
</Content>
|
||||
<Content component={ContentVariants.dt} className='pf-v6-u-min-width'>
|
||||
Host Config Key
|
||||
</Content>
|
||||
<Content component={ContentVariants.dd}>
|
||||
{hostConfigKey || 'None'}
|
||||
</Content>
|
||||
<Content component={ContentVariants.dt} className='pf-v6-u-min-width'>
|
||||
TLS Certificate
|
||||
</Content>
|
||||
<Content component={ContentVariants.dd}>{getTlsStatus()}</Content>
|
||||
</Content>
|
||||
</Content>
|
||||
);
|
||||
};
|
||||
|
||||
export const RegisterNowList = () => {
|
||||
const activationKey = useAppSelector(selectActivationKey);
|
||||
const registrationType = useAppSelector(selectRegistrationType);
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import {
|
|||
CockpitUploadTypes,
|
||||
} from '../../../store/cockpit/types';
|
||||
import {
|
||||
AapRegistration,
|
||||
AwsUploadRequestOptions,
|
||||
AzureUploadRequestOptions,
|
||||
BlueprintExportResponse,
|
||||
|
|
@ -49,6 +50,11 @@ import { ApiRepositoryImportResponseRead } from '../../../store/service/contentS
|
|||
import {
|
||||
ComplianceType,
|
||||
initialState,
|
||||
RegistrationType,
|
||||
selectAapCallbackUrl,
|
||||
selectAapHostConfigKey,
|
||||
selectAapTlsCertificateAuthority,
|
||||
selectAapTlsConfirmation,
|
||||
selectActivationKey,
|
||||
selectArchitecture,
|
||||
selectAwsAccountId,
|
||||
|
|
@ -387,14 +393,7 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
|
|||
baseUrl: request.customizations.subscription?.['base-url'] || '',
|
||||
},
|
||||
registration: {
|
||||
registrationType:
|
||||
request.customizations?.subscription && isRhel(request.distribution)
|
||||
? request.customizations.subscription.rhc
|
||||
? 'register-now-rhc'
|
||||
: 'register-now-insights'
|
||||
: getSatelliteCommand(request.customizations.files)
|
||||
? 'register-satellite'
|
||||
: 'register-later',
|
||||
registrationType: getRegistrationType(request),
|
||||
activationKey: isRhel(request.distribution)
|
||||
? request.customizations.subscription?.['activation-key']
|
||||
: undefined,
|
||||
|
|
@ -403,6 +402,15 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
|
|||
caCert: request.customizations.cacerts?.pem_certs[0],
|
||||
},
|
||||
},
|
||||
aapRegistration: {
|
||||
callbackUrl:
|
||||
request.customizations?.aap_registration?.ansible_callback_url,
|
||||
hostConfigKey: request.customizations?.aap_registration?.host_config_key,
|
||||
tlsCertificateAuthority:
|
||||
request.customizations?.aap_registration?.tls_certificate_authority,
|
||||
skipTlsVerification:
|
||||
request.customizations?.aap_registration?.skip_tls_verification,
|
||||
},
|
||||
...commonRequestToState(request),
|
||||
};
|
||||
};
|
||||
|
|
@ -452,6 +460,15 @@ export const mapExportRequestToState = (
|
|||
},
|
||||
env: initialState.env,
|
||||
registration: initialState.registration,
|
||||
aapRegistration: {
|
||||
callbackUrl:
|
||||
request.customizations?.aap_registration?.ansible_callback_url,
|
||||
hostConfigKey: request.customizations?.aap_registration?.host_config_key,
|
||||
tlsCertificateAuthority:
|
||||
request.customizations?.aap_registration?.tls_certificate_authority,
|
||||
skipTlsVerification:
|
||||
request.customizations?.aap_registration?.skip_tls_verification,
|
||||
},
|
||||
...commonRequestToState(blueprintResponse),
|
||||
};
|
||||
};
|
||||
|
|
@ -461,6 +478,24 @@ const getFirstBootScript = (files?: File[]): string => {
|
|||
return firstBootFile?.data ? atob(firstBootFile.data) : '';
|
||||
};
|
||||
|
||||
const getAapRegistration = (state: RootState): AapRegistration | undefined => {
|
||||
const callbackUrl = selectAapCallbackUrl(state);
|
||||
const hostConfigKey = selectAapHostConfigKey(state);
|
||||
const tlsCertificateAuthority = selectAapTlsCertificateAuthority(state);
|
||||
const skipTlsVerification = selectAapTlsConfirmation(state);
|
||||
|
||||
if (!callbackUrl && !hostConfigKey && !tlsCertificateAuthority) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
ansible_callback_url: callbackUrl || '',
|
||||
host_config_key: hostConfigKey || '',
|
||||
tls_certificate_authority: tlsCertificateAuthority || undefined,
|
||||
skip_tls_verification: skipTlsVerification || undefined,
|
||||
};
|
||||
};
|
||||
|
||||
const getImageRequests = (
|
||||
state: RootState,
|
||||
): ImageRequest[] | CockpitImageRequest[] => {
|
||||
|
|
@ -482,6 +517,24 @@ const getImageRequests = (
|
|||
}));
|
||||
};
|
||||
|
||||
const getRegistrationType = (request: BlueprintResponse): RegistrationType => {
|
||||
const subscription = request.customizations.subscription;
|
||||
const distribution = request.distribution;
|
||||
const files = request.customizations.files;
|
||||
|
||||
if (subscription && isRhel(distribution)) {
|
||||
if (subscription.rhc) {
|
||||
return 'register-now-rhc';
|
||||
} else {
|
||||
return 'register-now-insights';
|
||||
}
|
||||
} else if (getSatelliteCommand(files)) {
|
||||
return 'register-satellite';
|
||||
} else {
|
||||
return 'register-later';
|
||||
}
|
||||
};
|
||||
|
||||
const getSatelliteCommand = (files?: File[]): string => {
|
||||
const satelliteCommandFile = files?.find(
|
||||
(file) => file.path === SATELLITE_PATH,
|
||||
|
|
@ -642,6 +695,7 @@ const getCustomizations = (state: RootState, orgID: string): Customizations => {
|
|||
pem_certs: [satCert],
|
||||
}
|
||||
: undefined,
|
||||
aap_registration: getAapRegistration(state),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ import { useAppSelector } from '../../../store/hooks';
|
|||
import { BlueprintsResponse } from '../../../store/imageBuilderApi';
|
||||
import { useShowActivationKeyQuery } from '../../../store/rhsmApi';
|
||||
import {
|
||||
selectAapCallbackUrl,
|
||||
selectAapHostConfigKey,
|
||||
selectAapTlsCertificateAuthority,
|
||||
selectAapTlsConfirmation,
|
||||
selectActivationKey,
|
||||
selectBlueprintDescription,
|
||||
selectBlueprintId,
|
||||
|
|
@ -54,6 +58,8 @@ import {
|
|||
isSshKeyValid,
|
||||
isUserGroupValid,
|
||||
isUserNameValid,
|
||||
isValidUrl,
|
||||
validateMultipleCertificates,
|
||||
} from '../validators';
|
||||
|
||||
export type StepValidation = {
|
||||
|
|
@ -205,6 +211,62 @@ export function useRegistrationValidation(): StepValidation {
|
|||
return { errors: {}, disabledNext: false };
|
||||
}
|
||||
|
||||
export function useAAPValidation(): StepValidation {
|
||||
const errors: Record<string, string> = {};
|
||||
const callbackUrl = useAppSelector(selectAapCallbackUrl);
|
||||
const hostConfigKey = useAppSelector(selectAapHostConfigKey);
|
||||
const tlsCertificateAuthority = useAppSelector(
|
||||
selectAapTlsCertificateAuthority,
|
||||
);
|
||||
const tlsConfirmation = useAppSelector(selectAapTlsConfirmation);
|
||||
|
||||
if (!callbackUrl && !hostConfigKey && !tlsCertificateAuthority) {
|
||||
return { errors: {}, disabledNext: false };
|
||||
}
|
||||
if (!callbackUrl || callbackUrl.trim() === '') {
|
||||
errors.callbackUrl = 'Ansible Callback URL is required';
|
||||
} else if (!isValidUrl(callbackUrl)) {
|
||||
errors.callbackUrl = 'Callback URL must be a valid URL';
|
||||
}
|
||||
|
||||
if (!hostConfigKey || hostConfigKey.trim() === '') {
|
||||
errors.hostConfigKey = 'Host Config Key is required';
|
||||
}
|
||||
|
||||
if (tlsCertificateAuthority && tlsCertificateAuthority.trim() !== '') {
|
||||
const validation = validateMultipleCertificates(tlsCertificateAuthority);
|
||||
if (validation.errors.length > 0) {
|
||||
errors.certificate = validation.errors.join(' ');
|
||||
} else if (validation.validCertificates.length === 0) {
|
||||
errors.certificate = 'No valid certificates found in the input.';
|
||||
}
|
||||
}
|
||||
|
||||
if (callbackUrl && callbackUrl.trim() !== '') {
|
||||
const isHttpsUrl = callbackUrl.toLowerCase().startsWith('https://');
|
||||
|
||||
// If URL is HTTP, require TLS certificate
|
||||
if (
|
||||
!isHttpsUrl &&
|
||||
(!tlsCertificateAuthority || tlsCertificateAuthority.trim() === '')
|
||||
) {
|
||||
errors.certificate = 'HTTP URL requires a custom TLS certificate';
|
||||
return { errors, disabledNext: true };
|
||||
}
|
||||
|
||||
// For HTTPS URL, if the TLS confirmation is not checked, require certificate
|
||||
if (
|
||||
!tlsConfirmation &&
|
||||
(!tlsCertificateAuthority || tlsCertificateAuthority.trim() === '')
|
||||
) {
|
||||
errors.certificate =
|
||||
'HTTPS URL requires either a custom TLS certificate or confirmation that no custom certificate is needed';
|
||||
}
|
||||
}
|
||||
|
||||
return { errors, disabledNext: Object.keys(errors).length > 0 };
|
||||
}
|
||||
|
||||
export function useFilesystemValidation(): StepValidation {
|
||||
const mode = useAppSelector(selectFileSystemConfigurationType);
|
||||
const partitions = useAppSelector(selectPartitions);
|
||||
|
|
|
|||
|
|
@ -138,3 +138,85 @@ export const isServiceValid = (service: string) => {
|
|||
/[a-zA-Z]+/.test(service) // contains at least one letter
|
||||
);
|
||||
};
|
||||
|
||||
export const isValidUrl = (url: string): boolean => {
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
const isHttpOrHttps = ['http:', 'https:'].includes(parsedUrl.protocol);
|
||||
const hostname = parsedUrl.hostname;
|
||||
const hasValidDomain =
|
||||
/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?:\.[a-zA-Z]{2,})*$/.test(hostname);
|
||||
|
||||
return isHttpOrHttps && hasValidDomain;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const isValidCA = (ca: string) => {
|
||||
if (!ca || typeof ca !== 'string') return false;
|
||||
|
||||
const trimmed = ca.trim();
|
||||
|
||||
const pemPattern =
|
||||
/^-----BEGIN CERTIFICATE-----[\r\n]+([\s\S]*?)[\r\n]+-----END CERTIFICATE-----$/;
|
||||
|
||||
if (!pemPattern.test(trimmed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const match = trimmed.match(pemPattern);
|
||||
if (!match || !match[1]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const base64Content = match[1].replace(/[\r\n\s]/g, '');
|
||||
|
||||
const base64Pattern = /^[A-Za-z0-9+/]+(=*)$/;
|
||||
return base64Pattern.test(base64Content) && base64Content.length > 0;
|
||||
};
|
||||
|
||||
export const parseMultipleCertificates = (input: string): string[] => {
|
||||
if (!input || typeof input !== 'string') return [];
|
||||
|
||||
const blockPattern =
|
||||
/-----BEGIN CERTIFICATE-----[\s\S]*?(?=-----BEGIN CERTIFICATE-----|$)/g;
|
||||
|
||||
const matches = input.match(blockPattern);
|
||||
return matches ? matches.map((m) => m.trim()) : [];
|
||||
};
|
||||
|
||||
export const validateMultipleCertificates = (
|
||||
input: string,
|
||||
): {
|
||||
certificates: string[];
|
||||
validCertificates: string[];
|
||||
invalidCertificates: string[];
|
||||
errors: string[];
|
||||
} => {
|
||||
const certificates = parseMultipleCertificates(input);
|
||||
const validCertificates: string[] = [];
|
||||
const invalidCertificates: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
if (certificates.length === 0 && input.trim() !== '') {
|
||||
errors.push(
|
||||
'No valid certificate format found. Certificates must be in PEM/DER/CER format.',
|
||||
);
|
||||
return { certificates, validCertificates, invalidCertificates, errors };
|
||||
}
|
||||
|
||||
certificates.forEach((cert, index) => {
|
||||
if (isValidCA(cert)) {
|
||||
validCertificates.push(cert);
|
||||
} else {
|
||||
invalidCertificates.push(cert);
|
||||
errors.push(
|
||||
`Certificate ${index + 1} is not valid. Must be in PEM/DER/CER format.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return { certificates, validCertificates, invalidCertificates, errors };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ export type RegistrationType =
|
|||
| 'register-now'
|
||||
| 'register-now-insights'
|
||||
| 'register-now-rhc'
|
||||
| 'register-satellite';
|
||||
| 'register-satellite'
|
||||
| 'register-aap';
|
||||
|
||||
export type ComplianceType = 'openscap' | 'compliance';
|
||||
|
||||
|
|
@ -89,6 +90,12 @@ export type wizardState = {
|
|||
architecture: ImageRequest['architecture'];
|
||||
distribution: Distributions;
|
||||
imageTypes: ImageTypes[];
|
||||
aapRegistration: {
|
||||
callbackUrl: string | undefined;
|
||||
hostConfigKey: string | undefined;
|
||||
tlsCertificateAuthority: string | undefined;
|
||||
skipTlsVerification: boolean | undefined;
|
||||
};
|
||||
aws: {
|
||||
accountId: string;
|
||||
shareMethod: AwsShareMethod;
|
||||
|
|
@ -189,6 +196,12 @@ export const initialState: wizardState = {
|
|||
architecture: X86_64,
|
||||
distribution: RHEL_10,
|
||||
imageTypes: [],
|
||||
aapRegistration: {
|
||||
callbackUrl: undefined,
|
||||
hostConfigKey: undefined,
|
||||
tlsCertificateAuthority: undefined,
|
||||
skipTlsVerification: undefined,
|
||||
},
|
||||
aws: {
|
||||
accountId: '',
|
||||
shareMethod: 'sources',
|
||||
|
|
@ -376,6 +389,26 @@ export const selectSatelliteCaCertificate = (state: RootState) => {
|
|||
return state.wizard.registration.satelliteRegistration.caCert;
|
||||
};
|
||||
|
||||
export const selectAapRegistration = (state: RootState) => {
|
||||
return state.wizard.aapRegistration;
|
||||
};
|
||||
|
||||
export const selectAapCallbackUrl = (state: RootState) => {
|
||||
return state.wizard.aapRegistration?.callbackUrl;
|
||||
};
|
||||
|
||||
export const selectAapHostConfigKey = (state: RootState) => {
|
||||
return state.wizard.aapRegistration?.hostConfigKey;
|
||||
};
|
||||
|
||||
export const selectAapTlsCertificateAuthority = (state: RootState) => {
|
||||
return state.wizard.aapRegistration?.tlsCertificateAuthority;
|
||||
};
|
||||
|
||||
export const selectAapTlsConfirmation = (state: RootState) => {
|
||||
return state.wizard.aapRegistration?.skipTlsVerification;
|
||||
};
|
||||
|
||||
export const selectComplianceProfileID = (state: RootState) => {
|
||||
return state.wizard.compliance.profileID;
|
||||
};
|
||||
|
|
@ -627,6 +660,22 @@ export const wizardSlice = createSlice({
|
|||
changeSatelliteCaCertificate: (state, action: PayloadAction<string>) => {
|
||||
state.registration.satelliteRegistration.caCert = action.payload;
|
||||
},
|
||||
changeAapCallbackUrl: (state, action: PayloadAction<string>) => {
|
||||
state.aapRegistration.callbackUrl = action.payload;
|
||||
},
|
||||
|
||||
changeAapHostConfigKey: (state, action: PayloadAction<string>) => {
|
||||
state.aapRegistration.hostConfigKey = action.payload;
|
||||
},
|
||||
changeAapTlsCertificateAuthority: (
|
||||
state,
|
||||
action: PayloadAction<string>,
|
||||
) => {
|
||||
state.aapRegistration.tlsCertificateAuthority = action.payload;
|
||||
},
|
||||
changeAapTlsConfirmation: (state, action: PayloadAction<boolean>) => {
|
||||
state.aapRegistration.skipTlsVerification = action.payload;
|
||||
},
|
||||
changeActivationKey: (
|
||||
state,
|
||||
action: PayloadAction<ActivationKeys['name']>,
|
||||
|
|
@ -1230,6 +1279,10 @@ export const {
|
|||
changeTimezone,
|
||||
changeSatelliteRegistrationCommand,
|
||||
changeSatelliteCaCertificate,
|
||||
changeAapCallbackUrl,
|
||||
changeAapHostConfigKey,
|
||||
changeAapTlsCertificateAuthority,
|
||||
changeAapTlsConfirmation,
|
||||
addNtpServer,
|
||||
removeNtpServer,
|
||||
changeHostname,
|
||||
|
|
|
|||
|
|
@ -714,6 +714,9 @@ describe('Import modal', () => {
|
|||
),
|
||||
);
|
||||
|
||||
// AAP
|
||||
await clickNext();
|
||||
|
||||
// Firstboot
|
||||
await clickNext();
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ vi.mock('@unleash/proxy-client-react', () => ({
|
|||
switch (flag) {
|
||||
case 'image-builder.compliance.enabled':
|
||||
return true;
|
||||
case 'image-builder.aap.enabled':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,12 +109,12 @@ describe('Step Services', () => {
|
|||
router = undefined;
|
||||
});
|
||||
|
||||
test('clicking Next loads First boot script', async () => {
|
||||
test('clicking Next loads Ansible Automation Platform', async () => {
|
||||
await renderCreateMode();
|
||||
await goToServicesStep();
|
||||
await clickNext();
|
||||
await screen.findByRole('heading', {
|
||||
name: 'First boot configuration',
|
||||
name: 'Ansible Automation Platform',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,8 @@ vi.mock('@unleash/proxy-client-react', () => ({
|
|||
return true;
|
||||
case 'image-builder.templates.enabled':
|
||||
return true;
|
||||
case 'image-builder.aap.enabled':
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue