mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 20:36:26 +02:00
* updating verbiage and testing with new listtable replacement * remove and fix empty state * cleanup * rename Co-authored-by: Dan Rivera <dan.rivera@hashicorp.com>
206 lines
6.4 KiB
TypeScript
206 lines
6.4 KiB
TypeScript
/**
|
|
* 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 { WIZARD_ID_MAP } from 'vault/utils/constants/wizard';
|
|
import errorMessage from 'vault/utils/error-message';
|
|
|
|
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';
|
|
import type WizardService from 'vault/services/wizard';
|
|
import type { HTMLElementEvent } from 'vault/forms';
|
|
import type { PaginatedMetadata } from 'core/utils/paginate-list';
|
|
|
|
/**
|
|
* @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[] & PaginatedMetadata;
|
|
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 flags: FlagsService;
|
|
@service declare readonly flashMessages: FlashMessageService;
|
|
@service declare readonly wizard: WizardService;
|
|
@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;
|
|
@tracked showSetupAlert = false;
|
|
@tracked shouldRenderIntroModal = false;
|
|
|
|
wizardId = WIZARD_ID_MAP.namespace;
|
|
|
|
tableColumns = [
|
|
{
|
|
key: 'id',
|
|
label: 'Path',
|
|
},
|
|
{
|
|
key: 'popupMenu',
|
|
label: 'Action',
|
|
width: '75px',
|
|
},
|
|
];
|
|
|
|
constructor(owner: unknown, args: Args) {
|
|
super(owner, args);
|
|
this.query = this.args.model.pageFilter || '';
|
|
}
|
|
|
|
get namespaceIds() {
|
|
return this.args.model.namespaces.map((namespace) => {
|
|
return { id: namespace.id };
|
|
});
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// Use a getter here as total is undefined instead of 0, but checking for > 0 will cover both cases.
|
|
get hasNamespaces() {
|
|
const { namespaces } = this.args.model;
|
|
return namespaces.meta?.total > 0;
|
|
}
|
|
|
|
// Show header and breadcrumbs when viewing the intro page or during the list view.
|
|
// Do not show during Guided Start as that has its own header
|
|
get showPageHeader() {
|
|
return !this.showWizard || this.wizard.isIntroVisible(this.wizardId);
|
|
}
|
|
|
|
get showContent() {
|
|
// Show when the 1) wizard is not shown OR 2) wizard intro modal is shown
|
|
// This ensures the wizard intro modal is shown on top of the list view and the background content is not blank behind the modal
|
|
return !this.showWizard || (this.shouldRenderIntroModal && this.wizard.isIntroVisible(this.wizardId));
|
|
}
|
|
|
|
get showIntroButton() {
|
|
return this.showContent && !this.hasNamespaces;
|
|
}
|
|
|
|
get showWizard() {
|
|
// Show when there are no existing namespaces and it is not in a dismissed state
|
|
return !this.wizard.isDismissed(this.wizardId) && !this.hasNamespaces;
|
|
}
|
|
|
|
@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(namespaceId: string) {
|
|
const nsToDelete = this.args.model.namespaces.find((ns) => ns.id === namespaceId) as 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 = errorMessage(error);
|
|
this.flashMessages.danger(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
|
|
showIntroPage() {
|
|
// Reset the wizard dismissal state to allow re-entering the wizard
|
|
this.wizard.reset(this.wizardId);
|
|
this.shouldRenderIntroModal = true;
|
|
}
|
|
|
|
@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);
|
|
}
|
|
}
|