Skip to content

Commit 86ed954

Browse files
ImageOutput: migrate BlueprintMode, ArchSelect, and Security review to usePlatformFeatures
Co-authored-by: Claude Sonnet 4 <noreply@anthropic.com>
1 parent 1051661 commit 86ed954

7 files changed

Lines changed: 224 additions & 41 deletions

File tree

src/Components/CreateImageWizard/steps/ImageOutput/components/ArchSelect.tsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import {
1010
} from '@patternfly/react-core';
1111

1212
import { ARCHES } from '@/constants';
13+
import { usePlatformFeatures } from '@/Hooks/usePlatformFeatures';
1314
import { ImageRequest } from '@/store/api/backend';
1415
import { useAppDispatch, useAppSelector } from '@/store/hooks';
15-
import { selectIsOnPremise } from '@/store/slices/env';
1616
import { changeArchitecture, selectArchitecture } from '@/store/slices/wizard';
1717

1818
type ArchSelectProps = {
@@ -22,7 +22,7 @@ type ArchSelectProps = {
2222
const ArchSelect = ({ isDisabled = false }: ArchSelectProps) => {
2323
const arch = useAppSelector(selectArchitecture);
2424
const dispatch = useAppDispatch();
25-
const isOnPremise = useAppSelector(selectIsOnPremise);
25+
const { canCrossArchBuild } = usePlatformFeatures();
2626
const [isOpen, setIsOpen] = useState(false);
2727

2828
const setArch = (_event?: React.MouseEvent, selection?: string | number) => {
@@ -34,9 +34,7 @@ const ArchSelect = ({ isDisabled = false }: ArchSelectProps) => {
3434
const setSelectOptions = () => {
3535
const options: ReactElement[] = [];
3636
const arches = ARCHES.filter((a) => {
37-
// we don't want to support cross-arch
38-
// builds for on-prem for now
39-
if (isOnPremise) {
37+
if (!canCrossArchBuild) {
4038
return a === arch;
4139
}
4240
return true;

src/Components/CreateImageWizard/steps/ImageOutput/components/BlueprintMode.tsx

Lines changed: 37 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import {
99
import { BuildIcon, RepositoryIcon } from '@patternfly/react-icons';
1010

1111
import { RHEL_10, RHEL_10_IMAGE_MODE_IMAGE, X86_64 } from '@/constants';
12+
import { usePlatformFeatures } from '@/Hooks/usePlatformFeatures';
1213
import { Distributions } from '@/store/api/backend';
1314
import { useAppDispatch, useAppSelector } from '@/store/hooks';
14-
import { selectIsOnPremise } from '@/store/slices/env';
1515
import {
1616
changeArchitecture,
1717
changeBlueprintMode,
@@ -26,7 +26,11 @@ import { getHostDistro } from '@/Utilities/getHostInfo';
2626

2727
const BlueprintMode = () => {
2828
const dispatch = useAppDispatch();
29-
const isOnPremise = useAppSelector(selectIsOnPremise);
29+
const {
30+
canCrossArchBuild,
31+
restoresPreviousSelections,
32+
setsDefaultImageSource,
33+
} = usePlatformFeatures();
3034
const isImageMode = useAppSelector(selectIsImageMode);
3135
const distribution = useAppSelector(selectDistribution);
3236
const architecture = useAppSelector(selectArchitecture);
@@ -35,7 +39,7 @@ const BlueprintMode = () => {
3539
const previousArch = useRef(architecture);
3640

3741
useEffect(() => {
38-
if (!isOnPremise) return;
42+
if (canCrossArchBuild) return;
3943
const fetchDefaultDistro = async () => {
4044
try {
4145
const distro = await getHostDistro();
@@ -46,7 +50,34 @@ const BlueprintMode = () => {
4650
};
4751

4852
fetchDefaultDistro();
49-
}, [isOnPremise]);
53+
}, [canCrossArchBuild]);
54+
55+
const handlePackageMode = () => {
56+
dispatch(changeBlueprintMode('package'));
57+
dispatch(
58+
changeDistribution(
59+
restoresPreviousSelections ? previousDistro.current : defaultDistro,
60+
),
61+
);
62+
// Image source is only relevant in image mode
63+
dispatch(changeImageSource(undefined));
64+
if (restoresPreviousSelections) {
65+
dispatch(changeArchitecture(previousArch.current));
66+
}
67+
};
68+
69+
const handleImageMode = () => {
70+
if (restoresPreviousSelections) {
71+
previousDistro.current = distribution;
72+
previousArch.current = architecture;
73+
}
74+
dispatch(changeBlueprintMode('image'));
75+
dispatch(changeImageTypes([]));
76+
if (setsDefaultImageSource) {
77+
dispatch(changeArchitecture(X86_64));
78+
dispatch(changeImageSource(RHEL_10_IMAGE_MODE_IMAGE));
79+
}
80+
};
5081

5182
return (
5283
<FormGroup label='Image type' isRequired>
@@ -56,38 +87,15 @@ const BlueprintMode = () => {
5687
text='Package mode'
5788
buttonId='blueprint-mode-package'
5889
isSelected={!isImageMode}
59-
onChange={() => {
60-
dispatch(changeBlueprintMode('package'));
61-
dispatch(
62-
changeDistribution(
63-
isOnPremise ? defaultDistro : previousDistro.current,
64-
),
65-
);
66-
// Image source is only relevant in image mode
67-
dispatch(changeImageSource(undefined));
68-
if (!isOnPremise) {
69-
dispatch(changeArchitecture(previousArch.current));
70-
}
71-
}}
90+
onChange={handlePackageMode}
7291
aria-describedby='blueprint-mode-description'
7392
/>
7493
<ToggleGroupItem
7594
icon={<BuildIcon />}
7695
text='Image mode'
7796
buttonId='blueprint-mode-image'
7897
isSelected={isImageMode}
79-
onChange={() => {
80-
if (!isOnPremise) {
81-
previousDistro.current = distribution;
82-
previousArch.current = architecture;
83-
}
84-
dispatch(changeBlueprintMode('image'));
85-
dispatch(changeImageTypes([]));
86-
if (!isOnPremise) {
87-
dispatch(changeArchitecture(X86_64));
88-
dispatch(changeImageSource(RHEL_10_IMAGE_MODE_IMAGE));
89-
}
90-
}}
98+
onChange={handleImageMode}
9199
aria-describedby='blueprint-mode-description'
92100
/>
93101
</ToggleGroup>

src/Components/CreateImageWizard/steps/ImageOutput/tests/ArchSelect.test.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import { AARCH64, X86_64 } from '@/constants';
44
import { selectArchitecture } from '@/store/slices/wizard';
55
import { clickWithWait, createUser } from '@/test/testUtils';
66

7-
import { openArchSelect, renderArchSelect, selectArch } from './helpers';
7+
import {
8+
openArchSelect,
9+
renderArchSelect,
10+
renderArchSelectOnPrem,
11+
selectArch,
12+
} from './helpers';
813

914
describe('ArchSelect', () => {
1015
test('renders with default x86_64 architecture', () => {
@@ -76,4 +81,30 @@ describe('ArchSelect', () => {
7681
expect(screen.queryByRole('listbox')).not.toBeInTheDocument();
7782
});
7883
});
84+
85+
describe('on-prem (no cross-arch build)', () => {
86+
test('only shows the current architecture option', async () => {
87+
const user = createUser();
88+
renderArchSelectOnPrem();
89+
90+
await openArchSelect(user);
91+
92+
expect(screen.getByRole('option', { name: X86_64 })).toBeInTheDocument();
93+
expect(
94+
screen.queryByRole('option', { name: AARCH64 }),
95+
).not.toBeInTheDocument();
96+
});
97+
98+
test('only shows aarch64 when that is the current arch', async () => {
99+
const user = createUser();
100+
renderArchSelectOnPrem({ architecture: AARCH64 });
101+
102+
await openArchSelect(user);
103+
104+
expect(screen.getByRole('option', { name: AARCH64 })).toBeInTheDocument();
105+
expect(
106+
screen.queryByRole('option', { name: X86_64 }),
107+
).not.toBeInTheDocument();
108+
});
109+
});
79110
});

src/Components/CreateImageWizard/steps/ImageOutput/tests/BlueprintMode.test.tsx

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
import { screen } from '@testing-library/react';
22

3+
import { AARCH64, RHEL_10, RHEL_9, X86_64 } from '@/constants';
34
import { createUser } from '@/test/testUtils';
45

5-
import { renderBlueprintMode, toggleBlueprintMode } from './helpers';
6+
import {
7+
renderBlueprintMode,
8+
renderBlueprintModeHosted,
9+
toggleBlueprintMode,
10+
} from './helpers';
611

712
describe('BlueprintMode', () => {
813
describe('Rendering', () => {
@@ -131,4 +136,81 @@ describe('BlueprintMode', () => {
131136
expect(imageModeButton).toHaveAttribute('aria-pressed', 'true');
132137
});
133138
});
139+
140+
describe('Hosted platform behavior', () => {
141+
test('restores previous distribution when switching back to package mode', async () => {
142+
const { store } = renderBlueprintModeHosted({ distribution: RHEL_9 });
143+
const user = createUser();
144+
145+
// Switch to image mode (saves previous distro)
146+
await toggleBlueprintMode(user, 'image');
147+
// Switch back to package mode (restores previous distro)
148+
await toggleBlueprintMode(user, 'package');
149+
150+
expect(store.getState().wizard.distribution).toBe(RHEL_9);
151+
});
152+
153+
test('restores previous architecture when switching back to package mode', async () => {
154+
const { store } = renderBlueprintModeHosted({ architecture: AARCH64 });
155+
const user = createUser();
156+
157+
await toggleBlueprintMode(user, 'image');
158+
// Image mode forces x86_64
159+
expect(store.getState().wizard.architecture).toBe(X86_64);
160+
161+
await toggleBlueprintMode(user, 'package');
162+
// Package mode restores previous arch
163+
expect(store.getState().wizard.architecture).toBe(AARCH64);
164+
});
165+
166+
test('sets default image source when switching to image mode', async () => {
167+
const { store } = renderBlueprintModeHosted();
168+
const user = createUser();
169+
170+
await toggleBlueprintMode(user, 'image');
171+
172+
expect(store.getState().wizard.imageSource).toBeDefined();
173+
});
174+
175+
test('forces x86_64 architecture when switching to image mode', async () => {
176+
const { store } = renderBlueprintModeHosted({ architecture: AARCH64 });
177+
const user = createUser();
178+
179+
await toggleBlueprintMode(user, 'image');
180+
181+
expect(store.getState().wizard.architecture).toBe(X86_64);
182+
});
183+
});
184+
185+
describe('On-prem platform behavior', () => {
186+
test('does not save or restore previous selections', async () => {
187+
const { store } = renderBlueprintMode({ distribution: RHEL_9 });
188+
const user = createUser();
189+
190+
await toggleBlueprintMode(user, 'image');
191+
await toggleBlueprintMode(user, 'package');
192+
193+
// On-prem uses the default distro (RHEL_10 from state), not previous
194+
expect(store.getState().wizard.distribution).toBe(RHEL_10);
195+
});
196+
197+
test('does not set default image source when switching to image mode', async () => {
198+
const { store } = renderBlueprintMode();
199+
const user = createUser();
200+
201+
await toggleBlueprintMode(user, 'image');
202+
203+
expect(store.getState().wizard.imageSource).toBeUndefined();
204+
});
205+
206+
test('does not change architecture when switching to image mode', async () => {
207+
const { store } = renderBlueprintMode({ architecture: AARCH64 });
208+
const user = createUser();
209+
210+
await toggleBlueprintMode(user, 'image');
211+
212+
// On-prem doesn't force x86_64
213+
expect(store.getState().wizard.architecture).toBe(AARCH64);
214+
});
215+
});
134216
});

src/Components/CreateImageWizard/steps/ImageOutput/tests/helpers.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,23 @@ export const renderArchSelect = (
4141
});
4242
};
4343

44+
export const renderArchSelectOnPrem = (
45+
wizardStateOverrides: WizardStateOverrides = {},
46+
) => {
47+
return renderWithRedux(
48+
<ArchSelect />,
49+
{
50+
...defaultStateOverrides,
51+
...wizardStateOverrides,
52+
},
53+
{
54+
preloadedState: {
55+
env: { isOnPremise: true },
56+
},
57+
},
58+
);
59+
};
60+
4461
// Interaction helpers
4562
export const openArchSelect = async (user: UserEventInstance) => {
4663
const toggle = await screen.findByTestId('arch_select');
@@ -120,6 +137,16 @@ export const renderBlueprintMode = (
120137
);
121138
};
122139

140+
// BlueprintMode render function (uses hosted store)
141+
export const renderBlueprintModeHosted = (
142+
wizardStateOverrides: WizardStateOverrides = {},
143+
) => {
144+
return renderWithRedux(<BlueprintMode />, {
145+
...defaultStateOverrides,
146+
...wizardStateOverrides,
147+
});
148+
};
149+
123150
export const toggleBlueprintMode = async (
124151
user: UserEventInstance,
125152
mode: 'package' | 'image',

src/Components/CreateImageWizard/steps/Review/components/Security/index.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import React from 'react';
22

33
import { Card, CardBody } from '@patternfly/react-core';
44

5+
import { usePlatformFeatures } from '@/Hooks/usePlatformFeatures';
56
import { useAppSelector } from '@/store/hooks';
6-
import { selectFips, selectIsOnPremise } from '@/store/slices';
7+
import { selectFips } from '@/store/slices';
78

89
import { FIPSDetails, SecurityDetails } from './components';
910
import { isSecurityConfigured, SecuritySummary } from './types';
@@ -16,7 +17,7 @@ type SecurityCardProps = ReviewCardProps & {
1617
};
1718

1819
const Security = ({ restrictions, security }: SecurityCardProps) => {
19-
const isOnPremise = useAppSelector(selectIsOnPremise);
20+
const { securitySectionLabel } = usePlatformFeatures();
2021
const { enabled: fipsEnabled } = useAppSelector(selectFips);
2122

2223
if (restrictions.openscap.shouldHide && restrictions.fips.shouldHide) {
@@ -30,9 +31,7 @@ const Security = ({ restrictions, security }: SecurityCardProps) => {
3031
return (
3132
<Card>
3233
<ReviewCardHeader
33-
title={
34-
isOnPremise ? 'Security configuration' : 'Compliance configuration'
35-
}
34+
title={securitySectionLabel}
3635
stepId='base-settings-step'
3736
sectionId='security-section'
3837
/>

src/Components/CreateImageWizard/steps/Review/components/Security/tests/Security.test.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,44 @@ describe('Security', () => {
6060
});
6161
});
6262

63+
describe('Platform labels', () => {
64+
test('shows "Compliance configuration" on hosted', () => {
65+
renderWithRedux(<Security restrictions={createDefaultRestrictions()} />, {
66+
imageTypes: ['guest-image'],
67+
fips: {
68+
enabled: true,
69+
},
70+
});
71+
72+
expect(screen.getByText('Compliance configuration')).toBeInTheDocument();
73+
expect(
74+
screen.queryByText('Security configuration'),
75+
).not.toBeInTheDocument();
76+
});
77+
78+
test('shows "Security configuration" on-prem', () => {
79+
renderWithRedux(
80+
<Security restrictions={createDefaultRestrictions()} />,
81+
{
82+
imageTypes: ['guest-image'],
83+
fips: {
84+
enabled: true,
85+
},
86+
},
87+
{
88+
preloadedState: {
89+
env: { isOnPremise: true },
90+
},
91+
},
92+
);
93+
94+
expect(screen.getByText('Security configuration')).toBeInTheDocument();
95+
expect(
96+
screen.queryByText('Compliance configuration'),
97+
).not.toBeInTheDocument();
98+
});
99+
});
100+
63101
describe('OpenSCAP', () => {
64102
test('displays OpenSCAP profile when compliance type is openscap', () => {
65103
const mockSummaryWithCIS = {

0 commit comments

Comments
 (0)