From cb9e2e49bb576007c0f3829aad95c616e2372c7d Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Thu, 23 Apr 2026 13:12:34 -0400 Subject: [PATCH] [UI][VAULT-44082] Billing metrics dashboard date subtext and update dashboard copy (#14174) (#14230) * Update billing date subtext * Update billing acceptance tests... * Code cleanup and tests Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com> --- ui/app/components/billing/date-range.hbs | 24 ++- ui/app/components/billing/date-range.ts | 4 +- ui/app/components/billing/page/overview.hbs | 9 +- ui/app/components/billing/page/overview.ts | 7 + ui/tests/acceptance/billing/overview-test.js | 183 ++++--------------- ui/tests/helpers/general-selectors.ts | 1 + 6 files changed, 73 insertions(+), 155 deletions(-) diff --git a/ui/app/components/billing/date-range.hbs b/ui/app/components/billing/date-range.hbs index 06c12b4b86..8b7c3840ca 100644 --- a/ui/app/components/billing/date-range.hbs +++ b/ui/app/components/billing/date-range.hbs @@ -5,17 +5,33 @@ - + {{#each this.dateDropdownOptions as |option|}} {{option.label}} {{/each}} - Values update every 10 minutes. - Last updated: - {{date-format @selectedDateOption.updated_at "hh:mm:ss a"}} + + {{#if @isSelectedDateInvalid}} + No data available. + {{else}} + Values update every 10 minutes. Last updated: + {{date-format @selectedDateOption.updated_at "MMMM d, yyyy, hh:mm:ss aaa" withTimeZone=true}} + {{/if}} + \ No newline at end of file diff --git a/ui/app/components/billing/date-range.ts b/ui/app/components/billing/date-range.ts index 8bbc033377..457cfb0c79 100644 --- a/ui/app/components/billing/date-range.ts +++ b/ui/app/components/billing/date-range.ts @@ -24,8 +24,8 @@ export default class BillingDateRange extends Component { const options = []; for (const option of this.args.months) { - const formattedDate = dateFormat([option.month, 'MMM yyyy'], {}); - options.push({ label: `From start of ${formattedDate}`, value: option.month }); + const formattedDate = dateFormat([option.month, 'MMMM yyyy'], {}); + options.push({ label: formattedDate, value: option.month }); } return options; diff --git a/ui/app/components/billing/page/overview.hbs b/ui/app/components/billing/page/overview.hbs index c3c8bb2a45..c901a0e059 100644 --- a/ui/app/components/billing/page/overview.hbs +++ b/ui/app/components/billing/page/overview.hbs @@ -9,7 +9,7 @@ <:description> - 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. <:actions> @@ -17,7 +17,12 @@ - + diff --git a/ui/app/components/billing/page/overview.ts b/ui/app/components/billing/page/overview.ts index adbc28f923..fe1721ba8b 100644 --- a/ui/app/components/billing/page/overview.ts +++ b/ui/app/components/billing/page/overview.ts @@ -14,6 +14,7 @@ import type { Month, NormalizedMetricsData } from 'vault/vault/billing/overview' import type { SystemReadBillingOverviewResponse } from '@hashicorp/vault-client-typescript'; const REFRESH_PERIOD_MS = 10 * 60 * 1000 + 30 * 1000; // 10 minutes 30 seconds +export const INVALID_DATE_TIME = '0001-01-01T00:00:00Z'; export default class BillingPageOverview extends Component { @service declare readonly api: ApiService; @@ -28,6 +29,8 @@ export default class BillingPageOverview extends Component { /** Milliseconds to wait between each poll. Updated dynamically based on API response. */ private _interval = 5000; + invalidDateTime = INVALID_DATE_TIME; + detailsByMetric = { Secrets: [ NormalizedBillingMetrics.STATIC_SECRETS_KV, @@ -55,6 +58,10 @@ export default class BillingPageOverview extends Component { return this.selectedDateOption ?? this.months[0] ?? null; } + get isSelectedDateInvalid() { + return this.selectedDate?.updated_at === this.invalidDateTime; + } + /** * Calculates how long to wait before the next poll based on when the data was last updated. * Waits until 10m30s after `updated_at`, so polls align with the server's refresh cadence. diff --git a/ui/tests/acceptance/billing/overview-test.js b/ui/tests/acceptance/billing/overview-test.js index a16c29f10c..c4e8e974dd 100644 --- a/ui/tests/acceptance/billing/overview-test.js +++ b/ui/tests/acceptance/billing/overview-test.js @@ -12,144 +12,8 @@ import sinon from 'sinon'; import { login, logout } from 'vault/tests/helpers/auth/auth-helpers'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { NormalizedBillingMetrics } from 'vault/utils/metrics-helpers'; - -const mockMetrics = { - months: [ - { - month: '2026-02', - updated_at: '2026-01-14T10:49:00Z', // Signal partial-month data - usage_metrics: [ - { - metric_name: 'static_secrets', - metric_data: { - total: 10, - metric_details: [{ type: 'kv', count: 10 }], - }, - }, - { - metric_name: 'dynamic_roles', - metric_data: { - total: 60, - metric_details: [ - { type: 'aws_dynamic', count: 22 }, - { type: 'azure_dynamic', count: 20 }, - { type: 'database_dynamic', count: 30 }, - ], - }, - }, - { - metric_name: 'auto_rotated_roles', - metric_data: { - total: 30, - metric_details: [ - { type: 'aws_static', count: 22 }, - { type: 'azure_static', count: 20 }, - ], - }, - }, - { metric_name: 'kmip', metric_data: { used_in_month: true } }, - { metric_name: 'pki_units', metric_data: { total: 100.1234 } }, - { metric_name: 'data_protection_calls', metric_data: { total: 12 } }, - { - metric_name: 'ssh_units', - metric_data: { - total: 100.2468, - metric_details: [ - { type: 'otp_units', count: 50.1234 }, - { type: 'certificate_units', count: 50.1234 }, - ], - }, - }, - { - metric_name: 'managed_keys', - metric_data: { - total: 82, - metric_details: [ - { type: 'totp', count: 52 }, - { type: 'kmse', count: 30 }, - ], - }, - }, - /** - * Other metrics: - * - external_plugins - * - managed_keys (types: totp, kmse) - * - data_protection_calls (types: transit, transform) - * - id_token_units (types: oidc, spiffe) // not adding in 2.0.0 - * - * Additional metrics to be added for new features in Vault 1.22/2.0. - */ - ], - }, - { - month: '2026-01', - updated_at: '2026-01-14T10:49:00Z', // Signal partial-month data - usage_metrics: [ - { - metric_name: 'static_secrets', - metric_data: { - total: 10, - metric_details: [{ type: 'kv', count: 10 }], - }, - }, - { - metric_name: 'dynamic_roles', - metric_data: { - total: 60, - metric_details: [ - { type: 'aws_dynamic', count: 10 }, - { type: 'azure_dynamic', count: 20 }, - { type: 'database_dynamic', count: 30 }, - ], - }, - }, - { - metric_name: 'auto_rotated_roles', - metric_data: { - total: 30, - metric_details: [ - { type: 'aws_static', count: 10 }, - { type: 'azure_static', count: 20 }, - ], - }, - }, - { metric_name: 'kmip', metric_data: { used_in_month: true } }, - { metric_name: 'pki_units', metric_data: { total: 100.1234 } }, - { metric_name: 'data_protection_calls', metric_data: { total: 22 } }, - { metric_name: 'managed_keys', metric_data: { total: 44 } }, - { - metric_name: 'ssh_units', - metric_data: { - total: 100.2468, - metric_details: [ - { type: 'otp_units', count: 50.1234 }, - { type: 'certificate_units', count: 51.22 }, - ], - }, - }, - { - metric_name: 'managed_keys', - metric_data: { - total: 82, - metric_details: [ - { type: 'totp', count: 2 }, - { type: 'kmse', count: 1 }, - ], - }, - }, - /** - * Other metrics: - * - external_plugins - * - managed_keys (types: totp, kmse) - * - data_protection_calls (types: transit, transform) - * - id_token_units (types: oidc, spiffe) // not adding in 2.0.0 - * - * Additional metrics to be added for new features in Vault 1.22/2.0. - */ - ], - }, - ], -}; +import { dateFormat } from 'core/helpers/date-format'; +import { METRICS_DATA_RESPONSE } from 'vault/tests/helpers/billing/stubs'; const SELECTORS = { metricDetail: (metricKey) => `[data-test-metric-detail="${metricKey}"]`, @@ -162,11 +26,12 @@ module('Acceptance | billing/overview', function (hooks) { hooks.beforeEach(async function () { this.version = this.owner.lookup('service:version'); - this.server.get('/sys/billing/overview', () => mockMetrics); + this.mockMetrics = METRICS_DATA_RESPONSE.data; + this.server.get('/sys/billing/overview', () => this.mockMetrics); // Stub the API service const api = this.owner.lookup('service:api'); - this.billingStub = sinon.stub(api.sys, 'systemReadBillingOverview').resolves(mockMetrics); + this.billingStub = sinon.stub(api.sys, 'systemReadBillingOverview').resolves(this.mockMetrics); }); hooks.afterEach(function () { @@ -185,8 +50,17 @@ module('Acceptance | billing/overview', function (hooks) { assert .dom(GENERAL.hdsPageHeaderDescription) .hasText( - '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.' ); + assert.dom(GENERAL.textBody('Last updated date time')).hasText( + `Values update every 10 minutes. Last updated: ${dateFormat( + [this.mockMetrics.months[0].updated_at, 'MMMM d, yyyy, hh:mm:ss aaa'], + { + withTimeZone: true, + } + )}` + ); + assert.dom(GENERAL.cardContainer('Summary')).exists(); assert.dom(GENERAL.cardContainer('Secrets')).exists(); @@ -194,9 +68,9 @@ module('Acceptance | billing/overview', function (hooks) { assert.dom(SELECTORS.metricDetail(NormalizedBillingMetrics.STATIC_SECRETS_KV)).exists(); assert.dom(SELECTORS.metricDetailValue(NormalizedBillingMetrics.STATIC_SECRETS_KV)).hasText('10'); assert.dom(SELECTORS.metricDetail(NormalizedBillingMetrics.DYNAMIC_ROLES_TOTAL)).exists(); - assert.dom(SELECTORS.metricDetailValue(NormalizedBillingMetrics.DYNAMIC_ROLES_TOTAL)).hasText('60'); + assert.dom(SELECTORS.metricDetailValue(NormalizedBillingMetrics.DYNAMIC_ROLES_TOTAL)).hasText('130'); assert.dom(SELECTORS.metricDetail(NormalizedBillingMetrics.AUTO_ROTATED_ROLES_TOTAL)).exists(); - assert.dom(SELECTORS.metricDetailValue(NormalizedBillingMetrics.AUTO_ROTATED_ROLES_TOTAL)).hasText('30'); + assert.dom(SELECTORS.metricDetailValue(NormalizedBillingMetrics.AUTO_ROTATED_ROLES_TOTAL)).hasText('70'); assert.dom(GENERAL.cardContainer('Credential units')).exists(); assert.dom(SELECTORS.metricDetail(NormalizedBillingMetrics.PKI_UNITS_TOTAL)).exists(); @@ -212,19 +86,34 @@ module('Acceptance | billing/overview', function (hooks) { assert.dom(SELECTORS.metricDetail(NormalizedBillingMetrics.DATA_PROTECTION_CALLS_TRANSFORM)).exists(); assert .dom(SELECTORS.metricDetailValue(NormalizedBillingMetrics.DATA_PROTECTION_CALLS_TRANSFORM)) - .hasText('0'); + .hasText('220'); assert.dom(SELECTORS.metricDetail(NormalizedBillingMetrics.DATA_PROTECTION_CALLS_TRANSIT)).exists(); assert .dom(SELECTORS.metricDetailValue(NormalizedBillingMetrics.DATA_PROTECTION_CALLS_TRANSIT)) - .hasText('0'); + .hasText('200'); assert.dom(GENERAL.cardContainer('Managed keys')).exists(); assert.dom(SELECTORS.metricDetail(NormalizedBillingMetrics.MANAGED_KEYS_TOTP)).exists(); - assert.dom(SELECTORS.metricDetailValue(NormalizedBillingMetrics.MANAGED_KEYS_TOTP)).hasText('52'); + assert.dom(SELECTORS.metricDetailValue(NormalizedBillingMetrics.MANAGED_KEYS_TOTP)).hasText('220'); assert.dom(SELECTORS.metricDetail(NormalizedBillingMetrics.MANAGED_KEYS_KMSE)).exists(); - assert.dom(SELECTORS.metricDetailValue(NormalizedBillingMetrics.MANAGED_KEYS_KMSE)).hasText('30'); + assert.dom(SELECTORS.metricDetailValue(NormalizedBillingMetrics.MANAGED_KEYS_KMSE)).hasText('210'); await logout(); }); + + test('display no data available when updated_at is invalid', async function (assert) { + this.server.get('/sys/license/features', () => ({ features: ['Consumption Billing'] })); + const mockMetricsInvalidDate = { ...this.mockMetrics }; + mockMetricsInvalidDate.months[1].updated_at = '0001-01-01T00:00:00Z'; + this.server.get('/sys/billing/overview', () => mockMetricsInvalidDate); + await login(); + assert.dom(GENERAL.navLink('Billing metrics')).hasText('Billing metrics'); + await click(GENERAL.navLink('Billing metrics')); + await click(GENERAL.dropdownToggle('Date range')); + await click(GENERAL.menuItem('2025-12')); + assert.dom(GENERAL.textBody('Last updated date time')).hasText('No data available.'); + await logout(); + }); + test('hide billing/overview when license endpoint does not have consumption billing', async function (assert) { this.server.get('/sys/license/features', () => ({ features: [] })); await login(); diff --git a/ui/tests/helpers/general-selectors.ts b/ui/tests/helpers/general-selectors.ts index da484f84d7..2a58163047 100644 --- a/ui/tests/helpers/general-selectors.ts +++ b/ui/tests/helpers/general-selectors.ts @@ -195,4 +195,5 @@ export const GENERAL = { tooltip: (label: string) => `[data-test-tooltip="${label}"]`, tooltipText: '.hds-tooltip-container', textDisplay: (attr: string) => `[data-test-text-display="${attr}"]`, + textBody: (attr: string) => `[data-test-text-body="${attr}"]`, };