Skip to content

Commit c499aa5

Browse files
hc-github-team-secure-vault-corelane-wetmorehellobontempo
authored
UI: Namespace Wizard (#11556) (#12053)
* fill guided start content * move namespace logic into page component * add page component tests for namespace wizard * add tree chart and changelog, update state management * fix failing page usage test * add back in breadcrumb update lost in merge conflict resolution across files * fix test * update terraform template function usage * Update ui/app/components/wizard/namespaces/step-3.hbs * formatting and fixes * revert usage page changes * move snippet generators into util and update code snippet initialization * update test namespace page args * move namespace wizard logic into its own component * fix nested namespace creation via api and cli code snippets * test update * nested namespace terraform snippet * remove outdated comment * test clean up and hide wizard in CE --------- Co-authored-by: lane-wetmore <[email protected]> Co-authored-by: claire bontempo <[email protected]>
1 parent e3cdfec commit c499aa5

File tree

29 files changed

+1826
-216
lines changed

29 files changed

+1826
-216
lines changed

changelog/_11556.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:feature
2+
**UI Namespace Wizard (Enterprise)**: Onboarding wizard which provides advice to users based on their intended usage and guides them through namespace creation.
3+
```

ui/app/components/page/namespaces.hbs

Lines changed: 94 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -4,88 +4,102 @@
44
}}
55

66
{{#if (has-feature "Namespaces")}}
7-
<Page::Header @title="Namespaces">
8-
<:breadcrumbs>
9-
<Page::Breadcrumbs
10-
@breadcrumbs={{array (hash label="Vault" route="vault.cluster.dashboard" icon="vault") (hash label="Namespaces")}}
11-
/>
12-
</:breadcrumbs>
13-
</Page::Header>
14-
15-
<Toolbar>
16-
<ToolbarFilters>
17-
<FilterInputExplicit
18-
@query={{@model.pageFilter}}
19-
@placeholder="Search"
20-
@handleSearch={{this.handleSearch}}
21-
@handleInput={{this.handleInput}}
22-
@handleKeyDown={{this.handleKeyDown}}
23-
/>
24-
</ToolbarFilters>
25-
<ToolbarActions>
26-
<Hds::Button
27-
class="has-right-margin-4"
28-
@color="secondary"
29-
@icon="reload"
30-
@iconPosition="trailing"
31-
@text="Refresh list"
32-
{{on "click" this.refreshNamespaceList}}
33-
data-test-button="refresh-namespace-list"
34-
/>
35-
<ToolbarLink @route="vault.cluster.access.namespaces.create" @type="add" data-test-link-to="create-namespace">
36-
Create namespace
37-
</ToolbarLink>
38-
</ToolbarActions>
39-
</Toolbar>
7+
{{#if this.showWizard}}
8+
<Wizard::Namespaces::NamespaceWizard
9+
@onDismiss={{fn (mut this.hasDismissedWizard) true}}
10+
@onRefresh={{this.refreshNamespaceList}}
11+
/>
12+
{{else}}
13+
<Page::Header @title="Namespaces">
14+
<:breadcrumbs>
15+
<Page::Breadcrumbs
16+
@breadcrumbs={{array (hash label="Vault" route="vault.cluster.dashboard" icon="vault") (hash label="Namespaces")}}
17+
/>
18+
</:breadcrumbs>
19+
</Page::Header>
4020

41-
<ListView
42-
@items={{@model.namespaces}}
43-
@itemNoun="namespace"
44-
@paginationRouteName="vault.cluster.access.namespaces"
45-
@onPageChange={{this.handlePageChange}}
46-
as |list|
47-
>
48-
{{#if @model.namespaces.length}}
49-
<ListItem as |Item|>
50-
<Item.content>
51-
{{list.item.id}}
52-
</Item.content>
53-
<Item.menu>
54-
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
55-
<dd.ToggleIcon @icon="more-horizontal" @text="More options" @hasChevron={{false}} data-test-popup-menu-trigger />
56-
{{#let (concat this.namespace.path (if this.namespace.path "/") list.item.id) as |targetNamespace|}}
57-
{{#if (includes targetNamespace this.namespace.accessibleNamespaces)}}
58-
<dd.Interactive {{on "click" (fn this.switchNamespace targetNamespace)}} data-test-popup-menu="switch">Switch
59-
to namespace</dd.Interactive>
60-
{{/if}}
61-
{{/let}}
62-
<dd.Interactive
63-
@color="critical"
64-
{{on "click" (fn (mut this.nsToDelete) list.item)}}
65-
data-test-popup-menu="delete"
66-
>Delete</dd.Interactive>
67-
</Hds::Dropdown>
68-
{{#if (eq this.nsToDelete list.item)}}
69-
<ConfirmModal
70-
@color="critical"
71-
@onClose={{fn (mut this.nsToDelete) null}}
72-
@onConfirm={{fn this.deleteNamespace list.item}}
73-
@confirmTitle="Delete this namespace?"
74-
@confirmMessage="Any engines or mounts in this namespace will also be removed."
75-
/>
76-
{{/if}}
77-
</Item.menu>
78-
</ListItem>
79-
{{else}}
80-
<list.empty>
81-
<Hds::Link::Standalone
82-
@icon="learn-link"
83-
@text="Secure multi-tenancy with namespaces tutorial"
84-
@href={{doc-link "/vault/tutorials/enterprise/namespaces"}}
21+
<Toolbar>
22+
<ToolbarFilters>
23+
<FilterInputExplicit
24+
@query={{@model.pageFilter}}
25+
@placeholder="Search"
26+
@handleSearch={{this.handleSearch}}
27+
@handleInput={{this.handleInput}}
28+
@handleKeyDown={{this.handleKeyDown}}
8529
/>
86-
</list.empty>
87-
{{/if}}
88-
</ListView>
30+
</ToolbarFilters>
31+
<ToolbarActions>
32+
<Hds::Button
33+
class="has-right-margin-4"
34+
@color="secondary"
35+
@icon="reload"
36+
@iconPosition="trailing"
37+
@text="Refresh list"
38+
{{on "click" this.refreshNamespaceList}}
39+
data-test-button="refresh-namespace-list"
40+
/>
41+
<ToolbarLink @route="vault.cluster.access.namespaces.create" @type="add" data-test-link-to="create-namespace">
42+
Create namespace
43+
</ToolbarLink>
44+
</ToolbarActions>
45+
</Toolbar>
46+
47+
<ListView
48+
@items={{@model.namespaces}}
49+
@itemNoun="namespace"
50+
@paginationRouteName="vault.cluster.access.namespaces"
51+
@onPageChange={{this.handlePageChange}}
52+
as |list|
53+
>
54+
{{#if @model.namespaces.length}}
55+
<ListItem as |Item|>
56+
<Item.content>
57+
{{list.item.id}}
58+
</Item.content>
59+
<Item.menu>
60+
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
61+
<dd.ToggleIcon
62+
@icon="more-horizontal"
63+
@text="More options"
64+
@hasChevron={{false}}
65+
data-test-popup-menu-trigger
66+
/>
67+
{{#let (concat this.namespace.path (if this.namespace.path "/") list.item.id) as |targetNamespace|}}
68+
{{#if (includes targetNamespace this.namespace.accessibleNamespaces)}}
69+
<dd.Interactive
70+
{{on "click" (fn this.switchNamespace targetNamespace)}}
71+
data-test-popup-menu="switch"
72+
>Switch to namespace</dd.Interactive>
73+
{{/if}}
74+
{{/let}}
75+
<dd.Interactive
76+
@color="critical"
77+
{{on "click" (fn (mut this.nsToDelete) list.item)}}
78+
data-test-popup-menu="delete"
79+
>Delete</dd.Interactive>
80+
</Hds::Dropdown>
81+
{{#if (eq this.nsToDelete list.item)}}
82+
<ConfirmModal
83+
@color="critical"
84+
@onClose={{fn (mut this.nsToDelete) null}}
85+
@onConfirm={{fn this.deleteNamespace list.item}}
86+
@confirmTitle="Delete this namespace?"
87+
@confirmMessage="Any engines or mounts in this namespace will also be removed."
88+
/>
89+
{{/if}}
90+
</Item.menu>
91+
</ListItem>
92+
{{else}}
93+
<list.empty>
94+
<Hds::Link::Standalone
95+
@icon="learn-link"
96+
@text="Secure multi-tenancy with namespaces tutorial"
97+
@href={{doc-link "/vault/tutorials/enterprise/namespaces"}}
98+
/>
99+
</list.empty>
100+
{{/if}}
101+
</ListView>
102+
{{/if}}
89103
{{else}}
90104
<UpgradePage @title="Namespaces" @minimumEdition="Vault Enterprise Pro" />
91105
{{/if}}

ui/app/components/page/namespaces.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type FlashMessageService from 'vault/services/flash-messages';
1414
import type NamespaceService from 'vault/services/namespace';
1515
import type RouterService from '@ember/routing/router-service';
1616
import type { HTMLElementEvent } from 'vault/forms';
17+
import { DISMISSED_WIZARD_KEY } from '../wizard';
1718

1819
/**
1920
* @module PageNamespaces
@@ -45,19 +46,31 @@ export default class PageNamespacesComponent extends Component<Args> {
4546
@service declare readonly api: ApiService;
4647
@service declare readonly router: RouterService;
4748
@service declare readonly flashMessages: FlashMessageService;
48-
// Use namespaceService alias to avoid collision with namespaces
49-
// input parameter from the route.
5049
@service declare namespace: NamespaceService;
5150

5251
// The `query` property is used to track the filter
5352
// input value separately from updating the `pageFilter`
5453
// browser query param to prevent unnecessary re-renders.
5554
@tracked query;
5655
@tracked nsToDelete = null;
56+
@tracked hasDismissedWizard = false;
57+
58+
wizardId = 'namespace';
5759

5860
constructor(owner: unknown, args: Args) {
5961
super(owner, args);
6062
this.query = this.args.model.pageFilter || '';
63+
64+
// check if the wizard has already been dismissed
65+
const dismissedWizards = localStorage.getItem(DISMISSED_WIZARD_KEY);
66+
if (dismissedWizards?.includes(this.wizardId)) {
67+
this.hasDismissedWizard = true;
68+
}
69+
}
70+
71+
get showWizard() {
72+
// Show when there are no existing namespaces and it is not in a dismissed state
73+
return !this.hasDismissedWizard && !this.args.model.namespaces?.length;
6174
}
6275

6376
@action
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{{!
2+
Copyright IBM Corp. 2016, 2025
3+
SPDX-License-Identifier: BUSL-1.1
4+
}}
5+
6+
<Page::Header @title="Namespaces Guided Start" />
7+
8+
<div class="wizard" data-test-guided-setup>
9+
<Hds::Stepper::Nav
10+
class="has-top-margin-xl has-bottom-margin-xl is-flex-column is-flex-grow-1"
11+
@isInteractive={{true}}
12+
@onStepChange={{this.onNavStepChange}}
13+
@currentStep={{@currentStep}}
14+
@steps={{@steps}}
15+
as |S|
16+
>
17+
{{#each @steps as |step|}}
18+
<S.Step>
19+
<:title>{{step.title}}</:title>
20+
</S.Step>
21+
22+
<S.Panel class="content" data-test-content>
23+
{{#let (component step.component) as |StepComponent|}}
24+
<StepComponent @wizardState={{@wizardState}} @updateWizardState={{@updateWizardState}} />
25+
{{/let}}
26+
</S.Panel>
27+
{{/each}}
28+
</Hds::Stepper::Nav>
29+
30+
<div class="button-bar">
31+
{{#if (gt @currentStep 0)}}
32+
<Hds::Button
33+
@text="Back"
34+
@color="tertiary"
35+
@icon="chevron-left"
36+
{{on "click" (fn this.onStepChange -1)}}
37+
data-test-back-button
38+
/>
39+
{{/if}}
40+
41+
<Hds::ButtonSet>
42+
{{yield to="exit"}}
43+
{{#if this.isFinalStep}}
44+
{{yield to="submit"}}
45+
{{else}}
46+
<Hds::Button
47+
@text="Next"
48+
disabled={{not @canProceed}}
49+
{{on "click" (fn this.onStepChange 1)}}
50+
data-test-button="Next"
51+
/>
52+
{{/if}}
53+
</Hds::ButtonSet>
54+
</div>
55+
56+
</div>
Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,9 @@
66
import Component from '@glimmer/component';
77
import { action } from '@ember/object';
88

9-
/**
10-
* @module QuickStart
11-
* QuickStart component holds the wizard content pages and navigation controls.
12-
*
13-
* @example
14-
* <QuickStart @currentStep={{@currentStep}} @steps={{@steps}} @onStepChange={{@onStepChange}} @onDismiss={{@onDismiss}} @hasSubmitBlock={{has-block "submit"}} />
15-
*/
16-
179
interface Args {
1810
/**
19-
* The active step
11+
* The active step. Steps are zero-indexed.
2012
*/
2113
currentStep: number;
2214
/**
@@ -33,30 +25,34 @@ interface Args {
3325
*/
3426
onStepChange: CallableFunction;
3527
/**
36-
* Helper arg to conditionally render a custom submit button upon
37-
* completion of the wizard. Necessary to avoid a nested block error.
28+
* Whether the current step allows proceeding to the next step
3829
*/
39-
hasSubmitBlock: boolean;
30+
canProceed?: boolean;
31+
/**
32+
* State tracked across steps.
33+
*/
34+
wizardState?: unknown;
35+
/**
36+
* Callback to update state tracked across steps.
37+
*/
38+
updateWizardState?: CallableFunction;
4039
}
4140

42-
export default class QuickStart extends Component<Args> {
43-
constructor(owner: unknown, args: Args) {
44-
super(owner, args);
45-
}
46-
41+
export default class GuidedSetup extends Component<Args> {
4742
get isFinalStep() {
4843
return this.args.currentStep === this.args.steps.length - 1;
4944
}
5045

5146
@action
5247
onStepChange(change: number) {
53-
const { currentStep, steps, onStepChange } = this.args;
48+
const { currentStep, onStepChange } = this.args;
5449
const target = currentStep + change;
50+
onStepChange(target);
51+
}
5552

56-
if (target < 0 || target > steps.length - 1) {
57-
onStepChange(currentStep);
58-
} else {
59-
onStepChange(target);
60-
}
53+
@action
54+
onNavStepChange(_event: Event, stepIndex: number) {
55+
const { onStepChange } = this.args;
56+
onStepChange(stepIndex);
6157
}
6258
}

0 commit comments

Comments
 (0)