mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 04:16:31 +02:00
[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>
This commit is contained in:
parent
ebe1a5d496
commit
cb9e2e49bb
@ -5,17 +5,33 @@
|
||||
|
||||
<Hds::Layout::Flex class="has-top-margin-l" @align="center">
|
||||
<Hds::Dropdown @isInline={{true}} as |D|>
|
||||
<D.ToggleButton @text="From start of {{date-format this.selectedDate.month 'MMMM yyyy'}}" @color="secondary" />
|
||||
<D.ToggleButton
|
||||
@text="{{date-format this.selectedDate.month 'MMMM yyyy'}}"
|
||||
@color="secondary"
|
||||
data-test-dropdown="Date range"
|
||||
/>
|
||||
{{#each this.dateDropdownOptions as |option|}}
|
||||
<D.Checkmark
|
||||
@selected={{eq option.value this.selectedDate.month}}
|
||||
{{on "click" (fn this.updateSelectedDropdownOption option.value)}}
|
||||
data-test-popup-menu={{option.value}}
|
||||
>
|
||||
{{option.label}}
|
||||
</D.Checkmark>
|
||||
{{/each}}
|
||||
</Hds::Dropdown>
|
||||
<Hds::Text::Body @tag="p" @size="200" @color="foreground-primary" class="has-left-margin-s">Values update every 10 minutes.
|
||||
Last updated:
|
||||
{{date-format @selectedDateOption.updated_at "hh:mm:ss a"}}</Hds::Text::Body>
|
||||
<Hds::Text::Body
|
||||
@tag="p"
|
||||
@size="200"
|
||||
@color="foreground-primary"
|
||||
class="has-left-margin-s"
|
||||
data-test-text-body="Last updated date time"
|
||||
>
|
||||
{{#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}}
|
||||
</Hds::Text::Body>
|
||||
</Hds::Layout::Flex>
|
||||
@ -24,8 +24,8 @@ export default class BillingDateRange extends Component<Args> {
|
||||
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;
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
</:breadcrumbs>
|
||||
<:description>
|
||||
<Hds::Text::Body>
|
||||
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.
|
||||
</Hds::Text::Body>
|
||||
</:description>
|
||||
<:actions>
|
||||
@ -17,7 +17,12 @@
|
||||
</:actions>
|
||||
</Page::Header>
|
||||
|
||||
<Billing::DateRange @months={{this.months}} @onDateChange={{this.onDateChange}} @selectedDateOption={{this.selectedDate}} />
|
||||
<Billing::DateRange
|
||||
@months={{this.months}}
|
||||
@onDateChange={{this.onDateChange}}
|
||||
@selectedDateOption={{this.selectedDate}}
|
||||
@isSelectedDateInvalid={{this.isSelectedDateInvalid}}
|
||||
/>
|
||||
|
||||
<Hds::Layout::Flex @direction="row" @gap="16" @wrap={{true}} class="has-top-margin-s">
|
||||
<Hds::Layout::Flex::Item @basis="30%" @grow={{true}} @shrink={{true}}>
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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}"]`,
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user