mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-28 04:10:44 -04:00
Co-authored-by: Nina Bucholtz <nina.balachandranmary@gmail.com>
This commit is contained in:
parent
3a3b6a3725
commit
86e46a1691
3 changed files with 267 additions and 22 deletions
|
|
@ -5,7 +5,6 @@
|
|||
|
||||
import { service } from '@ember/service';
|
||||
import { action } from '@ember/object';
|
||||
import { tracked } from '@glimmer/tracking';
|
||||
import Component from '@glimmer/component';
|
||||
import { SecurityPolicy } from 'vault/components/wizard/namespaces/step-1';
|
||||
import { CreationMethod } from 'vault/utils/constants/snippet';
|
||||
|
|
@ -17,8 +16,9 @@ import type FlashMessageService from 'vault/services/flash-messages';
|
|||
import type NamespaceService from 'vault/services/namespace';
|
||||
import type RouterService from '@ember/routing/router-service';
|
||||
import type WizardService from 'vault/services/wizard';
|
||||
import type { StepConfig } from 'vault/services/wizard';
|
||||
|
||||
const DEFAULT_STEPS = [
|
||||
const DEFAULT_STEPS: StepConfig[] = [
|
||||
{ title: 'Select setup', component: 'wizard/namespaces/step-1' },
|
||||
{ title: 'Map out namespaces', component: 'wizard/namespaces/step-2' },
|
||||
{ title: 'Apply changes', component: 'wizard/namespaces/step-3' },
|
||||
|
|
@ -38,6 +38,14 @@ interface WizardState {
|
|||
codeSnippet: string | null;
|
||||
}
|
||||
|
||||
const DEFAULT_WIZARD_STATE: WizardState = {
|
||||
securityPolicyChoice: null,
|
||||
namespacePaths: null,
|
||||
namespaceBlocks: null,
|
||||
creationMethod: null,
|
||||
codeSnippet: null,
|
||||
};
|
||||
|
||||
export default class WizardNamespacesWizardComponent extends Component<Args> {
|
||||
@service declare readonly api: ApiService;
|
||||
@service declare readonly router: RouterService;
|
||||
|
|
@ -45,21 +53,24 @@ export default class WizardNamespacesWizardComponent extends Component<Args> {
|
|||
@service declare readonly wizard: WizardService;
|
||||
@service declare namespace: NamespaceService;
|
||||
|
||||
@tracked currentStep = 0;
|
||||
@tracked steps = DEFAULT_STEPS;
|
||||
@tracked wizardState: WizardState = {
|
||||
securityPolicyChoice: null,
|
||||
namespacePaths: null,
|
||||
namespaceBlocks: null,
|
||||
creationMethod: null,
|
||||
codeSnippet: null,
|
||||
};
|
||||
|
||||
methods = CreationMethod;
|
||||
policy = SecurityPolicy;
|
||||
|
||||
wizardId = WIZARD_ID_MAP.namespace;
|
||||
|
||||
get currentStep() {
|
||||
return this.wizard.getCurrentStep(this.wizardId);
|
||||
}
|
||||
|
||||
get steps() {
|
||||
const steps = this.wizard.getSteps(this.wizardId);
|
||||
return steps.length > 0 ? steps : DEFAULT_STEPS;
|
||||
}
|
||||
|
||||
get wizardState(): WizardState {
|
||||
return { ...DEFAULT_WIZARD_STATE, ...this.wizard.getState<Partial<WizardState>>(this.wizardId) };
|
||||
}
|
||||
|
||||
// Whether the current step requirements have been met to proceed to the next step
|
||||
get canProceed() {
|
||||
switch (this.currentStep) {
|
||||
|
|
@ -75,7 +86,7 @@ export default class WizardNamespacesWizardComponent extends Component<Args> {
|
|||
}
|
||||
|
||||
get isFinalStep() {
|
||||
return this.currentStep === this.steps.length - 1;
|
||||
return this.wizard.isFinalStep(this.wizardId);
|
||||
}
|
||||
|
||||
get shouldShowExitButton() {
|
||||
|
|
@ -91,18 +102,18 @@ export default class WizardNamespacesWizardComponent extends Component<Args> {
|
|||
|
||||
updateSteps() {
|
||||
if (this.wizardState.securityPolicyChoice === SecurityPolicy.FLEXIBLE) {
|
||||
this.steps = [
|
||||
this.wizard.setSteps(this.wizardId, [
|
||||
{ title: 'Select setup', component: 'wizard/namespaces/step-1' },
|
||||
{ title: 'Apply changes', component: 'wizard/namespaces/step-3' },
|
||||
];
|
||||
]);
|
||||
} else {
|
||||
this.steps = DEFAULT_STEPS;
|
||||
this.wizard.setSteps(this.wizardId, DEFAULT_STEPS);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onStepChange(step: number) {
|
||||
this.currentStep = step;
|
||||
this.wizard.setCurrentStep(this.wizardId, step);
|
||||
// if user policy selection changes which steps we show, update upon page navigation
|
||||
// instead of flashing the changes when toggling
|
||||
this.updateSteps();
|
||||
|
|
@ -110,10 +121,7 @@ export default class WizardNamespacesWizardComponent extends Component<Args> {
|
|||
|
||||
@action
|
||||
updateWizardState(key: string, value: unknown) {
|
||||
this.wizardState = {
|
||||
...this.wizardState,
|
||||
[key]: value,
|
||||
};
|
||||
this.wizard.updateState(this.wizardId, key, value);
|
||||
}
|
||||
|
||||
@action
|
||||
|
|
@ -126,6 +134,7 @@ export default class WizardNamespacesWizardComponent extends Component<Args> {
|
|||
@action
|
||||
async onDismiss() {
|
||||
this.wizard.dismiss(this.wizardId);
|
||||
this.wizard.clearWizardState(this.wizardId);
|
||||
await this.args.onRefresh();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,22 @@ import { DISMISSED_WIZARD_KEY } from 'vault/utils/constants/wizard';
|
|||
|
||||
import type { WizardId } from 'vault/app-types';
|
||||
|
||||
export interface StepConfig {
|
||||
title: string;
|
||||
component: string;
|
||||
}
|
||||
|
||||
// Unique identifier for each wizard, used to track dismissal state and step state.
|
||||
export type WizardID = string;
|
||||
|
||||
// Dynamic state storage for each wizard defined at the component level.
|
||||
// This allows for flexible state management across different wizards without
|
||||
// needing to predefine specific properties for each wizard in the service.
|
||||
export type WizardState = Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* WizardService manages the state of wizards across the application,
|
||||
* particularly tracking which wizards have been dismissed by the user.
|
||||
* including tracking which wizards have been dismissed by the user.
|
||||
* This service provides a centralized way to check and update wizard
|
||||
* dismissal state instead of directly accessing localStorage.
|
||||
*/
|
||||
|
|
@ -20,6 +33,11 @@ export default class WizardService extends Service {
|
|||
@tracked dismissedWizards: string[] = this.loadDismissedWizards();
|
||||
@tracked introVisibleState: Record<string, boolean> = {};
|
||||
|
||||
/* Tracked properties for step state management */
|
||||
@tracked private stepData: Record<WizardID, WizardState> = {};
|
||||
@tracked private currentStep: Record<WizardID, number> = {};
|
||||
@tracked private steps: Record<WizardID, StepConfig[]> = {};
|
||||
|
||||
/**
|
||||
* Load dismissed wizards from localStorage
|
||||
*/
|
||||
|
|
@ -94,4 +112,94 @@ export default class WizardService extends Service {
|
|||
[wizardId]: visible,
|
||||
};
|
||||
}
|
||||
|
||||
/* Step state management */
|
||||
|
||||
/**
|
||||
* Retrieve the stored state for a wizard, typed by the caller.
|
||||
* Returns an empty object if no state has been set yet, so consumers
|
||||
* should merge with their own defaults when a fully-typed object is required.
|
||||
* @param wizardId - The unique identifier for the wizard
|
||||
* @returns The wizard's current state, cast to the caller-supplied type
|
||||
*/
|
||||
getState<T extends object>(wizardId: WizardId): T {
|
||||
return (this.stepData[wizardId] ?? {}) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutably update a single key in the wizard's stored state.
|
||||
* Other keys in the state are left unchanged.
|
||||
* @param wizardId - The unique identifier for the wizard
|
||||
* @param key - The state key to update
|
||||
* @param value - The new value for that key
|
||||
*/
|
||||
updateState(wizardId: WizardId, key: string, value: unknown): void {
|
||||
this.stepData = {
|
||||
...this.stepData,
|
||||
[wizardId]: { ...this.stepData[wizardId], [key]: value },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the wizard's step data to an empty object and return navigation to step 0.
|
||||
* The step configuration (registered via setSteps) is intentionally preserved so
|
||||
* the same wizard can be re-entered without re-registering its steps.
|
||||
* @param wizardId - The unique identifier for the wizard
|
||||
*/
|
||||
clearWizardState(wizardId: WizardId): void {
|
||||
this.stepData = { ...this.stepData, [wizardId]: {} };
|
||||
this.currentStep = { ...this.currentStep, [wizardId]: 0 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the index of the currently active step for a wizard.
|
||||
* Defaults to 0 if the wizard has not yet navigated to any step.
|
||||
* @param wizardId - The unique identifier for the wizard
|
||||
* @returns The zero-indexed current step number
|
||||
*/
|
||||
getCurrentStep(wizardId: WizardId): number {
|
||||
return this.currentStep[wizardId] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active step index for a wizard.
|
||||
* @param wizardId - The unique identifier for the wizard
|
||||
* @param step - The zero-indexed step to navigate to
|
||||
*/
|
||||
setCurrentStep(wizardId: WizardId, step: number): void {
|
||||
this.currentStep = { ...this.currentStep, [wizardId]: step };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the registered step configuration array for a wizard.
|
||||
* Returns an empty array if no steps have been registered yet, allowing
|
||||
* consumers to supply their own defaults on first render.
|
||||
* @param wizardId - The unique identifier for the wizard
|
||||
* @returns Array of step configuration objects
|
||||
*/
|
||||
getSteps(wizardId: WizardId): StepConfig[] {
|
||||
return this.steps[wizardId] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the step configuration array for a wizard.
|
||||
* Use this to add, remove, or reorder steps dynamically — for example,
|
||||
* skipping a step based on a user's earlier selection.
|
||||
* @param wizardId - The unique identifier for the wizard
|
||||
* @param steps - The new step configuration to register
|
||||
*/
|
||||
setSteps(wizardId: WizardId, steps: StepConfig[]): void {
|
||||
this.steps = { ...this.steps, [wizardId]: steps };
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true when the current step is the last one in the registered configuration.
|
||||
* Returns false if no steps have been registered yet.
|
||||
* @param wizardId - The unique identifier for the wizard
|
||||
* @returns Whether the wizard is on its final step
|
||||
*/
|
||||
isFinalStep(wizardId: WizardId): boolean {
|
||||
const steps = this.getSteps(wizardId);
|
||||
return steps.length > 0 && this.getCurrentStep(wizardId) === steps.length - 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ import sinon from 'sinon';
|
|||
import localStorage from 'vault/lib/local-storage';
|
||||
import { DISMISSED_WIZARD_KEY } from 'vault/utils/constants/wizard';
|
||||
|
||||
const STEPS = [
|
||||
{ title: 'Step 1', component: 'wizard/step-1' },
|
||||
{ title: 'Step 2', component: 'wizard/step-2' },
|
||||
{ title: 'Step 3', component: 'wizard/step-3' },
|
||||
];
|
||||
|
||||
module('Unit | Service | wizard', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
|
|
@ -231,4 +237,126 @@ module('Unit | Service | wizard', function (hooks) {
|
|||
assert.false(this.service.isIntroVisible('wizard3'), 'wizard3 is not visible');
|
||||
});
|
||||
});
|
||||
|
||||
/* Step state management */
|
||||
module('#getState / #updateState', function () {
|
||||
test('getState returns empty object for uninitialized wizard', function (assert) {
|
||||
assert.deepEqual(this.service.getState('namespace'), {}, 'returns empty object when not set');
|
||||
});
|
||||
|
||||
test('updateState sets a single key without mutating other keys', function (assert) {
|
||||
this.service.updateState('namespace', 'choice', 'strict');
|
||||
|
||||
assert.strictEqual(this.service.getState('namespace').choice, 'strict', 'updates the specified key');
|
||||
});
|
||||
|
||||
test('successive updates accumulate correctly', function (assert) {
|
||||
this.service.updateState('namespace', 'choice', 'strict');
|
||||
this.service.updateState('namespace', 'data', ['ns1', 'ns2']);
|
||||
|
||||
const state = this.service.getState('namespace');
|
||||
assert.strictEqual(state.choice, 'strict', 'first update persists');
|
||||
assert.deepEqual(state.data, ['ns1', 'ns2'], 'second update persists');
|
||||
});
|
||||
|
||||
test('updating one wizard does not affect another', function (assert) {
|
||||
this.service.updateState('namespace', 'choice', 'strict');
|
||||
|
||||
assert.strictEqual(this.service.getState('namespace').choice, 'strict', 'namespace updated');
|
||||
assert.strictEqual(this.service.getState('acl-policy').choice, undefined, 'acl-policy unchanged');
|
||||
});
|
||||
});
|
||||
|
||||
module('#clearWizardState', function () {
|
||||
test('resets state to empty object and step to 0', function (assert) {
|
||||
this.service.updateState('namespace', 'choice', 'strict');
|
||||
this.service.setCurrentStep('namespace', 2);
|
||||
|
||||
this.service.clearWizardState('namespace');
|
||||
|
||||
assert.deepEqual(this.service.getState('namespace'), {}, 'state is empty after clear');
|
||||
assert.strictEqual(this.service.getCurrentStep('namespace'), 0, 'step resets to 0');
|
||||
});
|
||||
|
||||
test('preserves step configuration after clear', function (assert) {
|
||||
this.service.setSteps('namespace', STEPS);
|
||||
|
||||
this.service.clearWizardState('namespace');
|
||||
|
||||
assert.deepEqual(this.service.getSteps('namespace'), STEPS, 'step config is preserved');
|
||||
});
|
||||
});
|
||||
|
||||
module('#getCurrentStep / #setCurrentStep', function () {
|
||||
test('returns 0 for uninitialized wizard', function (assert) {
|
||||
assert.strictEqual(this.service.getCurrentStep('namespace'), 0, 'defaults to 0');
|
||||
});
|
||||
|
||||
test('setCurrentStep updates the active step', function (assert) {
|
||||
this.service.setCurrentStep('namespace', 2);
|
||||
|
||||
assert.strictEqual(this.service.getCurrentStep('namespace'), 2, 'step is updated');
|
||||
});
|
||||
|
||||
test('step changes for one wizard do not affect another', function (assert) {
|
||||
this.service.setCurrentStep('namespace', 1);
|
||||
|
||||
assert.strictEqual(this.service.getCurrentStep('namespace'), 1, 'namespace at step 1');
|
||||
assert.strictEqual(this.service.getCurrentStep('acl-policy'), 0, 'acl-policy still at step 0');
|
||||
});
|
||||
});
|
||||
|
||||
module('#getSteps / #setSteps', function () {
|
||||
test('getSteps returns empty array for uninitialized wizard', function (assert) {
|
||||
assert.deepEqual(this.service.getSteps('namespace'), [], 'returns empty array');
|
||||
});
|
||||
|
||||
test('setSteps replaces the step configuration', function (assert) {
|
||||
this.service.setSteps('namespace', STEPS);
|
||||
|
||||
assert.deepEqual(this.service.getSteps('namespace'), STEPS, 'step config is stored');
|
||||
});
|
||||
|
||||
test('setSteps for one wizard does not affect another', function (assert) {
|
||||
this.service.setSteps('namespace', STEPS);
|
||||
|
||||
assert.strictEqual(this.service.getSteps('namespace').length, 3, 'namespace has 3 steps');
|
||||
assert.deepEqual(this.service.getSteps('acl-policy'), [], 'acl-policy still has no steps');
|
||||
});
|
||||
});
|
||||
|
||||
module('#isFinalStep', function () {
|
||||
test('returns false for uninitialized wizard (no steps)', function (assert) {
|
||||
assert.false(this.service.isFinalStep('namespace'), 'returns false when no steps registered');
|
||||
});
|
||||
|
||||
test('returns false when not on the last step', function (assert) {
|
||||
this.service.setSteps('namespace', STEPS);
|
||||
|
||||
assert.false(this.service.isFinalStep('namespace'), 'returns false on step 0 of 3');
|
||||
|
||||
this.service.setCurrentStep('namespace', 1);
|
||||
assert.false(this.service.isFinalStep('namespace'), 'returns false on step 1 of 3');
|
||||
});
|
||||
|
||||
test('returns true when on the last step', function (assert) {
|
||||
this.service.setSteps('namespace', STEPS);
|
||||
this.service.setCurrentStep('namespace', 2);
|
||||
|
||||
assert.true(this.service.isFinalStep('namespace'), 'returns true on final step');
|
||||
});
|
||||
|
||||
test('reflects the new final step after setSteps reduces the step count', function (assert) {
|
||||
this.service.setSteps('namespace', STEPS);
|
||||
this.service.setCurrentStep('namespace', 1);
|
||||
|
||||
// Reduce to 2 steps — step 1 is now the final step
|
||||
this.service.setSteps('namespace', [STEPS[0], STEPS[2]]);
|
||||
|
||||
assert.true(
|
||||
this.service.isFinalStep('namespace'),
|
||||
'correctly identifies new final step after setSteps'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue