From 68564cde459025ffdade019b51b526e85046ce1d Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 4 May 2026 11:02:15 -0600 Subject: [PATCH] [UI] billing dashboard remaining tickets (#14447) (#14465) * VAULT-44326 only poll currentmonth and intial load * VAULT-44370 API always returns the value now, so we can remove this logic * Fix tests.. * Add logic back in Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com> --- ui/app/components/billing/date-range.hbs | 5 ++- ui/app/components/billing/page/overview.hbs | 1 + ui/app/components/billing/page/overview.ts | 46 ++++++++++++++++---- ui/app/utils/metrics-helpers.ts | 12 +++-- ui/tests/acceptance/billing/overview-test.js | 27 ++++++++---- 5 files changed, 70 insertions(+), 21 deletions(-) diff --git a/ui/app/components/billing/date-range.hbs b/ui/app/components/billing/date-range.hbs index 8b7c3840ca..f1546521f7 100644 --- a/ui/app/components/billing/date-range.hbs +++ b/ui/app/components/billing/date-range.hbs @@ -30,7 +30,10 @@ {{#if @isSelectedDateInvalid}} No data available. {{else}} - Values update every 10 minutes. Last updated: + {{#if @isCurrentMonth}} + Values update every 10 minutes. + {{/if}} + Last updated: {{date-format @selectedDateOption.updated_at "MMMM d, yyyy, hh:mm:ss aaa" withTimeZone=true}} {{/if}} diff --git a/ui/app/components/billing/page/overview.hbs b/ui/app/components/billing/page/overview.hbs index 529dbcf58e..6142cd3bc0 100644 --- a/ui/app/components/billing/page/overview.hbs +++ b/ui/app/components/billing/page/overview.hbs @@ -19,6 +19,7 @@ @onDateChange={{this.onDateChange}} @selectedDateOption={{this.selectedDate}} @isSelectedDateInvalid={{this.isSelectedDateInvalid}} + @isCurrentMonth={{this.isCurrentMonth}} /> diff --git a/ui/app/components/billing/page/overview.ts b/ui/app/components/billing/page/overview.ts index c8246be4ef..260f60ffe1 100644 --- a/ui/app/components/billing/page/overview.ts +++ b/ui/app/components/billing/page/overview.ts @@ -54,7 +54,18 @@ export default class BillingPageOverview extends Component { constructor(owner: unknown, args: object) { super(owner, args); - this.startPoll(); + this.initializeBillingMetrics(); + } + + get isCurrentMonth() { + if (!this.selectedDateOption?.month) return false; + const selectedDate = new Date(`${this.selectedDateOption.month}-01T00:00:00Z`); + const currentDate = new Date(); + + return ( + selectedDate.getUTCFullYear() === currentDate.getUTCFullYear() && + selectedDate.getUTCMonth() === currentDate.getUTCMonth() + ); } get selectedDate() { @@ -78,7 +89,7 @@ export default class BillingPageOverview extends Component { fetchBillingMetrics = async () => { const response: SystemReadBillingOverviewResponse | null | undefined = await this.api.sys.systemReadBillingOverview(); - this.months = (response?.months as Month[]) || []; + this.months = (response?.months?.slice(0, 2) as Month[]) || []; const updatedMonthFromSelectedMonth = this.months.find( (month: Month) => month.month === this.selectedDateOption?.month ); @@ -88,13 +99,26 @@ export default class BillingPageOverview extends Component { this._interval = this.calculatePollingInterval(updatedMonth.updated_at); } - this.onDateChange(updatedMonth ?? null); + this.selectedDateOption = updatedMonth ?? null; + this.normalizedMetricData = normalizeMetricData(updatedMonth); return this.months; }; + async initializeBillingMetrics() { + await this.fetchBillingMetrics(); + this.updatePollingState(); + } + + updatePollingState() { + if (this.isCurrentMonth) { + this.startPoll(); + } else { + this.stopPoll(); + } + } + /** - * Starts the polling loop, invoking fetchBillingMetrics immediately and then - * repeatedly on each interval. No-ops if polling is already active. + * Starts the polling loop and repeatedly invokes fetchBillingMetrics on each interval. */ startPoll() { if (this._timer) return; @@ -111,7 +135,7 @@ export default class BillingPageOverview extends Component { } }; - poll(); + this._timer = setTimeout(poll, this._interval); } /** @@ -136,10 +160,16 @@ export default class BillingPageOverview extends Component { }; @action - onDateChange(dropdownOption: Month | null | undefined) { + async onDateChange(dropdownOption: Month | null | undefined) { this.selectedDateOption = dropdownOption; - this.normalizedMetricData = normalizeMetricData(dropdownOption); + + if (this.isCurrentMonth) { + this.stopPoll(); + await this.fetchBillingMetrics(); + } + + this.updatePollingState(); } willDestroy() { diff --git a/ui/app/utils/metrics-helpers.ts b/ui/app/utils/metrics-helpers.ts index 1bbc152139..2338737e14 100644 --- a/ui/app/utils/metrics-helpers.ts +++ b/ui/app/utils/metrics-helpers.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: BUSL-1.1 */ +import { calculateSum } from 'vault/utils/chart-helpers'; + import type { Month, NormalizedMetricsData } from 'vault/vault/billing/overview'; export enum NormalizedBillingMetrics { @@ -87,11 +89,13 @@ export function normalizeMetricData(metric: Month | null | undefined) { typeof normalized[NormalizedBillingMetrics.ID_TOKEN_UNITS_TOTAL] === 'number' ? normalized[NormalizedBillingMetrics.ID_TOKEN_UNITS_TOTAL] : 0; - normalized[NormalizedBillingMetrics.CREDENTIAL_UNITS_TOTAL] = - sshUnitsTotal + pkiUnitsTotal + idTokenUnitsTotal; + normalized[NormalizedBillingMetrics.CREDENTIAL_UNITS_TOTAL] = calculateSum([ + sshUnitsTotal, + pkiUnitsTotal, + idTokenUnitsTotal, + ]); - // The API omits metrics that have zero usage rather than returning them with a count of 0. - // To avoid blank values in the UI, we explicitly set any missing metric keys to 0. + // Explicitly set any missing metric keys to 0. for (const metricsKey of Object.values(NormalizedBillingMetrics)) { if (!(metricsKey in normalized)) { normalized[metricsKey] = 0; diff --git a/ui/tests/acceptance/billing/overview-test.js b/ui/tests/acceptance/billing/overview-test.js index fb92b24889..29c945b3ef 100644 --- a/ui/tests/acceptance/billing/overview-test.js +++ b/ui/tests/acceptance/billing/overview-test.js @@ -27,6 +27,10 @@ module('Acceptance | billing/overview', function (hooks) { hooks.beforeEach(async function () { this.version = this.owner.lookup('service:version'); this.mockMetrics = METRICS_DATA_RESPONSE.data; + this.todayDate = new Date(); + this.currentMonth = this.todayDate.toISOString(); + this.mockMetrics.months[0].month = dateFormat([this.todayDate, 'yyyy-MM'], {}); + this.mockMetrics.months[0].updated_at = this.currentMonth; this.server.get('/sys/billing/overview', () => this.mockMetrics); // Stub the API service @@ -52,14 +56,10 @@ module('Acceptance | billing/overview', function (hooks) { .hasText( '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, - } - )}` - ); + // Vault update every 10 minute only shows if the current month is selected. + assert + .dom(GENERAL.textBody('Last updated date time')) + .hasTextContaining('Values update every 10 minutes.'); assert.dom(GENERAL.cardContainer('Summary')).exists(); @@ -109,6 +109,17 @@ module('Acceptance | billing/overview', function (hooks) { await logout(); }); + test('should not display updated at text if current month is not selected', async function (assert) { + this.server.get('/sys/license/features', () => ({ features: ['Consumption Billing'] })); + await login(); + assert.dom(GENERAL.navLink('Billing metrics')).hasText('Billing metrics'); + await click(GENERAL.navLink('Billing metrics')); + assert.strictEqual(currentURL(), '/vault/billing/overview'); + await click(GENERAL.dropdownToggle('Date range')); + await click(GENERAL.menuItem('2025-12')); + assert.dom(GENERAL.textBody('Last updated date time')).hasTextContaining('Last updated: January 14'); + }); + 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 };