Vault Automation f17451d675
[VAULT-39375] Ability to pick namespaces in usage dashboard (#9143) (#9259)
Co-authored-by: Eren Tantekin <eren.tantekin@hashicorp.com>
Co-authored-by: Jim Wright <jim.wright@hashicorp.com>
2025-09-16 13:36:39 -07:00

137 lines
5.5 KiB
TypeScript

/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@ember/component';
import { service } from '@ember/service';
import type FlagsService from 'vault/services/flags';
import type ApiService from 'vault/services/api';
import type NamespaceService from 'vault/services/namespace';
import type AuthService from 'vault/vault/services/auth';
import type {
getUsageDataFunction,
getNamespaceDataFunction,
UsageDashboardData,
} from '@hashicorp-internal/vault-reporting/types/index';
import type { UtilizationReport } from 'vault/usage';
interface NamespaceOption {
path: string;
label: string;
}
/**
* @module UsagePage
* @description This component is responsible for fetching usage data and mounting the vault-reporting dashboard view.
* It uses the `api` service to make a request to the sys/utilization-report endpoint to get the usage data.
* The data is then processed to replace engine and auth method names with their display names if available.
* The component also uses the `flags` service to determine if the cluster is HVD managed or not.
*
* The logic is self-contained and this component has no args.
*
* @example ```js
* <Usage::Page />
* ```
*/
export default class UsagePage extends Component {
@service declare readonly api: ApiService;
@service declare readonly flags: FlagsService;
@service declare readonly namespace: NamespaceService;
@service declare readonly auth: AuthService;
handleFetchUsageData: getUsageDataFunction = async (namespace?: string) => {
/**
* We get a partially typed response from the API client, but only 1 level deep.
* Casting the nested types here and falling back to defaults in the mappings.
* We should get typescript errors if top level interfaces in the API client or
* the vault-reporting addon change.
*/
// Convert "root" display value back to empty string for API calls
const apiNamespace = namespace === 'root' ? undefined : namespace;
const response = (await this.api.sys.generateUtilizationReport(apiNamespace)) as UtilizationReport;
const { lease_count_quotas, replication_status, pki, secret_sync } = response;
const data: UsageDashboardData = {
authMethods: (response.auth_methods as Record<string, number>) || {},
secretEngines: (response.secret_engines as Record<string, number>) || {},
leasesByAuthMethod: (response.leases_by_auth_method as Record<string, number>) || {},
leaseCountQuotas: {
globalLeaseCountQuota: {
capacity: lease_count_quotas?.global_lease_count_quota?.capacity || 0,
count: lease_count_quotas?.global_lease_count_quota?.count || 0,
name: lease_count_quotas?.global_lease_count_quota?.name || '',
},
totalLeaseCountQuotas: lease_count_quotas?.total_lease_count_quotas || 0,
},
replicationStatus: {
drState: replication_status?.dr_state || 'disabled',
prState: replication_status?.pr_state || 'disabled',
drPrimary: replication_status?.dr_primary ?? false,
prPrimary: replication_status?.pr_primary ?? false,
},
kvv1Secrets: response.kvv1_secrets || 0,
kvv2Secrets: response.kvv2_secrets || 0,
namespaces: response.namespaces || 0,
pki: {
totalIssuers: pki?.total_issuers || 0,
totalRoles: pki?.total_roles || 0,
},
secretSync: {
destinations: secret_sync?.destinations || {},
totalDestinations: secret_sync?.total_destinations || 0,
},
};
return data;
};
handleFetchNamespaceData: getNamespaceDataFunction = async () => {
await this.namespace?.findNamespacesForUser?.perform();
const options = this.getOptions(this.namespace?.accessibleNamespaces);
const data = {
keys: options.map((option) => option.label),
};
return data;
};
/**
* getOptions from ui/app/components/namespace-picker.ts
* We might consider moving this into a util function and sharing it across both files
*/
private getOptions(accessibleNamespaces: string[]): NamespaceOption[] {
/* Each namespace option has 2 properties: { path and label }
* - path: full namespace path (used to navigate to the namespace)
* - label: text displayed inside the namespace picker dropdown (if root, then path is "", else label = path)
*
* Example:
* | path | label |
* | ---- | ----- |
* | '' | 'root' |
* | 'parent' | 'parent' |
* | 'parent/child' | 'parent/child' |
*/
const options = (accessibleNamespaces || []).map((ns: string) => ({ path: ns, label: ns }));
// Add the user's root namespace because `sys/internal/ui/namespaces` does not include it.
const userRootNamespace = this.auth.authData?.userRootNamespace;
if (!options?.find((o) => o.path === userRootNamespace)) {
// the 'root' namespace is technically an empty string so we manually add the 'root' label.
const label = userRootNamespace === '' ? 'root' : userRootNamespace;
options.unshift({ path: userRootNamespace, label });
}
// If there are no namespaces returned by the internal endpoint, add the current namespace
// to the list of options. This is a fallback for when the user has access to a single namespace.
if (options.length === 0) {
// 'path' defined in the namespace service is the full namespace path
options.push({ path: this.namespace.path, label: this.namespace.path });
}
return options;
}
}