Skip to content

Commit a82fe4e

Browse files
Wizard: clear timezone and locale for unsupported image types
Rely on the existing restrictions system to clear wizard state when image types don't support timezone or locale. Add clearTimezone and clearLocale reducers to atomically reset related state (timezone + NTP servers, languages + keyboard). Co-authored-by: Gianluca Zuccarelli <gzuccare@redhat.com>
1 parent f329dd4 commit a82fe4e

9 files changed

Lines changed: 235 additions & 7 deletions

File tree

src/Components/CreateImageWizard/CreateImageWizard.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
changeArchitecture,
2424
changeDistribution,
2525
changeTimezone,
26+
clearLocale,
27+
clearTimezone,
2628
initializeWizard,
2729
selectDistribution,
2830
selectImageSource,
@@ -308,6 +310,13 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
308310
return;
309311
}
310312

313+
if (restrictions.timezone.shouldHide) {
314+
if (timezone) {
315+
dispatch(clearTimezone());
316+
}
317+
return;
318+
}
319+
311320
const defaultTimezone =
312321
distribution === RHEL_10 || targetEnvironments.includes('azure')
313322
? DEFAULT_TIMEZONE
@@ -316,7 +325,24 @@ const CreateImageWizard = ({ isEdit }: CreateImageWizardProps) => {
316325
if (!timezone) {
317326
dispatch(changeTimezone(defaultTimezone));
318327
}
319-
}, [distribution, targetEnvironments, isEdit, dispatch]);
328+
}, [
329+
distribution,
330+
targetEnvironments,
331+
isEdit,
332+
dispatch,
333+
restrictions.timezone.shouldHide,
334+
timezone,
335+
]);
336+
337+
useEffect(() => {
338+
if (isEdit) {
339+
return;
340+
}
341+
342+
if (restrictions.locale.shouldHide) {
343+
dispatch(clearLocale());
344+
}
345+
}, [isEdit, dispatch, restrictions.locale.shouldHide]);
320346

321347
// Duplicating some of the logic from the Wizard component to allow for custom nav items status
322348
// for original code see https://github.com/patternfly/patternfly-react/blob/184c55f8d10e1d94ffd72e09212db56c15387c5e/packages/react-core/src/components/Wizard/WizardNavInternal.tsx#L128

src/Components/CreateImageWizard3/CreateImageWizard3.tsx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
changeProxy,
3131
changeServerUrl,
3232
changeTimezone,
33+
clearLocale,
34+
clearTimezone,
3335
initializeWizard,
3436
loadWizardState,
3537
selectDistribution,
@@ -265,6 +267,13 @@ const CreateImageWizard3 = () => {
265267
return;
266268
}
267269

270+
if (restrictions.timezone.shouldHide) {
271+
if (timezone) {
272+
dispatch(clearTimezone());
273+
}
274+
return;
275+
}
276+
268277
const defaultTimezone =
269278
distribution === RHEL_10 || targetEnvironments.includes('azure')
270279
? DEFAULT_TIMEZONE
@@ -273,7 +282,24 @@ const CreateImageWizard3 = () => {
273282
if (!timezone) {
274283
dispatch(changeTimezone(defaultTimezone));
275284
}
276-
}, [distribution, targetEnvironments, mode, dispatch]);
285+
}, [
286+
distribution,
287+
targetEnvironments,
288+
mode,
289+
dispatch,
290+
restrictions.timezone.shouldHide,
291+
timezone,
292+
]);
293+
294+
useEffect(() => {
295+
if (mode !== 'create') {
296+
return;
297+
}
298+
299+
if (restrictions.locale.shouldHide) {
300+
dispatch(clearLocale());
301+
}
302+
}, [mode, dispatch, restrictions.locale.shouldHide]);
277303

278304
useEffect(() => {
279305
if (!isOnPremise && showWizardModal && !hasTrackedWizardOpenedRef.current) {

src/store/api/distributions/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export const DISTRO_DETAILS: Record<string, ImageTypeInfo> = {
7373
},
7474
'network-installer': {
7575
name: 'network-installer',
76-
supported_blueprint_options: ['locale', 'fips'],
76+
supported_blueprint_options: ['fips', 'locale'],
7777
},
7878
'pxe-tar-xz': {
7979
name: 'pxe-tar-xz',

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,11 @@ 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

57-
expect(networkInstallerSupported).toEqual(['locale', 'fips']);
57+
expect(networkInstallerSupported).toEqual(['fips', 'locale']);
5858
});
5959
});
6060

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/api/distributions/tests/mocks/fixtures.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ export const mockImageTypes = {
6464
'aap',
6565
]),
6666
networkInstallerMinimal: createMockImageType('network-installer', [
67-
'locale',
6867
'fips',
68+
'locale',
6969
]),
7070
wslWithoutFilesystemKernelOpenscap: createMockImageType('wsl', [
7171
'packages',

src/store/slices/wizard/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1466,6 +1466,10 @@ export const wizardSlice = createSlice({
14661466
clearLanguages: (state) => {
14671467
state.locale.languages = [];
14681468
},
1469+
clearLocale: (state) => {
1470+
state.locale.languages = [];
1471+
state.locale.keyboard = '';
1472+
},
14691473
changeKeyboard: (state, action: PayloadAction<string>) => {
14701474
state.locale.keyboard = action.payload;
14711475
},
@@ -1616,6 +1620,10 @@ export const wizardSlice = createSlice({
16161620
}
16171621
}
16181622
},
1623+
clearTimezone: (state) => {
1624+
state.timezone.timezone = '';
1625+
state.timezone.ntpservers = [];
1626+
},
16191627
changeHostname: (state, action: PayloadAction<string>) => {
16201628
state.hostname = action.payload;
16211629
},
@@ -1844,6 +1852,7 @@ export const {
18441852
removeLanguage,
18451853
replaceLanguage,
18461854
clearLanguages,
1855+
clearLocale,
18471856
changeKeyboard,
18481857
changeBlueprintName,
18491858
setIsCustomName,
@@ -1877,6 +1886,7 @@ export const {
18771886
changeAapTlsConfirmation,
18781887
addNtpServer,
18791888
removeNtpServer,
1889+
clearTimezone,
18801890
changeHostname,
18811891
addPort,
18821892
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)