Skip to content

Commit 91a3e6a

Browse files
Wizard: clear timezone and locale for unsupported image types
Network-installer already excluded timezone from its supported blueprint options, but the wizard wasn't acting on that restriction. Add clearTimezone and clearLocale reducers and wire them into both wizard versions so state is reset when restrictions indicate a customization should be hidden. Co-authored-by: Gianluca Zuccarelli <gzuccare@redhat.com>
1 parent b5a74d4 commit 91a3e6a

6 files changed

Lines changed: 202 additions & 4 deletions

File tree

src/Components/CreateImageWizard/CreateImageWizard.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import {
3434
changeProxy,
3535
changeServerUrl,
3636
changeTimezone,
37+
clearLocale,
38+
clearTimezone,
3739
initializeWizard,
3840
loadWizardState,
3941
selectDistribution,
@@ -121,6 +123,7 @@ const CreateImageWizard = () => {
121123
const { userData } = useGetUser(auth);
122124
const hasTrackedInitialStepRef = useRef(false);
123125
const hasTrackedWizardOpenedRef = useRef(false);
126+
const isHostDistroDetected = useRef(!isOnPremise);
124127

125128
const {
126129
data: blueprintDetails,
@@ -247,6 +250,7 @@ const CreateImageWizard = () => {
247250
const initializeHostDistro = async () => {
248251
const distro = await getHostDistro();
249252
dispatch(changeDistribution(distro));
253+
isHostDistroDetected.current = true;
250254
};
251255

252256
const initializeHostArch = async () => {
@@ -297,15 +301,33 @@ const CreateImageWizard = () => {
297301
return;
298302
}
299303

304+
if (restrictions.locale.shouldHide) {
305+
dispatch(clearLocale());
306+
}
307+
308+
if (restrictions.timezone.shouldHide) {
309+
if (timezone) {
310+
dispatch(clearTimezone());
311+
}
312+
return;
313+
}
314+
300315
const defaultTimezone =
301316
distribution === RHEL_10 || targetEnvironments.includes('azure')
302317
? DEFAULT_TIMEZONE
303318
: 'America/New_York';
304319

305-
if (!timezone) {
320+
if (!timezone && isHostDistroDetected.current) {
306321
dispatch(changeTimezone(defaultTimezone));
307322
}
308-
}, [distribution, targetEnvironments, mode, dispatch]);
323+
}, [
324+
distribution,
325+
targetEnvironments,
326+
mode,
327+
dispatch,
328+
restrictions,
329+
timezone,
330+
]);
309331

310332
useEffect(() => {
311333
if (!isOnPremise && showWizardModal && !hasTrackedWizardOpenedRef.current) {

src/store/api/distributions/tests/distributionDetailsApi.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ describe('DISTRO_DETAILS configuration', () => {
5050
});
5151

5252
describe('network-installer restrictions', () => {
53-
it('should only allow locale and fips for network-installer', () => {
53+
it('should only allow fips and locale for network-installer', () => {
5454
const networkInstallerSupported =
5555
DISTRO_DETAILS['network-installer'].supported_blueprint_options;
5656

src/store/api/distributions/tests/hooks.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ describe('useCustomizationRestrictions hook logic', () => {
247247
expect(result.packages.shouldHide).toBe(true);
248248
expect(result.filesystem.shouldHide).toBe(true);
249249
expect(result.kernel.shouldHide).toBe(true);
250+
expect(result.timezone.shouldHide).toBe(true);
250251
expect(result.users.shouldHide).toBe(true);
251252
});
252253

src/store/slices/wizard/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1463,6 +1463,10 @@ export const wizardSlice = createSlice({
14631463
clearLanguages: (state) => {
14641464
state.locale.languages = [];
14651465
},
1466+
clearLocale: (state) => {
1467+
state.locale.languages = [];
1468+
state.locale.keyboard = '';
1469+
},
14661470
changeKeyboard: (state, action: PayloadAction<string>) => {
14671471
state.locale.keyboard = action.payload;
14681472
},
@@ -1613,6 +1617,10 @@ export const wizardSlice = createSlice({
16131617
}
16141618
}
16151619
},
1620+
clearTimezone: (state) => {
1621+
state.timezone.timezone = '';
1622+
state.timezone.ntpservers = [];
1623+
},
16161624
changeHostname: (state, action: PayloadAction<string>) => {
16171625
state.hostname = action.payload;
16181626
},
@@ -1841,6 +1849,7 @@ export const {
18411849
removeLanguage,
18421850
replaceLanguage,
18431851
clearLanguages,
1852+
clearLocale,
18441853
changeKeyboard,
18451854
changeBlueprintName,
18461855
setIsCustomName,
@@ -1874,6 +1883,7 @@ export const {
18741883
changeAapTlsConfirmation,
18751884
addNtpServer,
18761885
removeNtpServer,
1886+
clearTimezone,
18771887
changeHostname,
18781888
addPort,
18791889
removePort,

src/store/slices/wizard/tests/wizardSlice.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import wizardReducer, {
55
changeArchitecture,
66
changeDistribution,
77
changeImageTypes,
8+
clearLocale,
9+
clearTimezone,
810
initializeWizard,
911
initialState,
1012
loadWizardState,
@@ -232,4 +234,52 @@ describe('wizardSlice core reducers', () => {
232234
});
233235
});
234236
});
237+
238+
describe('clearLocale', () => {
239+
it('should reset languages and keyboard', () => {
240+
const stateWithLocale: wizardState = {
241+
...initialState,
242+
locale: {
243+
languages: ['en_US.UTF-8', 'fr_FR.UTF-8'],
244+
keyboard: 'us',
245+
},
246+
};
247+
248+
const result = wizardReducer(stateWithLocale, clearLocale());
249+
250+
expect(result.locale.languages).toEqual([]);
251+
expect(result.locale.keyboard).toBe('');
252+
});
253+
254+
it('should be a no-op on already empty locale state', () => {
255+
const result = wizardReducer(initialState, clearLocale());
256+
257+
expect(result.locale.languages).toEqual([]);
258+
expect(result.locale.keyboard).toBe('');
259+
});
260+
});
261+
262+
describe('clearTimezone', () => {
263+
it('should reset timezone and ntpservers', () => {
264+
const stateWithTimezone: wizardState = {
265+
...initialState,
266+
timezone: {
267+
timezone: 'America/New_York',
268+
ntpservers: ['0.pool.ntp.org', '1.pool.ntp.org'],
269+
},
270+
};
271+
272+
const result = wizardReducer(stateWithTimezone, clearTimezone());
273+
274+
expect(result.timezone.timezone).toBe('');
275+
expect(result.timezone.ntpservers).toEqual([]);
276+
});
277+
278+
it('should be a no-op on already empty timezone state', () => {
279+
const result = wizardReducer(initialState, clearTimezone());
280+
281+
expect(result.timezone.timezone).toBe('');
282+
expect(result.timezone.ntpservers).toEqual([]);
283+
});
284+
});
235285
});

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

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,16 @@
33
// Redux State → mapRequestFromState() → API Request. They could be replaced by unit
44
// tests for the request mapper functions (mapRequestToState/mapRequestFromState) which
55
// would be faster and more focused than full integration tests.
6-
import { EDIT_BLUEPRINT } from '../../../../../constants';
6+
import { screen, within } from '@testing-library/react';
7+
8+
import { CreateBlueprintRequest } from '@/store/api/backend';
9+
import { clickWithWait, createUser } from '@/test/testUtils';
10+
11+
import {
12+
CREATE_BLUEPRINT,
13+
EDIT_BLUEPRINT,
14+
RHEL_10,
15+
} from '../../../../../constants';
716
import { mockBlueprintIds } from '../../../../fixtures/blueprints';
817
import {
918
aarch64CreateBlueprintRequest,
@@ -13,8 +22,14 @@ import {
1322
x86_64CreateBlueprintRequest,
1423
} from '../../../../fixtures/editMode';
1524
import {
25+
blueprintRequest,
26+
enterBlueprintName,
27+
goToReview,
28+
interceptBlueprintRequest,
1629
interceptEditBlueprintRequest,
30+
renderCreateMode,
1731
renderEditMode,
32+
selectGuestImageTarget,
1833
} from '../../wizardTestUtils';
1934

2035
describe('Image output edit mode', () => {
@@ -82,3 +97,103 @@ describe('Image output edit mode', () => {
8297
expect(receivedRequest).toEqual(expectedRequest);
8398
});
8499
});
100+
101+
const selectNetworkInstaller = async () => {
102+
const user = createUser();
103+
const checkbox = await screen.findByRole('checkbox', {
104+
name: /Network installer checkbox/i,
105+
});
106+
await clickWithWait(user, checkbox);
107+
return checkbox;
108+
};
109+
110+
describe('Network installer target', () => {
111+
beforeEach(() => {
112+
vi.clearAllMocks();
113+
});
114+
115+
test('selecting network-installer shows alert and disables other checkboxes', async () => {
116+
await renderCreateMode();
117+
const networkInstallerCheckbox = await selectNetworkInstaller();
118+
119+
await screen.findByText(
120+
/This image type requires specific, minimal configuration for remote installation/i,
121+
);
122+
const guestImageCheckbox = await screen.findByRole('checkbox', {
123+
name: /Virtualization guest image/i,
124+
});
125+
expect(guestImageCheckbox).toBeDisabled();
126+
127+
const bareMetalCheckbox = await screen.findByRole('checkbox', {
128+
name: /Bare metal installer/i,
129+
});
130+
expect(bareMetalCheckbox).toBeDisabled();
131+
132+
expect(networkInstallerCheckbox).toBeChecked();
133+
expect(networkInstallerCheckbox).toBeEnabled();
134+
});
135+
136+
test('selecting another target first disables network-installer', async () => {
137+
await renderCreateMode();
138+
await selectGuestImageTarget();
139+
140+
const networkInstallerCheckbox = await screen.findByRole('checkbox', {
141+
name: /Network installer checkbox/i,
142+
});
143+
expect(networkInstallerCheckbox).toBeDisabled();
144+
});
145+
146+
test('selecting network-installer only shows base settings, advanced settings, and review steps', async () => {
147+
await renderCreateMode();
148+
await selectNetworkInstaller();
149+
150+
const navigation = await screen.findByRole('navigation', {
151+
name: /wizard steps/i,
152+
});
153+
154+
const stepButtons = within(navigation).getAllByRole('button');
155+
expect(stepButtons).toHaveLength(3);
156+
157+
expect(
158+
within(navigation).getByRole('button', { name: /base settings/i }),
159+
).toBeInTheDocument();
160+
expect(
161+
within(navigation).getByRole('button', { name: /advanced settings/i }),
162+
).toBeInTheDocument();
163+
expect(
164+
within(navigation).getByRole('button', { name: /review/i }),
165+
).toBeInTheDocument();
166+
});
167+
168+
test('can create a blueprint with network-installer', async () => {
169+
await renderCreateMode();
170+
await selectNetworkInstaller();
171+
await enterBlueprintName('Red Velvet');
172+
173+
await goToReview();
174+
175+
const receivedRequest = await interceptBlueprintRequest(CREATE_BLUEPRINT);
176+
177+
const expectedRequest: CreateBlueprintRequest = {
178+
...blueprintRequest,
179+
distribution: RHEL_10,
180+
customizations: {
181+
locale: {
182+
languages: ['C.UTF-8'],
183+
},
184+
},
185+
image_requests: [
186+
{
187+
architecture: 'x86_64',
188+
image_type: 'network-installer',
189+
upload_request: {
190+
options: {},
191+
type: 'aws.s3',
192+
},
193+
},
194+
],
195+
};
196+
197+
expect(receivedRequest).toEqual(expectedRequest);
198+
});
199+
});

0 commit comments

Comments
 (0)