diff --git a/ui/app/components/page/namespaces.hbs b/ui/app/components/page/namespaces.hbs
new file mode 100644
index 0000000000..0725b128d9
--- /dev/null
+++ b/ui/app/components/page/namespaces.hbs
@@ -0,0 +1,91 @@
+{{!
+ Copyright IBM Corp. 2016, 2025
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+{{#if (has-feature "Namespaces")}}
+
+ <:breadcrumbs>
+
+
+
+
+
+
+
+
+
+
+
+ Create namespace
+
+
+
+
+
+ {{#if @model.namespaces.length}}
+
+
+ {{list.item.id}}
+
+
+
+
+ {{#let (concat this.namespace.path (if this.namespace.path "/") list.item.id) as |targetNamespace|}}
+ {{#if (includes targetNamespace this.namespace.accessibleNamespaces)}}
+ Switch
+ to namespace
+ {{/if}}
+ {{/let}}
+ Delete
+
+ {{#if (eq this.nsToDelete list.item)}}
+
+ {{/if}}
+
+
+ {{else}}
+
+
+
+ {{/if}}
+
+{{else}}
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/app/components/page/namespaces.ts b/ui/app/components/page/namespaces.ts
new file mode 100644
index 0000000000..7762b18a83
--- /dev/null
+++ b/ui/app/components/page/namespaces.ts
@@ -0,0 +1,128 @@
+/**
+ * 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 keys from 'core/utils/keys';
+
+import type ApiService from 'vault/services/api';
+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 { HTMLElementEvent } from 'vault/forms';
+
+/**
+ * @module PageNamespaces
+ * PageNamespaces component handles the display and management of namespaces,
+ * including the namespace wizard for first-time users.
+ *
+ * @param {object} namespaces - list of namespaces
+ * @param {string} pageFilter - current page filter value
+ * @param {function} onFilterChange - callback function to handle filter changes, receives filter string or null to clear
+ * @param {function} onRefresh - callback function to refresh the namespace list from the route/controller
+ */
+
+interface Args {
+ model: {
+ namespaces: NamespaceModel[];
+ pageFilter: string | null;
+ };
+ onFilterChange: CallableFunction;
+ onRefresh: CallableFunction;
+}
+
+interface NamespaceModel {
+ id: string;
+ destroyRecord: () => Promise;
+ [key: string]: unknown;
+}
+
+export default class PageNamespacesComponent extends Component {
+ @service declare readonly api: ApiService;
+ @service declare readonly router: RouterService;
+ @service declare readonly flashMessages: FlashMessageService;
+ // Use namespaceService alias to avoid collision with namespaces
+ // input parameter from the route.
+ @service declare namespace: NamespaceService;
+
+ // The `query` property is used to track the filter
+ // input value separately from updating the `pageFilter`
+ // browser query param to prevent unnecessary re-renders.
+ @tracked query;
+ @tracked nsToDelete = null;
+
+ constructor(owner: unknown, args: Args) {
+ super(owner, args);
+ this.query = this.args.model.pageFilter || '';
+ }
+
+ @action
+ handleKeyDown(event: KeyboardEvent) {
+ const isEscKeyPressed = keys.ESC.includes(event.key);
+ if (isEscKeyPressed) {
+ // On escape, clear the filter
+ this.args.onFilterChange(null);
+ }
+ // ignore all other key events
+ }
+
+ @action
+ handleInput(evt: HTMLElementEvent) {
+ this.query = evt.target.value;
+ }
+
+ @action
+ handleSearch(evt: HTMLElementEvent) {
+ evt.preventDefault();
+ this.args.onFilterChange(this.query);
+ }
+
+ @action
+ async deleteNamespace(nsToDelete: NamespaceModel) {
+ try {
+ // Attempt to destroy the record
+ await nsToDelete.destroyRecord();
+
+ // Log success and optionally update the UI
+ this.flashMessages.success(`Successfully deleted namespace: ${nsToDelete.id}`);
+
+ // Call the refresh method to update the list
+ this.refreshNamespaceList();
+ } catch (error) {
+ const { message } = await this.api.parseError(error);
+ this.flashMessages.danger(`There was an error deleting this namespace: ${message}`);
+ }
+ this.nsToDelete = null;
+ }
+
+ @action
+ async refreshNamespaceList() {
+ try {
+ // Await the async operation to complete
+ await this.namespace.findNamespacesForUser.perform();
+ this.args.onRefresh();
+ } catch (error) {
+ this.flashMessages.danger('There was an error refreshing the namespace list.');
+ }
+ }
+
+ @action handlePageChange() {
+ this.args.onRefresh();
+ }
+
+ @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);
+ }
+}
diff --git a/ui/app/controllers/vault/cluster/access/namespaces/index.js b/ui/app/controllers/vault/cluster/access/namespaces/index.js
index 929b75cddd..20ff5f9606 100644
--- a/ui/app/controllers/vault/cluster/access/namespaces/index.js
+++ b/ui/app/controllers/vault/cluster/access/namespaces/index.js
@@ -3,11 +3,9 @@
* SPDX-License-Identifier: BUSL-1.1
*/
-import { service } from '@ember/service';
import { action } from '@ember/object';
-import { tracked } from '@glimmer/tracking';
import Controller from '@ember/controller';
-import keys from 'core/utils/keys';
+import { service } from '@ember/service';
/**
* @module ManageNamespaces
@@ -18,26 +16,15 @@ import keys from 'core/utils/keys';
* @param {string} pageFilter - value of queryParam
* @param {string} page - value of queryParam
*/
+
export default class ManageNamespacesController extends Controller {
- queryParams = ['pageFilter', 'page'];
-
- // Use namespaceService alias to avoid collision with namespaces
- // input parameter from the route.
- @service('namespace') namespaceService;
@service router;
- @service flashMessages;
-
- // The `query` property is used to track the filter
- // input value seperately from updating the `pageFilter`
- // browser query param to prevent unnecessary re-renders.
- @tracked query;
- @tracked pageFilter = '';
constructor() {
super(...arguments);
- this.query = this.pageFilter;
}
+ @action
navigate(pageFilter) {
const route = 'vault.cluster.access.namespaces.index';
const args = [route, { queryParams: { page: 1, pageFilter: pageFilter || null } }];
@@ -45,56 +32,7 @@ export default class ManageNamespacesController extends Controller {
}
@action
- handleKeyDown(event) {
- const isEscKeyPressed = keys.ESC.includes(event.key);
- if (isEscKeyPressed) {
- // On escape, transition to roles index route.
- this.navigate();
- }
- // ignore all other key events
- }
-
- @action handleInput(evt) {
- this.query = evt.target.value;
- }
-
- @action
- handleSearch(evt) {
- evt.preventDefault();
- this.navigate(this.query);
- }
-
- @action
- async deleteNamespace(nsToDelete) {
- try {
- // Attempt to destroy the record
- await nsToDelete.destroyRecord();
-
- // Log success and optionally update the UI
- this.flashMessages.success(`Successfully deleted namespace: ${nsToDelete.id}`);
-
- // Call the refresh method to update the list
- this.refreshNamespaceList();
- } catch (error) {
- this.flashMessages.danger(`There was an error deleting this namespace: ${error.message}`);
- }
- }
-
- @action
- async refreshNamespaceList() {
- try {
- // Await the async operation to complete
- await this.namespaceService.findNamespacesForUser.perform();
- this.send('reload'); // Trigger the reload only after the task completes
- } catch (error) {
- this.flashMessages.danger('There was an error refreshing the namespace list.');
- }
- }
-
- @action
- switchNamespace(targetNamespace) {
- this.router.transitionTo('vault.cluster.dashboard', {
- queryParams: { namespace: targetNamespace },
- });
+ refreshRoute() {
+ this.send('reload');
}
}
diff --git a/ui/app/templates/vault/cluster/access/namespaces/index.hbs b/ui/app/templates/vault/cluster/access/namespaces/index.hbs
index 1cf5b0da16..a8998b9d9d 100644
--- a/ui/app/templates/vault/cluster/access/namespaces/index.hbs
+++ b/ui/app/templates/vault/cluster/access/namespaces/index.hbs
@@ -3,91 +3,4 @@
SPDX-License-Identifier: BUSL-1.1
}}
-{{#if (has-feature "Namespaces")}}
-
- <:breadcrumbs>
-
-
-
-
-
-
-
-
-
-
-
- Create namespace
-
-
-
-
-
- {{#if this.model.namespaces.length}}
-
-
- {{list.item.id}}
-
-
-
-
- {{#let
- (concat this.namespaceService.path (if this.namespaceService.path "/") list.item.id)
- as |targetNamespace|
- }}
- {{#if (includes targetNamespace this.namespaceService.accessibleNamespaces)}}
- Switch
- to namespace
- {{/if}}
- {{/let}}
- Delete
-
- {{#if (eq this.nsToDelete list.item)}}
-
- {{/if}}
-
-
- {{else}}
-
-
-
- {{/if}}
-
-{{else}}
-
-{{/if}}
\ No newline at end of file
+
\ No newline at end of file
diff --git a/ui/lib/core/addon/components/list-view.hbs b/ui/lib/core/addon/components/list-view.hbs
index 9b91e3047b..a41d9a87e2 100644
--- a/ui/lib/core/addon/components/list-view.hbs
+++ b/ui/lib/core/addon/components/list-view.hbs
@@ -18,6 +18,7 @@
@showSizeSelector={{false}}
@totalItems={{@items.meta.total}}
@queryFunction={{this.paginationQueryParams}}
+ @onPageChange={{@onPageChange}}
/>
{{/if}}