[UI] Billing overview bugs (#14562) (#14587) (#14590)

* VAULT-44732 redirect to dashboard if user manually goes to billing route and does not have permissions

* Fix playwright tests

* VAULT-44730 update plugin label to custom plugins

* Add acceptance test for redirect

* Update labels

* Add tests

Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com>
This commit is contained in:
Vault Automation 2026-05-06 18:05:40 -06:00 committed by GitHub
parent ab2f15c896
commit 875788314e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 77 additions and 19 deletions

View File

@ -50,8 +50,9 @@ export default class SummaryCard extends Component<Args> {
showBadge: true,
},
[NormalizedBillingMetrics.EXTERNAL_PLUGINS_TOTAL]: {
label: 'Plugins',
tooltipText: 'Highest number of plugins enabled on the cluster at any time during the month.',
label: 'Custom plugins',
tooltipText:
'Highest number of non-official plugins enabled on the cluster at any time during the month.',
},
};

View File

@ -5,6 +5,8 @@
import Route from '@ember/routing/route';
import { service } from '@ember/service';
import RouterService from '@ember/routing/router-service';
import { computeNavBar, RouteName } from 'core/helpers/display-nav-item';
import type ApiService from 'vault/services/api';
import type Controller from '@ember/controller';
@ -19,8 +21,16 @@ interface RouteController extends Controller {
}
export default class BillingOverviewRoute extends Route {
@service declare readonly router: RouterService;
@service declare readonly api: ApiService;
beforeModel() {
// if the route does not have the required permissions, redirect to the cluster dashboard
if (!computeNavBar(this, RouteName.BILLING_DASHBOARD)) {
this.router.replaceWith('vault.cluster.dashboard');
}
}
setupController(controller: RouteController, resolvedModel: Month[]) {
super.setupController(controller, resolvedModel);

View File

@ -15,24 +15,20 @@ test('billing metrics dashboard workflow', async ({ page }) => {
await page.route('**/sys/billing/overview', async (route) =>
route.fulfill({ json: METRICS_DATA_RESPONSE })
);
await page.goto('dashboard');
await page.getByRole('link', { name: 'Billing metrics' }).click();
await page.waitForResponse('**/sys/billing/overview');
});
await test.step('display billing metrics summary panel', async () => {
await expect(page.getByRole('button', { name: 'From start of January' })).toContainText(
'From start of January 2026'
);
await expect(page.getByRole('button', { name: 'January 2026' })).toContainText('January 2026');
await expect(page.getByRole('heading', { name: 'Billing metrics' })).toBeVisible();
await expect(page.getByText('Data reflects usage across')).toContainText(
'Data reflects usage across this Vault cluster. Billing metrics are used in license utilization.'
'Data reflects usage across this Vault cluster. Billing metrics determine license utilization.'
);
await expect(page.getByRole('heading', { name: 'Summary' })).toBeVisible();
await expect(page.locator('section')).toContainText(
'Summary Secrets 10 PKI units 100.1234 Data protection calls 420 Managed keys 430 KMIP Enabled Plugins 100'
'Summary Secrets 10 Credential units 303.617 Data protection calls 420 Managed keys 430 KMIP Enabled Custom plugins 100'
);
});
@ -43,10 +39,10 @@ test('billing metrics dashboard workflow', async ({ page }) => {
'Secrets Highest number of static secrets, static roles, and dynamic roles managed on the cluster during the month. Secrets replicated to this cluster are not counted. Total 210 KV Secrets 10 Dynamic roles 130 Static roles 70'
);
await expect(page.locator('section')).toContainText(
'Credential units Certificates, tokens, and other credentials issued during the month, adjusted by their duration. Total 200.3702 PKI units 100.1234 SSH OTP units 50.1234 SSH certificate units 50.1234'
'Credential units Certificates, tokens, and other credentials issued during the month, adjusted by their duration. Total 303.617 PKI units 100.1234 SSH OTP units 50.1234 SSH certificate units 50.1234 OIDC token units 52.1234 SPIFFE JWT units 51.1234'
);
await expect(page.locator('section')).toContainText(
'Data protection calls Total number of data elements processed. Total 420 Transform 220 Transit 200'
'Data protection calls Total number of data elements processed. Total 640 Transform 220 Transit 200 GCP KMS 220'
);
await expect(page.locator('section')).toContainText(
'Managed keys Highest number of cryptographic keys managed on the cluster during the month. Keys replicated to this cluster are not counted. Total 430 TOTP 220 KMSE 210'
@ -54,17 +50,15 @@ test('billing metrics dashboard workflow', async ({ page }) => {
});
await test.step('change the billing period date', async () => {
await page.getByRole('button', { name: 'From start of January' }).click();
await page.getByRole('option', { name: 'From start of Dec' }).click();
await page.getByRole('button', { name: 'January 2026' }).click();
await page.getByRole('option', { name: 'December 2025' }).click();
await expect(page.locator('section')).toContainText(
'Summary Secrets 2 PKI units 100.1234 Data protection calls 220 Managed keys 220 KMIP Not enabled Plugins 100'
'Summary Secrets 2 Credential units 200.3702 Data protection calls 220 Managed keys 220 KMIP Not enabled Custom plugins 100'
);
await expect(page.locator('section')).toContainText(
'Secrets Highest number of static secrets, static roles, and dynamic roles managed on the cluster during the month. Secrets replicated to this cluster are not counted. Total 192 KV Secrets 2 Dynamic roles 125 Static roles 65'
);
await expect(page.getByRole('button', { name: 'From start of December' })).toContainText(
'From start of December 2025'
);
await expect(page.getByRole('button', { name: 'December 2025' })).toContainText('December 2025');
});
});

View File

@ -38,7 +38,8 @@
{{#if
(or
(and this.isRootNamespace (has-permission "status" routeParams="raft"))
(and (has-permission "clients" routeParams="activity") (not this.hasChrootNamespace))
(display-nav-item this.navSection.clientCount)
(display-nav-item this.routeName.billingDashboard)
)
}}
<Nav.Title data-test-sidebar-nav-heading="Monitoring">Monitoring</Nav.Title>

View File

@ -6,7 +6,7 @@
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { click, currentURL } from '@ember/test-helpers';
import { click, currentURL, visit } from '@ember/test-helpers';
import sinon from 'sinon';
import { login, logout } from 'vault/tests/helpers/auth/auth-helpers';
@ -14,6 +14,7 @@ import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { NormalizedBillingMetrics } from 'vault/utils/metrics-helpers';
import { dateFormat } from 'core/helpers/date-format';
import { METRICS_DATA_RESPONSE } from 'vault/tests/helpers/billing/stubs';
import { createNS, deleteNSFromPaths, runCmd } from 'vault/tests/helpers/commands';
const SELECTORS = {
metricDetail: (metricKey) => `[data-test-metric-detail="${metricKey}"]`,
@ -141,4 +142,30 @@ module('Acceptance | billing/overview', function (hooks) {
assert.dom(GENERAL.navLink('Billing metrics')).doesNotExist('Billing metrics nav link is not present');
await logout();
});
test('should redirect to cluster dashboard when user switches namespace while on billing/overview route on enterprise', async function (assert) {
this.server.get('/sys/license/features', () => ({ features: ['Consumption Billing', 'Namespaces'] }));
// Login with root (no namespace)
await login();
const ns = 'namespace1';
await runCmd(createNS(ns), false);
assert.strictEqual(currentURL(), '/vault/dashboard', 'User is on dashboard after login');
// Navigate to billing/overview
await visit('/vault/billing/overview');
assert.strictEqual(currentURL(), '/vault/billing/overview', 'User navigated to billing overview');
// Trigger a route transition by visiting the current route again
await visit(`/vault/billing/overview?namespace=${ns}`);
// Should redirect back to cluster dashboard because namespace1 doesn't have billing permissions
assert.strictEqual(
currentURL(),
`/vault/dashboard?namespace=${ns}`,
'User is redirected to cluster dashboard after switching to namespace1'
);
await deleteNSFromPaths(ns);
});
});

View File

@ -118,6 +118,31 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) {
});
});
test('it should hide Monitoring heading if nav permissions is false', async function (assert) {
stubFeaturesAndPermissions(this.owner, true, false);
this.owner.lookup('service:permissions').hasNavPermission.returns(false);
await renderComponent();
assert
.dom(GENERAL.navHeading('Monitoring'))
.doesNotExist(
'Monitoring heading is hidden when raft status, client count, and billing dashboard conditions are false.'
);
});
test('it should show Monitoring heading when consumption billing is true', async function (assert) {
stubFeaturesAndPermissions(this.owner, true, false, ['Consumption Billing']);
const namespace = this.owner.lookup('service:namespace');
namespace.setNamespace('root');
await renderComponent();
assert
.dom(GENERAL.navHeading('Monitoring'))
.exists('Monitoring heading is visible when billing dashboard condition is true.');
});
test('it should hide client counts link in chroot namespace', async function (assert) {
this.owner.lookup('service:permissions').setPaths({
data: {