This commit is contained in:
Gianluca Zuccarelli 2025-08-21 11:08:52 -05:00 committed by GitHub
commit e57578cadc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 241 additions and 47 deletions

View file

@ -1,7 +1,7 @@
import { execSync } from 'child_process';
import { readFileSync } from 'node:fs';
import { expect, type Page } from '@playwright/test';
import { expect, FrameLocator, type Page } from '@playwright/test';
export const togglePreview = async (page: Page) => {
const toggleSwitch = page.locator('#preview-toggle');
@ -85,3 +85,15 @@ export const getHostDistroName = (): string => {
export const getHostArch = (): string => {
return execSync('uname -m').toString('utf-8').replace(/\s/g, '');
};
export const isRhel = async (frame: Page | FrameLocator) => {
if (isHosted()) {
return true;
}
const registerHeading = frame.getByRole('heading', {
name: 'Register systems using this image',
});
return registerHeading.isVisible();
};

View file

@ -1,6 +1,6 @@
import { expect, FrameLocator, type Page, test } from '@playwright/test';
import { closePopupsIfExist, isHosted } from './helpers';
import { closePopupsIfExist, isHosted, isRhel } from './helpers';
import { ibFrame, navigateToLandingPage } from './navHelpers';
/**
@ -45,11 +45,12 @@ export const fillInDetails = async (
/**
* Select "Register later" option in the wizard
* This function executes only on the hosted service
* This function executes only on if the registration
* step is visible (it won't be visible for Fedora)
* @param page - the page object
*/
export const registerLater = async (page: Page | FrameLocator) => {
if (isHosted()) {
if (await isRhel(page)) {
await page.getByRole('button', { name: 'Register' }).click();
await page.getByRole('radio', { name: 'Register later' }).click();
}

View file

@ -4,7 +4,7 @@ import TOML from '@ltd/j-toml';
import { expect, test } from '@playwright/test';
import { v4 as uuidv4 } from 'uuid';
import { closePopupsIfExist, isHosted } from './helpers/helpers';
import { closePopupsIfExist, isHosted, isRhel } from './helpers/helpers';
import { ensureAuthenticated } from './helpers/login';
import { ibFrame, navigateToLandingPage } from './helpers/navHelpers';
@ -27,11 +27,8 @@ test.describe.serial('test', () => {
.click();
await frame.getByRole('button', { name: 'Next', exact: true }).click();
if (isHosted()) {
frame.getByRole('heading', {
name: 'Register systems using this image',
});
await page.getByRole('radio', { name: /Register later/i }).click();
if (await isRhel(frame)) {
await frame.getByRole('radio', { name: /Register later/i }).click();
await frame.getByRole('button', { name: 'Next', exact: true }).click();
}
@ -281,7 +278,12 @@ test.describe.serial('test', () => {
// the first card should be the AWS card
await frame.locator('.pf-v6-c-card').first().click();
await frame.getByRole('button', { name: 'Next', exact: true }).click();
// Just use the default region
await frame.getByRole('button', { name: 'Next', exact: true }).click();
if (await isRhel(frame)) {
await frame.getByRole('radio', { name: /Register later/i }).click();
await frame.getByRole('button', { name: 'Next', exact: true }).click();
}
await frame.getByRole('button', { name: 'Review and finish' }).click();
await frame.getByRole('button', { name: 'Back', exact: true }).click();

View file

@ -318,7 +318,7 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
'parentId' in step && step.parentId === 'step-optional-steps';
useEffect(() => {
if (process.env.IS_ON_PREMISE) {
if (!isRhel(distribution)) {
if (step.id === 'step-oscap' && step.isVisited) {
setWasRegisterVisited(true);
}
@ -460,7 +460,7 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
name='Register'
id='step-register'
key='step-register'
isHidden={!!process.env.IS_ON_PREMISE || !isRhel(distribution)}
isHidden={!isRhel(distribution)}
navItem={CustomStatusNavItem}
status={
wasRegisterVisited

View file

@ -26,10 +26,14 @@ const ActivationKeyInformation = (): JSX.Element => {
} = useShowActivationKeyQuery(
{ name: activationKey! },
{
skip: !activationKey,
skip: !activationKey || !!process.env.IS_ON_PREMISE,
},
);
if (process.env.IS_ON_PREMISE) {
return <Content component={ContentVariants.dd}>{activationKey}</Content>;
}
return (
<>
{isFetchingActivationKeyInfo && <Spinner size='lg' />}

View file

@ -0,0 +1,123 @@
import React, { MutableRefObject, useEffect, useRef } from 'react';
import {
Button,
Content,
ContentVariants,
FormGroup,
FormGroupLabelHelp,
Popover,
} from '@patternfly/react-core';
import { ExternalLinkAltIcon } from '@patternfly/react-icons';
import { ACTIVATION_KEYS_URL, CDN_PROD_URL } from '../../../../../constants';
import { useAppDispatch, useAppSelector } from '../../../../../store/hooks';
import {
changeActivationKey,
changeBaseUrl,
changeOrgId,
changeServerUrl,
selectActivationKey,
selectOrgId,
} from '../../../../../store/wizardSlice';
import { ValidatedInput } from '../../../ValidatedInput';
const ManualRegistrationPopover = ({
ref,
}: {
ref: MutableRefObject<null>;
}) => {
return (
<Popover
triggerRef={ref}
headerContent='About Activation Keys & Organization ID'
position='right'
minWidth='30rem'
bodyContent={
<Content>
<Content component={ContentVariants.p}>
Activation keys assist you in registering and configuring systems.
Metadata such as role, system purpose, and usage can be
automatically attached to systems via an activation key, and
monitored with Subscription Watch.
</Content>
<Content component={ContentVariants.p}>
The Organization ID is the numeric identifier for your organization
and is separate from your account number. Your organization&apos;s
activation keys and organization ID are displayed on the Activation
Keys page in the Hybrid Cloud Console.
</Content>
<Button
component='a'
target='_blank'
variant='link'
icon={<ExternalLinkAltIcon />}
iconPosition='right'
isInline
href={ACTIVATION_KEYS_URL}
>
View activation keys in Hybrid Cloud Console
</Button>
</Content>
}
>
<FormGroupLabelHelp
ref={ref}
aria-label='About Activation Keys & Organization ID'
/>
</Popover>
);
};
export const ManualActivationKey = () => {
const dispatch = useAppDispatch();
const orgId = useAppSelector(selectOrgId);
const activationKey = useAppSelector(selectActivationKey);
const orgIdRef = useRef(null);
const activationKeyRef = useRef(null);
useEffect(() => {
dispatch(changeServerUrl('subscription.rhsm.redhat.com'));
dispatch(changeBaseUrl(CDN_PROD_URL));
}, [dispatch]);
return (
<>
<FormGroup
label={'Activation key'}
labelHelp={<ManualRegistrationPopover ref={activationKeyRef} />}
>
<ValidatedInput
placeholder='Activation key'
ariaLabel='Activation key'
value={activationKey || ''}
onChange={(_, value) => {
dispatch(changeActivationKey(value.trim()));
}}
validator={(v) => v !== undefined && v !== ''}
helperText='The activation key cannot be empty'
/>
</FormGroup>
<FormGroup
label={'Organization ID'}
labelHelp={<ManualRegistrationPopover ref={orgIdRef} />}
>
<ValidatedInput
placeholder='Organization ID'
ariaLabel='Organization ID'
value={orgId || ''}
onChange={(_, value) => {
dispatch(changeOrgId(value.trim()));
}}
validator={(v) => {
if (v === undefined || v === '') {
return false;
}
return /^\d+$/.test(v.trim());
}}
helperText='Please enter a valid Organization ID'
/>
</FormGroup>
</>
);
};

View file

@ -13,6 +13,7 @@ import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import ActivationKeyInformation from './components/ActivationKeyInformation';
import ActivationKeysList from './components/ActivationKeysList';
import { ManualActivationKey } from './components/ManualActivationKey';
import Registration from './components/Registration';
import SatelliteRegistration from './components/SatelliteRegistration';
@ -44,23 +45,25 @@ const RegistrationStep = () => {
<Title headingLevel='h1' size='xl'>
Register systems using this image
</Title>
<FormGroup label='Organization ID'>
<ClipboardCopy
hoverTip='Copy to clipboard'
clickTip='Successfully copied to clipboard!'
variant='inline-compact'
>
{orgId || ''}
</ClipboardCopy>
<FormHelperText>
<HelperText>
<HelperTextItem>
If using an activation key with command line registration, you
must provide your organization&apos;s ID
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
{!process.env.IS_ON_PREMISE && (
<FormGroup label='Organization ID'>
<ClipboardCopy
hoverTip='Copy to clipboard'
clickTip='Successfully copied to clipboard!'
variant='inline-compact'
>
{orgId || ''}
</ClipboardCopy>
<FormHelperText>
<HelperText>
<HelperTextItem>
If using an activation key with command line registration, you
must provide your organization&apos;s ID
</HelperTextItem>
</HelperText>
</FormHelperText>
</FormGroup>
)}
<Registration />
{registrationType === 'register-satellite' && <SatelliteRegistration />}
{!process.env.IS_ON_PREMISE &&
@ -76,6 +79,9 @@ const RegistrationStep = () => {
<ActivationKeyInformation />
</FormGroup>
)}
{process.env.IS_ON_PREMISE && registrationType !== 'register-later' && (
<ManualActivationKey />
)}
</Form>
);
};

View file

@ -20,6 +20,8 @@ import {
useCreateBPWithNotification as useCreateBlueprintMutation,
useUpdateBPWithNotification as useUpdateBlueprintMutation,
} from '../../../../../Hooks';
import { useAppSelector } from '../../../../../store/hooks';
import { selectOrgId } from '../../../../../store/wizardSlice';
import { resolveRelPath } from '../../../../../Utilities/path';
import { mapRequestFromState } from '../../../utilities/requestMapper';
import { useIsBlueprintValid } from '../../../utilities/useValidation';
@ -41,6 +43,7 @@ const ReviewWizardFooter = () => {
};
const navigate = useNavigate();
const isValid = useIsBlueprintValid();
const orgId = useAppSelector(selectOrgId);
useEffect(() => {
if (isUpdateSuccess || isCreateSuccess) {
@ -58,9 +61,7 @@ const ReviewWizardFooter = () => {
return requestBody;
}
// NOTE: This should be fine on-prem, we should
// be able to ignore the `org-id`
return mapRequestFromState(store, '');
return mapRequestFromState(store, orgId ?? '');
};
return (

View file

@ -72,6 +72,7 @@ import {
selectKeyboard,
selectLanguages,
selectNtpServers,
selectOrgId,
selectPackages,
selectPartitions,
selectRecommendedRepositories,
@ -704,6 +705,7 @@ export const RegisterAapList = () => {
};
export const RegisterNowList = () => {
const orgId = useAppSelector(selectOrgId);
const activationKey = useAppSelector(selectActivationKey);
const registrationType = useAppSelector(selectRegistrationType);
@ -711,7 +713,7 @@ export const RegisterNowList = () => {
// @ts-ignore type of 'activationKey' might not be strictly compatible with the expected type for 'name'.
{ name: activationKey },
{
skip: !activationKey,
skip: !activationKey || process.env.IS_ON_PREMISE,
},
);
return (
@ -753,6 +755,12 @@ export const RegisterNowList = () => {
<Content component={ContentVariants.dd}>
<ActivationKeyInformation />
</Content>
{process.env.IS_ON_PREMISE && (
<>
<Content component={ContentVariants.dt}>Organization ID</Content>
<Content component={ContentVariants.dd}>{orgId}</Content>
</>
)}
</Content>
</Content>
{isError && (

View file

@ -399,6 +399,9 @@ export const mapRequestToState = (request: BlueprintResponse): wizardState => {
activationKey: isRhel(request.distribution)
? request.customizations.subscription?.['activation-key']
: undefined,
orgId: isRhel(request.distribution)
? request.customizations.subscription?.['organization']?.toString()
: undefined,
satelliteRegistration: {
command: getSatelliteCommand(request.customizations.files),
caCert: request.customizations.cacerts?.pem_certs[0],

View file

@ -28,6 +28,7 @@ import {
selectKeyboard,
selectLanguages,
selectNtpServers,
selectOrgId,
selectPartitions,
selectRegistrationType,
selectSatelliteCaCertificate,
@ -123,6 +124,7 @@ type ValidationState = {
export function useRegistrationValidation(): StepValidation {
const registrationType = useAppSelector(selectRegistrationType);
const activationKey = useAppSelector(selectActivationKey);
const orgId = useAppSelector(selectOrgId);
const registrationCommand = useAppSelector(
selectSatelliteRegistrationCommand,
);
@ -132,15 +134,37 @@ export function useRegistrationValidation(): StepValidation {
useShowActivationKeyQuery(
{ name: activationKey! },
{
skip: !activationKey,
skip: !activationKey || !!process.env.IS_ON_PREMISE,
},
);
if (
registrationType !== 'register-later' &&
registrationType !== 'register-satellite' &&
!activationKey
) {
if (registrationType === 'register-later') {
return { errors: {}, disabledNext: false };
}
if (process.env.IS_ON_PREMISE) {
const errors: Record<string, string> = {};
let disabledNext = false;
if (!activationKey?.trim()) {
errors.activationKey = 'Activation Key not set';
disabledNext = true;
}
if (!orgId?.trim()) {
errors.orgId = 'Organization ID not set';
disabledNext = true;
} else if (!/^\d+$/.test(orgId.trim())) {
errors.orgId = 'Organization ID should be a numeric value';
disabledNext = true;
}
if (Object.keys(errors).length > 0) {
return { errors, disabledNext };
}
}
if (registrationType !== 'register-satellite' && !activationKey) {
return {
errors: { activationKey: 'No activation key selected' },
disabledNext: true,
@ -148,10 +172,10 @@ export function useRegistrationValidation(): StepValidation {
}
if (
registrationType !== 'register-later' &&
registrationType !== 'register-satellite' &&
activationKey &&
(isFetchingKeyInfo || isErrorKeyInfo)
(isFetchingKeyInfo || isErrorKeyInfo) &&
!process.env.IS_ON_PREMISE
) {
return {
errors: { activationKey: 'Invalid activation key' },

View file

@ -25,7 +25,9 @@ export const RELEASE_LIFECYCLE_URL =
export const AZURE_AUTH_URL =
'https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow';
export const COMPLIANCE_URL = '/insights/compliance/scappolicies';
export const ACTIVATION_KEYS_URL = '/insights/connector/activation-keys';
export const ACTIVATION_KEYS_URL = !process.env.IS_ON_PREMISE
? '/insights/connector/activation-keys'
: 'https://console.redhat.com/settings/connector/activation-keys';
export const COMPLIANCE_AND_VULN_SCANNING_URL =
'https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/6/html/security_guide/chap-compliance_and_vulnerability_scanning';
export const CREATING_IMAGES_WITH_IB_URL =

View file

@ -119,6 +119,7 @@ export type wizardState = {
registration: {
registrationType: RegistrationType;
activationKey: ActivationKeys['name'];
orgId: string | undefined;
satelliteRegistration: {
command: string | undefined;
caCert: string | undefined;
@ -226,6 +227,7 @@ export const initialState: wizardState = {
? 'register-later'
: 'register-now-rhc',
activationKey: undefined,
orgId: undefined,
satelliteRegistration: {
command: undefined,
caCert: undefined,
@ -381,6 +383,10 @@ export const selectActivationKey = (state: RootState) => {
return state.wizard.registration.activationKey;
};
export const selectOrgId = (state: RootState) => {
return state.wizard.registration.orgId;
};
export const selectSatelliteRegistrationCommand = (state: RootState) => {
return state.wizard.registration.satelliteRegistration?.command;
};
@ -682,6 +688,9 @@ export const wizardSlice = createSlice({
) => {
state.registration.activationKey = action.payload;
},
changeOrgId: (state, action: PayloadAction<string>) => {
state.registration.orgId = action.payload;
},
changeComplianceType: (state, action: PayloadAction<ComplianceType>) => {
state.compliance.complianceType = action.payload;
},
@ -1223,6 +1232,7 @@ export const {
reinitializeGcp,
changeRegistrationType,
changeActivationKey,
changeOrgId,
changeCompliance,
changeComplianceType,
changeFileSystemConfiguration,

View file

@ -30,10 +30,8 @@ const goToHostnameStep = async () => {
});
await waitFor(() => user.click(guestImageCheckBox));
if (!process.env.IS_ON_PREMISE) {
await clickNext(); // Registration
await clickRegisterLater();
}
await clickNext(); // Registration
await clickRegisterLater();
await goToStep(/Hostname/);
};