Repository: hashicorp/vault
Pull request #30692: [VAULT-36271] UI: Add refresh namespaces button and breadcrumb to manage namespace page
Author: login: beagins
Labels: name: ui
name: hashicorp-contributed-pr
description: If the PR is HashiCorp (i.e. not-community) contributed
Created: 2025-05-20T17:38:26Z | Merged: 2025-05-20T22:21:10Z

### Description
What does this PR do?
- [x] Added breadcrumb to Namespace list page for consistency with other pages
- [x] Added refresh list button to Namespace list page to manually refresh list as needed
- [x] Added test coverage
- [x] Enterprise tests passing ✅  
  > 258 tests completed in 148164 milliseconds, with 0 failed, 10 skipped, and 0 todo.
1492 assertions of 1492 passed, 0 failed.

<img width="1726" alt="Screenshot 2025-05-20 at 10 37 38 AM" src="https://github.com/user-attachments/assets/207c007f-3f44-4ecf-b36e-1d8dc2e4cb32" />


### TODO only if you're a HashiCorp employee
- [ ] **Backport Labels:** If this fix needs to be backported, use the appropriate `backport/` label that matches the desired release branch. Note that in the CE repo, the latest release branch will look like `backport/x.x.x`, but older release branches will be `backport/ent/x.x.x+ent`.
    - [ ] **LTS**: If this fixes a critical security vulnerability or [severity 1](https://www.hashicorp.com/customer-success/enterprise-support) bug, it will also need to be backported to the current [LTS versions](https://developer.hashicorp.com/vault/docs/enterprise/lts#why-is-there-a-risk-to-updating-to-a-non-lts-vault-enterprise-version) of Vault. To ensure this, use **all** available enterprise labels.
- [ ] **ENT Breakage:** If this PR either 1) removes a public function OR 2) changes the signature
  of a public function, even if that change is in a CE file, _double check_ that
  applying the patch for this PR to the ENT repo and running tests doesn't
  break any tests. Sometimes ENT only tests rely on public functions in CE
  files.
- [ ] **Jira:** If this change has an associated Jira, it's referenced either
  in the PR description, commit message, or branch name.
- [ ] **RFC:** If this change has an associated RFC, please link it in the description.
- [ ] **ENT PR:** If this change has an associated ENT PR, please link it in the
  description. Also, make sure the changelog is in this PR, _not_ in your ENT PR.



Diff:
diff --git a/changelog/30692.txt b/changelog/30692.txt
new file mode 100644
index 00000000000..09d36ce5693
--- /dev/null
+++ b/changelog/30692.txt
@@ -0,0 +1,3 @@
+```release-note:improvement
+ui: Add 'Refresh list' button to the namespace list page.
+```
\ No newline at end of file
diff --git a/ui/app/controllers/vault/cluster/access/namespaces/index.js b/ui/app/controllers/vault/cluster/access/namespaces/index.js
index 125c498105a..38cbce53e83 100644
--- a/ui/app/controllers/vault/cluster/access/namespaces/index.js
+++ b/ui/app/controllers/vault/cluster/access/namespaces/index.js
@@ -80,7 +80,7 @@ export default class ManageNamespacesController extends Controller {
     }
   }
 
-  // Refresh the namespace list
+  @action
   async refreshNamespaceList() {
     try {
       // Await the async operation to complete
diff --git a/ui/app/templates/vault/cluster/access/namespaces/index.hbs b/ui/app/templates/vault/cluster/access/namespaces/index.hbs
index 50460659c98..de110db3acc 100644
--- a/ui/app/templates/vault/cluster/access/namespaces/index.hbs
+++ b/ui/app/templates/vault/cluster/access/namespaces/index.hbs
@@ -5,6 +5,12 @@
 
 {{#if (has-feature "Namespaces")}}
   <PageHeader as |p|>
+    <p.top>
+      <Hds::Breadcrumb data-test-breadcrumbs>
+        <Hds::Breadcrumb::Item @text="Namespaces" @current={{true}} />
+      </Hds::Breadcrumb>
+    </p.top>
+
     <p.levelLeft>
       <h1 class="title is-3">
         Namespaces
@@ -23,7 +29,16 @@
       />
     </ToolbarFilters>
     <ToolbarActions>
-      <ToolbarLink @route="vault.cluster.access.namespaces.create" @type="add">
+      <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>
diff --git a/ui/tests/acceptance/access/namespaces/index-test.js b/ui/tests/acceptance/access/namespaces/index-test.js
index 9ddca0bcf3e..0918ba5a402 100644
--- a/ui/tests/acceptance/access/namespaces/index-test.js
+++ b/ui/tests/acceptance/access/namespaces/index-test.js
@@ -31,6 +31,12 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
     );
   });
 
+  test('it displays the breadcrumb trail', async function (assert) {
+    await visit('/vault/access/namespaces');
+    assert.dom(GENERAL.breadcrumb).exists({ count: 1 }, 'Only one breadcrumb is displayed');
+    assert.dom(GENERAL.breadcrumb).hasText('Namespaces', 'Breadcrumb trail is displayed correctly');
+  });
+
   test('it should render correct number of namespaces', async function (assert) {
     assert.expect(3);
     await visit('/vault/access/namespaces');
@@ -41,14 +47,37 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
     assert.dom('.hds-pagination').exists();
   });
 
+  test('it should show button to refresh namespace list', async function (assert) {
+    let refreshNetworkRequestTriggered;
+    const refreshNamespaceButton = GENERAL.testButton('refresh-namespace-list');
+
+    this.server.get('/sys/internal/ui/namespaces', () => {
+      refreshNetworkRequestTriggered = true;
+      return;
+    });
+
+    await visit('/vault/access/namespaces');
+
+    assert.dom(refreshNamespaceButton).hasText('Refresh list', 'Refresh button is rendered correctly');
+
+    refreshNetworkRequestTriggered = false;
+    await click(refreshNamespaceButton);
+    assert.true(
+      refreshNetworkRequestTriggered,
+      'Get namespaces network request was made when refresh button was clicked'
+    );
+  });
+
   test('it should show button to create new namespace', async function (assert) {
+    const createNamespaceLink = GENERAL.linkTo('create-namespace');
+
     await visit('/vault/access/namespaces');
-    const createNamespaceButton = 'nav.toolbar-actions a.toolbar-link';
+
     assert
-      .dom(createNamespaceButton)
+      .dom(createNamespaceLink)
       .hasText('Create namespace', 'Create namespace button is rendered correctly');
     assert
-      .dom(createNamespaceButton)
+      .dom(createNamespaceLink)
       .hasAttribute(
         'href',
         '/ui/vault/access/namespaces/create',


Reviews:
user: login: lane-wetmore
body: Just a few test selector suggestions!
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T20:39:39Z
user: login: beagins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T20:54:55Z
user: login: lane-wetmore
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T22:17:17Z
user: login: lane-wetmore
body: Looks good 👍 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T22:18:25Z

Review comments:
diff_hunk: @@ -5,6 +5,12 @@
 
 {{#if (has-feature "Namespaces")}}
   <PageHeader as |p|>
+    <p.top>
+      <Hds::Breadcrumb>
+        <Hds::Breadcrumb::Item @text="Namespaces" @current={{true}} />
path: ui/app/templates/vault/cluster/access/namespaces/index.hbs
user: login: lane-wetmore
body: ```suggestion
        <Hds::Breadcrumb::Item @text="Namespaces" @current={{true}}  data-test-breadcrumb="namespaces"/>
```
created_at: 2025-05-20T20:20:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,6 +31,12 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
     );
   });
 
+  test('it displays the breadcrumb trail', async function (assert) {
+    await visit('/vault/access/namespaces');
+    assert.dom('.hds-breadcrumb__text').exists({ count: 1 }, 'Only one breadcrumb is displayed');
+    assert.dom('.hds-breadcrumb__text').hasText('Namespaces', 'Breadcrumb trail is displayed correctly');
path: ui/tests/acceptance/access/namespaces/index-test.js
user: login: lane-wetmore
body: ```suggestion
    assert.dom(GENERAL.breadcrumb).exists({ count: 1 }, 'Only one breadcrumb is displayed');
    assert.dom(GENERAL.breadcrumbText("namespaces")).hasText('Namespaces', 'Breadcrumb trail is displayed correctly');
```
created_at: 2025-05-20T20:28:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,6 +31,12 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
     );
   });
 
+  test('it displays the breadcrumb trail', async function (assert) {
+    await visit('/vault/access/namespaces');
+    assert.dom('.hds-breadcrumb__text').exists({ count: 1 }, 'Only one breadcrumb is displayed');
+    assert.dom('.hds-breadcrumb__text').hasText('Namespaces', 'Breadcrumb trail is displayed correctly');
path: ui/tests/acceptance/access/namespaces/index-test.js
user: login: lane-wetmore
body: For this breadcrumb text assertion, we could add 

```
  breadcrumbText: (label: string) => `[data-test-breadcrumb="${label}"] span`,
```

to our general test selectors [here](https://github.com/hashicorp/vault/blob/a005b20ae5114f56b960d560924f19337ea3f8f4/ui/tests/helpers/general-selectors.ts#L12). It seems like something that would be handy for other tests to use too. 
created_at: 2025-05-20T20:32:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -23,6 +29,14 @@
       />
     </ToolbarFilters>
     <ToolbarActions>
+      <Hds::Button
+        class="has-right-margin-4"
+        @color="secondary"
+        @icon="reload"
+        @iconPosition="trailing"
+        @text="Refresh list"
+        {{on "click" this.refreshNamespaceList}}
+      />
path: ui/app/templates/vault/cluster/access/namespaces/index.hbs
user: login: lane-wetmore
body: ```suggestion
        {{on "click" this.refreshNamespaceList}}
        data-test-button="refresh-namespace-list"
      />
```
created_at: 2025-05-20T20:35:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -41,6 +47,26 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
     assert.dom('.hds-pagination').exists();
   });
 
+  test('it should show button to refresh namespace list', async function (assert) {
+    let refreshNetworkRequestTriggered;
+
+    this.server.get('/sys/internal/ui/namespaces', () => {
+      refreshNetworkRequestTriggered = true;
+      return;
+    });
+
+    await visit('/vault/access/namespaces');
+    const refreshListButton = 'nav.toolbar-actions .hds-button';
+    assert.dom(refreshListButton).hasText('Refresh list', 'Refresh button is rendered correctly');
path: ui/tests/acceptance/access/namespaces/index-test.js
user: login: lane-wetmore
body: ```suggestion
    assert.dom(GENERAL.testButton("refresh-namespace-list")).hasText('Refresh list', 'Refresh button is rendered correctly');
```
created_at: 2025-05-20T20:36:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,6 +31,12 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
     );
   });
 
+  test('it displays the breadcrumb trail', async function (assert) {
+    await visit('/vault/access/namespaces');
+    assert.dom('.hds-breadcrumb__text').exists({ count: 1 }, 'Only one breadcrumb is displayed');
+    assert.dom('.hds-breadcrumb__text').hasText('Namespaces', 'Breadcrumb trail is displayed correctly');
path: ui/tests/acceptance/access/namespaces/index-test.js
user: login: beagins
body: Good point - I'm hesitant to make changes to the general-selectors file right now because I know that Angel is currently working on a large [overhaul/cleanup pr re: selectors](https://github.com/hashicorp/vault/pull/30694/files#diff-1dbb5cd18e4020ba0b85a73be933a698bd136b8bc8fab9aed4c4493f7fcc2f24). But I think I can use an existing general selector to get around it. 
created_at: 2025-05-20T20:54:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -31,6 +31,12 @@ module('Acceptance | Enterprise | /access/namespaces', function (hooks) {
     );
   });
 
+  test('it displays the breadcrumb trail', async function (assert) {
+    await visit('/vault/access/namespaces');
+    assert.dom('.hds-breadcrumb__text').exists({ count: 1 }, 'Only one breadcrumb is displayed');
+    assert.dom('.hds-breadcrumb__text').hasText('Namespaces', 'Breadcrumb trail is displayed correctly');
path: ui/tests/acceptance/access/namespaces/index-test.js
user: login: lane-wetmore
body: Yeah, I saw that right after my review 😅. Using the existing selectors works too 😄 
created_at: 2025-05-20T22:17:17Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-20T17:41:37Z
body: CI Results:
All Go tests succeeded! :white_check_mark:
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-20T17:54:32Z
body: Build Results:
All builds succeeded! :white_check_mark:
author_association: NONE