Merge remote-tracking branch 'remotes/from/ce/main'

This commit is contained in:
hc-github-team-secure-vault-core 2026-05-04 17:30:33 +00:00
commit 2c4d9993bb
5 changed files with 70 additions and 21 deletions

View File

@ -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}}
</Hds::Text::Body>

View File

@ -19,6 +19,7 @@
@onDateChange={{this.onDateChange}}
@selectedDateOption={{this.selectedDate}}
@isSelectedDateInvalid={{this.isSelectedDateInvalid}}
@isCurrentMonth={{this.isCurrentMonth}}
/>
<Hds::Layout::Flex @direction="row" @gap="16" @wrap={{true}} class="has-top-margin-s">

View File

@ -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() {

View File

@ -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;

View File

@ -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 };