diff --git a/ui/app/components/billing/summary-card.ts b/ui/app/components/billing/summary-card.ts index a68f6aaa5c..bab54c9f32 100644 --- a/ui/app/components/billing/summary-card.ts +++ b/ui/app/components/billing/summary-card.ts @@ -50,8 +50,9 @@ export default class SummaryCard extends Component { 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.', }, }; diff --git a/ui/app/routes/vault/cluster/billing/overview.ts b/ui/app/routes/vault/cluster/billing/overview.ts index 290097dc94..bba04f69bb 100644 --- a/ui/app/routes/vault/cluster/billing/overview.ts +++ b/ui/app/routes/vault/cluster/billing/overview.ts @@ -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); diff --git a/ui/e2e/tests/superuser/billing-metrics-dashboard.spec.ts b/ui/e2e/tests/superuser/billing-metrics-dashboard.spec.ts index 147f524173..ce092ba395 100644 --- a/ui/e2e/tests/superuser/billing-metrics-dashboard.spec.ts +++ b/ui/e2e/tests/superuser/billing-metrics-dashboard.spec.ts @@ -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'); }); }); diff --git a/ui/lib/core/addon/components/sidebar/nav/cluster.hbs b/ui/lib/core/addon/components/sidebar/nav/cluster.hbs index 810cfdf8ac..94e0da630e 100644 --- a/ui/lib/core/addon/components/sidebar/nav/cluster.hbs +++ b/ui/lib/core/addon/components/sidebar/nav/cluster.hbs @@ -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) ) }} Monitoring diff --git a/ui/tests/acceptance/billing/overview-test.js b/ui/tests/acceptance/billing/overview-test.js index 29c945b3ef..3f9c397c2e 100644 --- a/ui/tests/acceptance/billing/overview-test.js +++ b/ui/tests/acceptance/billing/overview-test.js @@ -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); + }); }); diff --git a/ui/tests/integration/components/sidebar/nav/cluster-test.js b/ui/tests/integration/components/sidebar/nav/cluster-test.js index c661cf213c..7b69cd65d9 100644 --- a/ui/tests/integration/components/sidebar/nav/cluster-test.js +++ b/ui/tests/integration/components/sidebar/nav/cluster-test.js @@ -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: {