mirror of
https://github.com/hashicorp/vault.git
synced 2026-02-03 20:40:45 -05:00
* move namespace logic over into page component * add onPageChange arg and pass @model * update type and arg accessor Co-authored-by: lane-wetmore <lane.wetmore@hashicorp.com>
This commit is contained in:
parent
06ddf792f2
commit
7e7a1bfe63
5 changed files with 226 additions and 155 deletions
91
ui/app/components/page/namespaces.hbs
Normal file
91
ui/app/components/page/namespaces.hbs
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
{{!
|
||||
Copyright IBM Corp. 2016, 2025
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
{{#if (has-feature "Namespaces")}}
|
||||
<Page::Header @title="Namespaces">
|
||||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs
|
||||
@breadcrumbs={{array (hash label="Vault" route="vault.cluster.dashboard" icon="vault") (hash label="Namespaces")}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
</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"}}
|
||||
/>
|
||||
</list.empty>
|
||||
{{/if}}
|
||||
</ListView>
|
||||
{{else}}
|
||||
<UpgradePage @title="Namespaces" @minimumEdition="Vault Enterprise Pro" />
|
||||
{{/if}}
|
||||
128
ui/app/components/page/namespaces.ts
Normal file
128
ui/app/components/page/namespaces.ts
Normal file
|
|
@ -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<void>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export default class PageNamespacesComponent extends Component<Args> {
|
||||
@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<HTMLInputElement>) {
|
||||
this.query = evt.target.value;
|
||||
}
|
||||
|
||||
@action
|
||||
handleSearch(evt: HTMLElementEvent<HTMLInputElement>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,91 +3,4 @@
|
|||
SPDX-License-Identifier: BUSL-1.1
|
||||
}}
|
||||
|
||||
{{#if (has-feature "Namespaces")}}
|
||||
<Page::Header @title="Namespaces">
|
||||
<:breadcrumbs>
|
||||
<Page::Breadcrumbs
|
||||
@breadcrumbs={{array (hash label="Vault" route="vault.cluster.dashboard" icon="vault") (hash label="Namespaces")}}
|
||||
/>
|
||||
</:breadcrumbs>
|
||||
</Page::Header>
|
||||
|
||||
<Toolbar>
|
||||
<ToolbarFilters>
|
||||
<FilterInputExplicit
|
||||
@query={{this.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={{this.model.namespaces}}
|
||||
@itemNoun="namespace"
|
||||
@paginationRouteName="vault.cluster.access.namespaces"
|
||||
as |list|
|
||||
>
|
||||
{{#if this.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.namespaceService.path (if this.namespaceService.path "/") list.item.id)
|
||||
as |targetNamespace|
|
||||
}}
|
||||
{{#if (includes targetNamespace this.namespaceService.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}}
|
||||
<UpgradePage @title="Namespaces" @minimumEdition="Vault Enterprise Pro" />
|
||||
{{/if}}
|
||||
<Page::Namespaces @model={{this.model}} @onFilterChange={{this.navigate}} @onRefresh={{this.refreshRoute}} />
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
@showSizeSelector={{false}}
|
||||
@totalItems={{@items.meta.total}}
|
||||
@queryFunction={{this.paginationQueryParams}}
|
||||
@onPageChange={{@onPageChange}}
|
||||
/>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue