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}}