UI: update namespace empty state (#11924) (#12082)

* update namespace empty state

add in refresh button to empty state and update tests

update button design

* update copy

* update when exit button is shown

* update css class

* revert state changes

Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com>
This commit is contained in:
Vault Automation 2026-01-29 14:19:42 -05:00 committed by GitHub
parent c2034cb08a
commit eb1d3edfb0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 178 additions and 99 deletions

View file

@ -16,90 +16,150 @@
@breadcrumbs={{array (hash label="Vault" route="vault.cluster.dashboard" icon="vault") (hash label="Namespaces")}}
/>
</:breadcrumbs>
<:badges>
<Hds::Badge @icon="org" @text={{this.namespacePath}} data-test-badge="namespace-path" />
</:badges>
<:actions>
{{#unless @model.namespaces}}
<Hds::Button
class="has-right-margin-4"
@color="secondary"
@icon="bulb"
@text="New to Namespaces?"
{{on "click" this.enterGuidedStart}}
data-test-button="guided-start"
/>
<Hds::Button
class="has-right-margin-4"
@icon="plus"
@route="vault.cluster.access.namespaces.create"
@text="Create namespace"
data-test-button="create-namespace"
/>
{{/unless}}
</:actions>
</Page::Header>
<Toolbar>
<ToolbarFilters>
<FilterInputExplicit
@query={{@model.pageFilter}}
@placeholder="Search"
@handleSearch={{this.handleSearch}}
@handleInput={{this.handleInput}}
@handleKeyDown={{this.handleKeyDown}}
/>
</ToolbarFilters>
<ToolbarActions>
<Hds::Button
class="has-right-margin-4"
@color="secondary"
@icon="reload"
@iconPosition="trailing"
@text="Refresh list"
{{on "click" this.refreshNamespaceList}}
data-test-button="refresh-namespace-list"
/>
<ToolbarLink @route="vault.cluster.access.namespaces.create" @type="add" data-test-link-to="create-namespace">
Create namespace
</ToolbarLink>
</ToolbarActions>
</Toolbar>
<ListView
@items={{@model.namespaces}}
@itemNoun="namespace"
@paginationRouteName="vault.cluster.access.namespaces"
@onPageChange={{this.handlePageChange}}
as |list|
>
{{#if @model.namespaces.length}}
<ListItem as |Item|>
<Item.content>
{{list.item.id}}
</Item.content>
<Item.menu>
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="More options"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#let (concat this.namespace.path (if this.namespace.path "/") list.item.id) as |targetNamespace|}}
{{#if (includes targetNamespace this.namespace.accessibleNamespaces)}}
<dd.Interactive
{{on "click" (fn this.switchNamespace targetNamespace)}}
data-test-popup-menu="switch"
>Switch to namespace</dd.Interactive>
{{/if}}
{{/let}}
<dd.Interactive
@color="critical"
{{on "click" (fn (mut this.nsToDelete) list.item)}}
data-test-popup-menu="delete"
>Delete</dd.Interactive>
</Hds::Dropdown>
{{#if (eq this.nsToDelete list.item)}}
<ConfirmModal
@color="critical"
@onClose={{fn (mut this.nsToDelete) null}}
@onConfirm={{fn this.deleteNamespace list.item}}
@confirmTitle="Delete this namespace?"
@confirmMessage="Any engines or mounts in this namespace will also be removed."
/>
{{/if}}
</Item.menu>
</ListItem>
{{else}}
<list.empty>
<Hds::Link::Standalone
@icon="learn-link"
@text="Secure multi-tenancy with namespaces tutorial"
@href={{doc-link "/vault/tutorials/enterprise/namespaces"}}
{{#if @model.namespaces}}
<Toolbar>
<ToolbarFilters>
<FilterInputExplicit
@query={{@model.pageFilter}}
@placeholder="Search"
@handleSearch={{this.handleSearch}}
@handleInput={{this.handleInput}}
@handleKeyDown={{this.handleKeyDown}}
/>
</list.empty>
</ToolbarFilters>
<ToolbarActions>
<Hds::Button
class="has-right-margin-4"
@color="secondary"
@icon="reload"
@iconPosition="trailing"
@text="Refresh list"
{{on "click" this.refreshNamespaceList}}
data-test-button="refresh-namespace-list"
/>
<ToolbarLink @route="vault.cluster.access.namespaces.create" @type="add" data-test-link-to="create-namespace">
Create namespace
</ToolbarLink>
</ToolbarActions>
</Toolbar>
<ListView
@items={{@model.namespaces}}
@itemNoun="namespace"
@paginationRouteName="vault.cluster.access.namespaces"
@onPageChange={{this.handlePageChange}}
as |list|
>
{{#if @model.namespaces.length}}
<ListItem as |Item|>
<Item.content>
{{list.item.id}}
</Item.content>
<Item.menu>
<Hds::Dropdown @isInline={{true}} @listPosition="bottom-right" as |dd|>
<dd.ToggleIcon
@icon="more-horizontal"
@text="More options"
@hasChevron={{false}}
data-test-popup-menu-trigger
/>
{{#let (concat this.namespace.path (if this.namespace.path "/") list.item.id) as |targetNamespace|}}
{{#if (includes targetNamespace this.namespace.accessibleNamespaces)}}
<dd.Interactive
{{on "click" (fn this.switchNamespace targetNamespace)}}
data-test-popup-menu="switch"
>Switch to namespace</dd.Interactive>
{{/if}}
{{/let}}
<dd.Interactive
@color="critical"
{{on "click" (fn (mut this.nsToDelete) list.item)}}
data-test-popup-menu="delete"
>Delete</dd.Interactive>
</Hds::Dropdown>
{{#if (eq this.nsToDelete list.item)}}
<ConfirmModal
@color="critical"
@onClose={{fn (mut this.nsToDelete) null}}
@onConfirm={{fn this.deleteNamespace list.item}}
@confirmTitle="Delete this namespace?"
@confirmMessage="Any engines or mounts in this namespace will also be removed."
/>
{{/if}}
</Item.menu>
</ListItem>
{{else}}
<list.empty>
<Hds::Link::Standalone
@icon="learn-link"
@text="Secure multi-tenancy with namespaces tutorial"
@href={{doc-link "/vault/tutorials/enterprise/namespaces"}}
/>
</list.empty>
{{/if}}
</ListView>
{{else}}
{{#if this.showSetupAlert}}
<Hds::Alert @type="inline" class="top-margin-32" as |A|>
<A.Title>Your current setup is 1 namespace.</A.Title>
<A.Description>Based on your answer about your security policy in the Guided start, no new namespaces are required.</A.Description>
</Hds::Alert>
{{/if}}
</ListView>
<Hds::Card::Container @hasBorder={{true}} class="has-padding-l top-margin-32">
<Hds::ApplicationState @align="left" class="is-marginless" as |A|>
<A.Header
@title="No namespaces yet"
@titleTag="h2"
@icon="skip"
class="align-items-end"
data-test-empty-state-title
/>
<A.Body @text="Your namespaces will be listed here. Add a namespace to get started." />
<A.Footer as |F|>
<F.Button
@color="secondary"
@icon="reload"
@text="Refresh"
{{on "click" this.refreshNamespaceList}}
data-test-button="refresh-namespace-list"
/>
<F.LinkStandalone
@icon="learn-link"
@text="Secure multi-tenancy with namespaces tutorial"
@href={{doc-link "/vault/tutorials/enterprise/namespaces"}}
/>
</A.Footer>
</Hds::ApplicationState>
</Hds::Card::Container>
{{/if}}
{{/if}}
{{else}}
<UpgradePage @title="Namespaces" @minimumEdition="Vault Enterprise Pro" />
{{/if}}

View file

@ -10,6 +10,7 @@ import Component from '@glimmer/component';
import keys from 'core/utils/keys';
import type ApiService from 'vault/services/api';
import type FlagsService from 'vault/services/flags';
import type FlashMessageService from 'vault/services/flash-messages';
import type NamespaceService from 'vault/services/namespace';
import type RouterService from '@ember/routing/router-service';
@ -45,6 +46,7 @@ interface NamespaceModel {
export default class PageNamespacesComponent extends Component<Args> {
@service declare readonly api: ApiService;
@service declare readonly router: RouterService;
@service declare readonly flags: FlagsService;
@service declare readonly flashMessages: FlashMessageService;
@service declare namespace: NamespaceService;
@ -53,6 +55,7 @@ export default class PageNamespacesComponent extends Component<Args> {
// browser query param to prevent unnecessary re-renders.
@tracked query;
@tracked nsToDelete = null;
@tracked showSetupAlert = false;
@tracked hasDismissedWizard = false;
wizardId = 'namespace';
@ -68,6 +71,21 @@ export default class PageNamespacesComponent extends Component<Args> {
}
}
// show the full available namespace path e.g. "root/ns1/child2", "admin/ns1/child2"
get namespacePath() {
if (this.namespace.inRootNamespace) {
return 'root';
}
// For nested namespaces, show "root/" prefix if not HVD managed and no separate user root
if (!this.namespace.userRootNamespace && !this.flags.isHvdManaged) {
return `root/${this.namespace.path}`;
}
// If there is a userRootNamespace or it is HVD managed, then the path alone will suffice
return this.namespace.path;
}
get showWizard() {
// Show when there are no existing namespaces and it is not in a dismissed state
return !this.hasDismissedWizard && !this.args.model.namespaces?.length;
@ -123,6 +141,11 @@ export default class PageNamespacesComponent extends Component<Args> {
}
}
@action
enterGuidedStart() {
this.hasDismissedWizard = false;
}
@action handlePageChange() {
this.args.onRefresh();
}

View file

@ -17,9 +17,9 @@
<Wizard::Namespaces::Welcome />
</:welcome>
<:exit>
{{#unless (eq this.wizardState.creationMethod this.methods.UI)}}
{{#if this.shouldShowExitButton}}
<Hds::Button @text={{this.exitText}} @color="secondary" {{on "click" this.onDismiss}} />
{{/unless}}
{{/if}}
</:exit>
<:submit>
{{#if (eq this.wizardState.securityPolicyChoice this.policy.FLEXIBLE)}}

View file

@ -72,9 +72,17 @@ export default class WizardNamespacesWizardComponent extends Component<Args> {
}
}
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.currentStep === this.steps.length - 1 &&
this.wizardState.securityPolicyChoice === SecurityPolicy.STRICT
return this.isFinalStep && this.wizardState.securityPolicyChoice === SecurityPolicy.STRICT
? 'Done & Exit'
: 'Exit';
}

View file

@ -70,7 +70,7 @@ export default class WizardNamespacesStep3 extends Component<Args> {
icon: 'sidebar',
label: CreationMethod.UI,
description:
'Apply changes immediately. Note: Changes made here may be overwritten if you also use Infrastructure as Code (Terraform).',
'Apply changes immediately. Note: Changes made in the UI will be overwritten by any future updates made via Infrastructure as Code (Terraform).',
},
];
tabOptions = ['API', 'CLI'];

View file

@ -74,21 +74,9 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
const testNS = 'test-create-ns-ui';
// Verify test-create-ns does not exist in the Manage Namespace page
await fillIn(GENERAL.filterInputExplicit, testNS);
await click(GENERAL.button('Search'));
await waitFor(GENERAL.emptyStateTitle, {
timeout: 2000,
timeoutMessage: 'timed out waiting for empty state title to render',
});
assert
.dom(GENERAL.emptyStateTitle)
.hasText(
'No namespaces yet',
'Empty state is displayed when searching for the namespace we have created in the UI but have not refreshed the list yet'
);
// Create a new namespace in the UI
await click(GENERAL.linkTo('create-namespace'));
await click(GENERAL.button('create-namespace'));
await fillIn(GENERAL.inputByAttr('path'), testNS);
await click(GENERAL.submitButton);
@ -113,11 +101,11 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
// Setup: Create namespace(s) via the CLI
const testNS = 'asdf';
await runCmd(createNS(testNS), false);
await click(GENERAL.button('refresh-namespace-list'));
// Search for created namespace// Enter search text
await fillIn(GENERAL.filterInputExplicit, testNS);
await click(GENERAL.button('Search'));
await click(GENERAL.button('refresh-namespace-list'));
// Verify the menu options
await waitFor(GENERAL.menuTrigger, {
@ -135,11 +123,11 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
// Setup: Create namespace(s) via the CLI
const testNS = 'test-create-ns-switch';
await runCmd(createNS(testNS), false);
await click(GENERAL.button('refresh-namespace-list'));
// Search for created namespace
await fillIn(GENERAL.filterInputExplicit, testNS);
await click(GENERAL.button('Search'));
await click(GENERAL.button('refresh-namespace-list'));
// Switch namespace
await waitFor(GENERAL.menuTrigger);