vault/ui/app/components/page/namespaces.ts
Vault Automation 1331818193
UI: Fix namespace search showing empty state when namespaces exist (#13257) (#13286)
* updating verbiage and testing with new listtable replacement

* remove and fix empty state

* cleanup

* rename

Co-authored-by: Dan Rivera <dan.rivera@hashicorp.com>
2026-03-20 22:06:56 +00:00

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