vault/ui/app/components/wizard/namespaces/namespace-wizard.ts
Vault Automation 16f98c11ce
[UI] Dismiss Wizards in Playwright Tests (#12699) (#12728)
* adds constants util for wizards and updates service to use WizardId type

* updates wizards to use WIZARD_ID_MAP values

* updates wizard tests to use the service for dismissal

* updates playwright setup to add all wizard ids as dismissed in localStorage

* removes wizard dismissal step from existing playwright tests

* fixes issues accessing owner in beforeEach hooks of namespaces acceptance tests

Co-authored-by: Jordan Reimer <zofskeez@gmail.com>
2026-03-20 15:51:44 -04:00

186 lines
5.7 KiB
TypeScript

/**
* Copyright IBM Corp. 2016, 2025
* SPDX-License-Identifier: BUSL-1.1
*/
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/components/wizard/namespaces/step-3';
import { WIZARD_ID_MAP } from 'vault/utils/constants/wizard';
import type ApiService from 'vault/services/api';
import type Block from 'vault/components/wizard/namespaces/step-2';
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';
const DEFAULT_STEPS = [
{ 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' },
];
interface Args {
isIntroModal: boolean;
onRefresh: CallableFunction;
onFlexiblePolicyComplete: CallableFunction;
}
interface WizardState {
securityPolicyChoice: SecurityPolicy | null;
namespacePaths: string[] | null;
namespaceBlocks: Block[] | null;
creationMethod: CreationMethod | null;
codeSnippet: string | null;
}
export default class WizardNamespacesWizardComponent extends Component<Args> {
@service declare readonly api: ApiService;
@service declare readonly router: RouterService;
@service declare readonly flashMessages: FlashMessageService;
@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;
// Whether the current step requirements have been met to proceed to the next step
get canProceed() {
switch (this.currentStep) {
case 0: // Step 1 - requires security policy choice
return Boolean(this.wizardState.securityPolicyChoice);
case 1: // Step 2 - requires valid namespace inputs
return Boolean(this.wizardState.namespacePaths);
case 2: // Step 3 - no validation is needed
return true;
default:
return true;
}
}
get isFinalStep() {
return this.currentStep === this.steps.length - 1;
}
get shouldShowExitButton() {
// Show exit button unless we're on the final step with UI creation method
return !(this.wizardState.creationMethod === CreationMethod.UI && this.isFinalStep);
}
get exitText() {
return this.isFinalStep && this.wizardState.securityPolicyChoice === SecurityPolicy.STRICT
? 'Done & Exit'
: 'Exit';
}
updateSteps() {
if (this.wizardState.securityPolicyChoice === SecurityPolicy.FLEXIBLE) {
this.steps = [
{ title: 'Select setup', component: 'wizard/namespaces/step-1' },
{ title: 'Apply changes', component: 'wizard/namespaces/step-3' },
];
} else {
this.steps = DEFAULT_STEPS;
}
}
@action
onStepChange(step: number) {
this.currentStep = step;
// if user policy selection changes which steps we show, update upon page navigation
// instead of flashing the changes when toggling
this.updateSteps();
}
@action
updateWizardState(key: string, value: unknown) {
this.wizardState = {
...this.wizardState,
[key]: value,
};
}
@action
async onDone() {
await this.onDismiss();
this.args.onFlexiblePolicyComplete();
this.flashMessages.success(`Your current setup is 1 namespace.`, { title: 'Guided start complete' });
}
@action
async onDismiss() {
this.wizard.dismiss(this.wizardId);
await this.args.onRefresh();
}
@action
async onSubmit() {
switch (this.wizardState.creationMethod) {
case CreationMethod.UI:
await this.createNamespacesFromWizard();
break;
default:
// The other creation methods require the user to execute the commands on their own
// In these cases, there is no submit button
break;
}
}
@action
onIntroChange(visible: boolean) {
this.wizard.setIntroVisible(this.wizardId, visible);
}
@action
async createNamespacesFromWizard() {
try {
const { namespacePaths } = this.wizardState;
if (!namespacePaths) return;
for (const nsPath of namespacePaths) {
const parts = nsPath.split('/');
const namespaceName = parts[parts.length - 1] as string;
const parentPath = parts.length > 1 ? parts.slice(0, -1).join('/') : undefined;
// this provides the full nested path for the header
const fullPath = parentPath ? this.namespace.path + '/' + parentPath : undefined;
await this.createNamespace(namespaceName, fullPath);
}
this.flashMessages.success('Your new configuration has been applied.', { title: 'Namespaces created' });
} catch (error) {
const { message } = await this.api.parseError(error);
this.flashMessages.danger(`Error creating namespaces: ${message}`);
} finally {
this.onDismiss();
}
}
@action
switchNamespace(targetNamespace: string) {
this.router.transitionTo('vault.cluster.dashboard', {
queryParams: { namespace: targetNamespace },
});
}
async createNamespace(path: string, header?: string) {
const headers = header ? this.api.buildHeaders({ namespace: header }) : undefined;
await this.api.sys.systemWriteNamespacesPath(path, {}, headers);
}
}