Skip to content

Commit 649ccd6

Browse files
cezudasclaude
andauthored
Preselect multi-select wizard step options when configured (#2023)
Fixes OPS-3797. --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4ca9b82 commit 649ccd6

7 files changed

Lines changed: 201 additions & 2 deletions

File tree

packages/react-ui/src/app/features/benchmark/use-benchmark-wizard-navigation.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,47 @@ describe('useBenchmarkWizardNavigation', () => {
168168
expect(result.current.currentSelections).toEqual([]);
169169
});
170170

171+
it('should initialize selections from preselectedOptions when step has pre-selected options', async () => {
172+
const stepResponse = buildStepResponse({
173+
selectionType: 'multi-select',
174+
preselectedOptions: ['us-east-1'],
175+
});
176+
mockGetWizardStep.mockResolvedValue(stepResponse);
177+
178+
const { result } = renderHook(() =>
179+
useBenchmarkWizardNavigation(connectedProviders),
180+
);
181+
182+
act(() => {
183+
result.current.setSelectedProvider('aws');
184+
});
185+
186+
await act(async () => {
187+
await result.current.handleNextFromInitial();
188+
});
189+
190+
expect(result.current.currentSelections).toEqual(['us-east-1']);
191+
});
192+
193+
it('should initialize selections to empty array when step has no preselectedOptions', async () => {
194+
const stepResponse = buildStepResponse({ preselectedOptions: undefined });
195+
mockGetWizardStep.mockResolvedValue(stepResponse);
196+
197+
const { result } = renderHook(() =>
198+
useBenchmarkWizardNavigation(connectedProviders),
199+
);
200+
201+
act(() => {
202+
result.current.setSelectedProvider('aws');
203+
});
204+
205+
await act(async () => {
206+
await result.current.handleNextFromInitial();
207+
});
208+
209+
expect(result.current.currentSelections).toEqual([]);
210+
});
211+
171212
it('should reset selections and history when fetching first step', async () => {
172213
const firstStepResponse = buildStepResponse({ currentStep: 'region' });
173214
const secondStepResponse = buildStepResponse({
@@ -286,6 +327,37 @@ describe('useBenchmarkWizardNavigation', () => {
286327
expect(result.current.currentSelections).toEqual([]);
287328
});
288329

330+
it('should initialize next step selections from preselectedOptions', async () => {
331+
const firstStep = buildStepResponse({
332+
currentStep: 'region',
333+
nextStep: 'instance-type',
334+
});
335+
const secondStep = buildStepResponse({
336+
currentStep: 'instance-type',
337+
nextStep: 'confirm',
338+
selectionType: 'multi-select',
339+
preselectedOptions: ['t3.medium', 't3.large'],
340+
});
341+
mockGetWizardStep
342+
.mockResolvedValueOnce(firstStep)
343+
.mockResolvedValueOnce(secondStep);
344+
345+
const { result } = renderHook(() =>
346+
useBenchmarkWizardNavigation(connectedProviders),
347+
);
348+
349+
act(() => result.current.setSelectedProvider('aws'));
350+
await act(async () => await result.current.handleNextFromInitial());
351+
352+
act(() => result.current.setCurrentSelections(['us-east-1']));
353+
await act(async () => await result.current.handleNextFromProviderStep());
354+
355+
expect(result.current.currentSelections).toEqual([
356+
't3.medium',
357+
't3.large',
358+
]);
359+
});
360+
289361
it('should accumulate history across multiple steps', async () => {
290362
const regionStep = buildStepResponse({
291363
currentStep: 'region',

packages/react-ui/src/app/features/benchmark/use-benchmark-wizard-navigation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export const useBenchmarkWizardNavigation = (
9292
request: {},
9393
});
9494
setCurrentStepResponse(stepResponse);
95-
setCurrentSelections([]);
95+
setCurrentSelections(stepResponse.preselectedOptions ?? []);
9696
setStepHistory([]);
9797
setWizardPhase('provider-step');
9898
};
@@ -127,7 +127,7 @@ export const useBenchmarkWizardNavigation = (
127127

128128
setStepHistory(newHistory);
129129
setCurrentStepResponse(nextStepResponse);
130-
setCurrentSelections([]);
130+
setCurrentSelections(nextStepResponse.preselectedOptions ?? []);
131131
};
132132

133133
const handleEditSetup = () => {

packages/server/api/src/app/benchmark/provider-adapter.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export type WizardConfigStep = {
3535
optionsSource?: WizardStepOptionsSource;
3636
nextStep?: string;
3737
conditional?: WizardStepConditional;
38+
selectAll?: boolean;
3839
};
3940

4041
export type WizardConfig = {

packages/server/api/src/app/benchmark/providers/aws/aws.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"id": "workflows",
3939
"title": "Which AWS workflows should we run to analyze the data?",
4040
"selectionType": "multi-select",
41+
"selectAll": true,
4142
"optionsSource": {
4243
"type": "static",
4344
"values": [

packages/server/api/src/app/benchmark/wizard.service.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,17 @@ export async function resolveWizardNavigation(
160160

161161
const { totalSteps, stepIndex } = getStepProgress(config, stepToShow);
162162

163+
if (stepToShow.selectAll && stepToShow.selectionType !== 'multi-select') {
164+
throwValidationError(
165+
`Step "${stepToShow.id}" has selectAll: true but selectionType is not "multi-select".`,
166+
);
167+
}
168+
169+
const preselectedOptions =
170+
stepToShow.selectAll && options.length > 0
171+
? options.map((o) => o.id)
172+
: undefined;
173+
163174
return {
164175
currentStep: stepToShow.id,
165176
title: stepToShow.title,
@@ -169,5 +180,6 @@ export async function resolveWizardNavigation(
169180
options,
170181
totalSteps,
171182
stepIndex,
183+
preselectedOptions,
172184
};
173185
}

packages/server/api/test/unit/benchmark/wizard.service.test.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,118 @@ describe('resolveWizardNavigation', () => {
233233
);
234234
});
235235

236+
describe('selectAll', () => {
237+
it('returns preselectedOptions with all option ids when selectAll is true and options exist', async () => {
238+
const selectAllConfig = {
239+
provider: 'selectall',
240+
steps: [
241+
{
242+
id: 'step1',
243+
title: 'Pick all',
244+
selectionType: 'multi-select' as const,
245+
selectAll: true,
246+
optionsSource: {
247+
type: 'static' as const,
248+
values: [
249+
{ id: 'opt1', displayName: 'Option 1' },
250+
{ id: 'opt2', displayName: 'Option 2' },
251+
],
252+
},
253+
},
254+
],
255+
};
256+
const selectAllAdapter: ProviderAdapter = {
257+
config: selectAllConfig,
258+
resolveOptions: mockResolveOptions,
259+
evaluateCondition: mockEvaluateCondition,
260+
};
261+
adapters.set('selectall', selectAllAdapter);
262+
263+
try {
264+
const result = await resolveWizardNavigation(
265+
'selectall',
266+
{},
267+
TEST_PROJECT_ID,
268+
);
269+
expect(result.preselectedOptions).toEqual(['opt1', 'opt2']);
270+
} finally {
271+
adapters.delete('selectall');
272+
}
273+
});
274+
275+
it('returns preselectedOptions as undefined when selectAll is true but options are empty', async () => {
276+
const selectAllEmptyConfig = {
277+
provider: 'selectall-empty',
278+
steps: [
279+
{
280+
id: 'step1',
281+
title: 'Pick all',
282+
selectionType: 'multi-select' as const,
283+
selectAll: true,
284+
optionsSource: { type: 'dynamic' as const, method: 'listOptions' },
285+
},
286+
],
287+
};
288+
mockResolveOptions.mockResolvedValue([]);
289+
const selectAllEmptyAdapter: ProviderAdapter = {
290+
config: selectAllEmptyConfig,
291+
resolveOptions: mockResolveOptions,
292+
evaluateCondition: mockEvaluateCondition,
293+
};
294+
adapters.set('selectall-empty', selectAllEmptyAdapter);
295+
296+
try {
297+
const result = await resolveWizardNavigation(
298+
'selectall-empty',
299+
{},
300+
TEST_PROJECT_ID,
301+
);
302+
expect(result.preselectedOptions).toBeUndefined();
303+
} finally {
304+
adapters.delete('selectall-empty');
305+
}
306+
});
307+
308+
it('returns preselectedOptions as undefined when selectAll is not set', async () => {
309+
const result = await resolveWizardNavigation('test', {}, TEST_PROJECT_ID);
310+
expect(result.preselectedOptions).toBeUndefined();
311+
});
312+
313+
it('throws a validation error when selectAll is true but selectionType is single', async () => {
314+
const invalidConfig = {
315+
provider: 'selectall-single',
316+
steps: [
317+
{
318+
id: 'step1',
319+
title: 'Pick one',
320+
selectionType: 'single' as const,
321+
selectAll: true,
322+
optionsSource: {
323+
type: 'static' as const,
324+
values: [{ id: 'opt1', displayName: 'Option 1' }],
325+
},
326+
},
327+
],
328+
};
329+
330+
const invalidAdapter: ProviderAdapter = {
331+
config: invalidConfig,
332+
resolveOptions: mockResolveOptions,
333+
evaluateCondition: mockEvaluateCondition,
334+
};
335+
336+
adapters.set('selectall-single', invalidAdapter);
337+
338+
try {
339+
await expect(
340+
resolveWizardNavigation('selectall-single', {}, TEST_PROJECT_ID),
341+
).rejects.toThrow();
342+
} finally {
343+
adapters.delete('selectall-single');
344+
}
345+
});
346+
});
347+
236348
it('throws when conditional step is not last step and onFailure.skipToStep is not set', async () => {
237349
mockEvaluateCondition.mockResolvedValue(false);
238350
const misconfigConfig = {

packages/shared/src/lib/benchmark/dto/wizard-response.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const BenchmarkWizardStepResponse = Type.Object({
2121
options: Type.Array(BenchmarkWizardOption),
2222
stepIndex: Type.Number(),
2323
totalSteps: Type.Number(),
24+
preselectedOptions: Type.Optional(Type.Array(Type.String())),
2425
});
2526

2627
export type BenchmarkWizardStepResponse = Static<

0 commit comments

Comments
 (0)