Backport UI: Render total monthly clients for HVD managed clusters and "new" for only self-managed into ce/main (#10703)

* UI: Render total monthly clients for HVD managed clusters and "new" for only self-managed (#10472)

* rename byMonthNewClients to byMonthClients

* render total vs new clients depending on cluster type

* fix mutation of original array order

* add changelog

* Apply suggestion from @hellobontempo

* Apply suggestion from @hellobontempo

* Apply suggestion from @hellobontempo

* add test coverage

* hide client list tab for HVD clusters

* add test coverage

* make copy changes

* apply copy updates from feedback sync

* skip hvd test on CE runs

* restart tests

---------

Co-authored-by: claire bontempo <68122737+hellobontempo@users.noreply.github.com>
Co-authored-by: claire bontempo <cbontempo@hashicorp.com>
This commit is contained in:
Vault Automation 2025-11-11 19:12:16 -05:00 committed by GitHub
parent 90571de3bc
commit e8ef7285a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 263 additions and 87 deletions

3
changelog/_10472.txt Normal file
View file

@ -0,0 +1,3 @@
```release-note:improvement
ui/activity: Display total instead of new monthly clients for HCP managed clusters
```

View file

@ -48,7 +48,7 @@ type ChartDatum = DatumBase & {
* @example
* <Clients::Charts::VerticalBarStacked
* @chartTitle="Total monthly usage"
* @data={{this.byMonthNewClients}}
* @data={{this.byMonthClients}}
* @chartLegend={{this.legend}}
* @chartHeight={{250}}
* />

View file

@ -39,7 +39,11 @@ Data visualizations render in in a flex row with a 1/3-width left element and a
<div class="legend-container" data-test-counts-card-legend>
{{#each @legend as |l idx|}}
<div class="legend-item">
<span class="dots legend-dot-{{if (eq l.key 'new_clients') 'new_clients' idx}}"></span>
{{#if (includes l.key (array "clients" "new_clients"))}}
<span class="dots legend-dot-total"></span>
{{else}}
<span class="dots legend-dot-{{idx}}"></span>
{{/if}}
<Hds::Text::Body @tag="p" @size="100">{{l.label}}</Hds::Text::Body>
</div>
{{/each}}

View file

@ -7,14 +7,14 @@
<div class="is-flex-column align-items-end">
{{#if this.version.isEnterprise}}
<Hds::Text::Display @tag="p" @size="100" class="has-bottom-margin-xs">
Change billing period
{{if this.flags.isHvdManaged "Change data period" "Change billing period"}}
</Hds::Text::Display>
<Hds::Dropdown class="has-left-margin-xs" @matchToggleWidth={{true}} as |D|>
<D.ToggleButton @text="Billing start date" @color="secondary" data-test-date-range-edit />
<Hds::Dropdown class="has-left-margin-xs" as |D|>
<D.ToggleButton @text={{this.formatDropdownDate @startTimestamp}} @color="secondary" data-test-date-range-edit />
<D.Description @text="Current period" />
<D.Checkmark
{{! Pass an empty string to reset query param because the current billing period is the default }}
{{on "click" (fn this.updateEnterpriseDateRange "")}}
{{on "click" (fn this.updateEnterpriseDateRange "" D.close)}}
@selected={{this.isSelected @billingStartTime}}
data-test-date-range-billing-start="0"
>
@ -25,7 +25,7 @@
<D.Description @text="Historical periods" />
{{#each this.historicalBillingPeriods as |period idx|}}
<D.Checkmark
{{on "click" (fn this.updateEnterpriseDateRange period)}}
{{on "click" (fn this.updateEnterpriseDateRange period D.close)}}
data-test-date-range-billing-start={{add idx 1}}
@selected={{this.isSelected period}}
>

View file

@ -11,6 +11,7 @@ import { buildISOTimestamp, parseAPITimestamp } from 'core/utils/date-formatters
import timestamp from 'core/utils/timestamp';
import { format } from 'date-fns';
import type FlagsService from 'vault/services/flags';
import type VersionService from 'vault/services/version';
import type { HTMLElementEvent } from 'forms';
@ -46,6 +47,7 @@ interface Args {
*/
export default class ClientsDateRangeComponent extends Component<Args> {
@service declare readonly flags: FlagsService;
@service declare readonly version: VersionService;
@tracked modalStart = ''; // format yyyy-MM
@ -64,7 +66,8 @@ export default class ClientsDateRangeComponent extends Component<Args> {
get historicalBillingPeriods() {
// we want whole billing periods
const count = Math.floor(this.args.retentionMonths / 12);
const totalMonths = this.args.retentionMonths || 48;
const count = Math.floor(totalMonths / 12);
const periods: string[] = [];
for (let i = 1; i <= count; i++) {
@ -118,9 +121,10 @@ export default class ClientsDateRangeComponent extends Component<Args> {
}
@action
updateEnterpriseDateRange(start: string) {
updateEnterpriseDateRange(start: string, close: CallableFunction) {
// We do not send an end_time so the backend handles computing the expected billing period
this.args.onChange({ start_time: start, end_time: '' });
close();
}
// HELPERS

View file

@ -26,7 +26,7 @@
{{#if this.version.isEnterprise}}
<PH.Description class="has-text-weight-semibold flex">
<p>
For billing period:
{{if this.flags.isHvdManaged "For data period:" "For billing period:"}}
<span data-test-date-range="start">{{this.formattedStartDate}}</span>
-
<span data-test-date-range="end">{{this.formattedEndDate}}</span>

View file

@ -32,6 +32,7 @@ import { task } from 'ember-concurrency';
*/
export default class ClientsPageHeaderComponent extends Component {
@service download;
@service flags;
@service namespace;
@service router;
@service store;

View file

@ -17,7 +17,7 @@
<div class="has-top-bottom-margin">
<Hds::Text::Body @tag="p" @size="100" class="has-bottom-margin-s">
This is the dashboard for your overall client count usages. Review Vault's
This is the dashboard for your overall client count usage. Review Vault's
<Hds::Link::Inline @href={{doc-link "/vault/docs/concepts/client-count"}} @isHrefExternal={{true}}>
client counting documentation</Hds::Link::Inline>
for more information.
@ -69,8 +69,12 @@
</Hds::Alert>
{{/if}}
{{#if this.version.isEnterprise}}
{{#if (and this.version.isEnterprise (not this.flags.isHvdManaged))}}
{{! The "Client list" tab only renders for enterprise versions so there is no need for the nav bar }}
{{! The "Client list" tab is hidden on HVD managed clusters (for now) because the "Month" filter for that page
uses the `client_first_used_time` timestamp. This timestamp tracks when a client is FIRST seen in the queried date range (i.e. billing period).
This is useful for self-managed customers who are billed on monthly NEW clients, but not for HVD users who are billed on TOTAL clients per
month regardless of whether the client was seen in a previous month. }}
<Clients::Counts::NavBar />
{{/if}}

View file

@ -10,6 +10,7 @@ import { parseAPITimestamp } from 'core/utils/date-formatters';
import { filterVersionHistory } from 'core/utils/client-count-utils';
import type AdapterError from '@ember-data/adapter/error';
import type FlagsService from 'vault/services/flags';
import type VersionService from 'vault/services/version';
import type ClientsActivityModel from 'vault/models/clients/activity';
import type ClientsConfigModel from 'vault/models/clients/config';
@ -26,6 +27,7 @@ interface Args {
}
export default class ClientsCountsPageComponent extends Component<Args> {
@service declare readonly flags: FlagsService;
@service declare readonly version: VersionService;
get formattedStartDate() {

View file

@ -3,18 +3,12 @@
SPDX-License-Identifier: BUSL-1.1
}}
<Clients::RunningTotal @byMonthNewClients={{this.byMonthNewClients}} @runningTotals={{@activity.total}} />
<Clients::RunningTotal @byMonthClients={{this.byMonthClients}} @runningTotals={{@activity.total}} />
{{! by_namespace is an empty array when there is no client count activity data }}
{{#if @activity.byNamespace}}
<Clients::CountsCard
@title="Client attribution"
@description="Select a month to view the client count per mount for that month."
>
<Clients::CountsCard @title="Client attribution">
<:subheader>
<Hds::Text::Body class="has-top-margin-l has-bottom-margin-m" @tag="p" @size="100" @color="faint">Use the filters to
view the clients attributed by path.
</Hds::Text::Body>
<Clients::FilterToolbar
@dataset={{this.activityData}}
@onFilter={{this.handleFilter}}

View file

@ -7,9 +7,11 @@ import Component from '@glimmer/component';
import { cached, tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { filterTableData, flattenMounts } from 'core/utils/client-count-utils';
import { service } from '@ember/service';
import type ClientsActivityModel from 'vault/vault/models/clients/activity';
import type { ClientFilterTypes } from 'vault/vault/client-counts/activity-api';
import type FlagsService from 'vault/services/flags';
export interface Args {
activity: ClientsActivityModel;
@ -18,10 +20,18 @@ export interface Args {
}
export default class ClientsOverviewPageComponent extends Component<Args> {
@service declare readonly flags: FlagsService;
@tracked selectedMonth = '';
@cached
get byMonthNewClients() {
get byMonthClients() {
// HVD clusters are billed differently and the monthly total is the important metric.
if (this.flags.isHvdManaged) {
return this.args.activity.byMonth;
}
// For self-managed clusters only the new_clients per month are relevant because clients accumulate over a billing period.
// (Since "total" per month is not cumulative it's not a useful metric)
return this.args.activity.byMonth?.map((m) => m?.new_clients) || [];
}
@ -30,7 +40,7 @@ export default class ClientsOverviewPageComponent extends Component<Args> {
// If no month is selected the table displays all of the activity for the queried date range.
const selectedMonth = this.args.filterQueryParams.month;
const namespaceData = selectedMonth
? this.byMonthNewClients.find((m) => m.timestamp === selectedMonth)?.namespaces
? this.byMonthClients.find((m) => m.timestamp === selectedMonth)?.namespaces
: this.args.activity.byNamespace;
// Get the array of "mounts" data nested in each namespace object and flatten
@ -39,7 +49,7 @@ export default class ClientsOverviewPageComponent extends Component<Args> {
@cached
get months() {
return this.byMonthNewClients.reverse().map((m) => m.timestamp);
return this.byMonthClients.map((m) => m.timestamp).reverse();
}
get tableData() {

View file

@ -3,16 +3,16 @@
SPDX-License-Identifier: BUSL-1.1
}}
{{#if @byMonthNewClients.length}}
<Clients::CountsCard
@title="Client usage trends for selected billing period"
@description={{this.chartContainerText}}
@legend={{this.chartLegend}}
>
{{#if @byMonthClients.length}}
<Clients::CountsCard @title="Client usage trends" @description={{this.chartContainerText}} @legend={{this.chartLegend}}>
<:dataLeft>
<Hds::Text::Body @tag="p">Client count and type distribution</Hds::Text::Body>
<VaultReporting::DonutChart @data={{this.donutChartData}} @title="Total Clients" class="donut-chart" />
<VaultReporting::DonutChart
@data={{this.donutChartData}}
@title={{if this.flags.isHvdManaged "Total unique clients" "Total clients"}}
class="donut-chart"
/>
</:dataLeft>
<:dataRight>
@ -37,7 +37,7 @@
<Clients::Charts::VerticalBarBasic
@chartTitle="Client usage by month"
@data={{this.runningTotalData}}
@dataKey="new_clients"
@dataKey={{this.dataKey}}
@chartHeight={{200}}
/>
{{/if}}
@ -45,7 +45,7 @@
</Clients::CountsCard>
{{else}}
{{! Renders when viewing activity log data that predates the monthly breakdown added in 1.11 }}
<Clients::UsageStats @title="Client usage" @description="Total client usage for the selected billing period.">
<Clients::UsageStats @title="Client usage" @description="Total client usage for the selected date range.">
<StatText
class="column"
@label="Total clients"

View file

@ -6,30 +6,42 @@
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { service } from '@ember/service';
import { toLabel } from 'core/helpers/to-label';
import type { ByMonthNewClients, TotalClients } from 'vault/vault/client-counts/activity-api';
import type { ByMonthClients, ByMonthNewClients, TotalClients } from 'vault/vault/client-counts/activity-api';
import type FlagsService from 'vault/services/flags';
import type VersionService from 'vault/services/version';
interface Args {
byMonthNewClients: ByMonthNewClients[];
byMonthClients: ByMonthClients[] | ByMonthNewClients[];
runningTotals: TotalClients;
}
export default class RunningTotal extends Component<Args> {
@service declare readonly flags: FlagsService;
@service declare readonly version: VersionService;
@tracked showStacked = false;
get chartContainerText() {
return `The total clients in the specified date range, displayed per month. This includes entity, non-entity${
this.flags.secretsSyncIsActivated ? ', ACME and secrets sync clients' : ' and ACME clients'
}. The total client count number is an important consideration for Vault billing.`;
const range = this.version.isEnterprise ? 'billing period' : 'date range';
return this.flags.isHvdManaged
? 'Number of total unique clients in the data period by client type, and total number of unique clients per month. The monthly total is the relevant billing metric.'
: `Number of clients in the ${range} by client type, and a breakdown of new clients per month during the ${range}. `;
}
get dataKey() {
return this.flags.isHvdManaged ? 'clients' : 'new_clients';
}
get runningTotalData() {
return this.args.byMonthNewClients.map((monthly) => ({
// The parent component determines whether `monthly.clients` in @byMonthClients represents "new" or "total" clients per month.
// (We render "new" for self-managed clusters and "total" for HVD-managed.)
// As a result, we do not use `this.dataKey` to select a property from `monthly` but to add a superficial key
// to the data that ensures the chart tooltip and legend text render appropriately.
return this.args.byMonthClients.map((monthly) => ({
...monthly,
new_clients: monthly.clients,
[this.dataKey]: monthly.clients,
}));
}
@ -54,6 +66,6 @@ export default class RunningTotal extends Component<Args> {
...(this.flags.secretsSyncIsActivated ? [{ key: 'secret_syncs', label: 'Secret sync clients' }] : []),
];
}
return [{ key: 'new_clients', label: 'New clients' }];
return [{ key: this.dataKey, label: toLabel([this.dataKey]) }];
}
}

View file

@ -25,7 +25,11 @@
<B.Td data-test-table-data={{key}} class="white-space-nowrap">
<Hds::Copy::Snippet @textToCopy={{value}} @color="secondary" />
</B.Td>
{{else if (and (eq key "clients") this.version.isEnterprise)}}
{{else if (and (eq key "clients") this.version.isEnterprise (not this.flags.isHvdManaged))}}
{{! The "Client list" tab is hidden on HVD managed clusters (for now) because the "Month" filter for that page
uses the `client_first_used_time` timestamp. This timestamp tracks when a client is FIRST seen in the queried date range (i.e. billing period).
This is useful for self-managed customers who are billed on monthly NEW clients, but not for HVD users who are billed on TOTAL clients per
month regardless of whether the client was seen in a previous month. }}
<B.Td data-test-table-data={{key}} class="white-space-nowrap">
<Hds::Link::Inline
@route="vault.cluster.clients.counts.client-list"

View file

@ -10,6 +10,7 @@ import { paginate } from 'core/utils/paginate-list';
import { next } from '@ember/runloop';
import { service } from '@ember/service';
import type FlagsService from 'vault/services/flags';
import type VersionService from 'vault/services/version';
import type { ClientFilterTypes } from 'vault/vault/client-counts/activity-api';
@ -52,6 +53,7 @@ interface Args {
}
export default class ClientsTable extends Component<Args> {
@service declare readonly flags: FlagsService;
@service declare readonly version: VersionService;
@tracked currentPage = 1;

View file

@ -78,7 +78,8 @@ export default class ClientsCountsRoute extends Route {
async fetchAndFormatExportData(startTimestamp: string | undefined, endTimestamp: string | undefined) {
// The "Client List" tab is only available on enterprise versions
if (this.version.isEnterprise) {
// For now, it is also hidden on HVD managed clusters
if (this.version.isEnterprise && !this.flags.isHvdManaged) {
const adapter = this.store.adapterFor('clients/activity');
let exportData, exportError;
try {

View file

@ -4,5 +4,25 @@
*/
import Route from '@ember/routing/route';
import { service } from '@ember/service';
export default class ClientsCountsClientListRoute extends Route {}
import type FlagsService from 'vault/services/flags';
import type RouterService from '@ember/routing/router-service';
import type VersionService from 'vault/services/version';
export default class ClientsCountsClientListRoute extends Route {
@service declare readonly flags: FlagsService;
@service declare readonly router: RouterService;
@service declare readonly version: VersionService;
// The "Client list" tab is only available on enterprise versions
// The "Client list" tab is hidden on HVD managed clusters (for now) because the "Month" filter for that page
// uses the `client_first_used_time` timestamp. This timestamp tracks when a client is FIRST seen in the queried date range (i.e. billing period).
// This is useful for self-managed customers who are billed on monthly NEW clients, but not for HVD users who are billed on TOTAL clients per
// month regardless of whether the client was seen in a previous month.
redirect() {
if (this.version.isCommunity || this.flags.isHvdManaged) {
this.router.transitionTo('vault.cluster.clients.counts');
}
}
}

View file

@ -20,7 +20,7 @@ $fourth: #6cc5b0;
border-radius: 50%;
display: inline-block;
}
.legend-dot-new_clients {
.legend-dot-total {
background-color: $single;
}
// numbers are indices because chart legend is iterated over to ensure

View file

@ -53,6 +53,24 @@ module('Acceptance | clients | counts | client list', function (hooks) {
test('it hides client list tab on community', async function (assert) {
this.version.type = 'community';
assert.dom(GENERAL.tab('client list')).doesNotExist();
// Navigate directly to URL to test redirect
await visit('/vault/clients/counts/client-list');
assert.strictEqual(currentURL(), '/vault/clients/counts/overview', 'it redirects to overview');
});
// skip this test on CE test runs because GET sys/license/features an enterprise only endpoint
test('enterprise: it hides client list tab on HVD managed clusters', async function (assert) {
this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
assert.dom(GENERAL.tab('client list')).doesNotExist();
// Navigate directly to URL to test redirect
await visit('/vault/clients/counts/client-list');
assert.strictEqual(
currentURL(),
'/vault/clients/counts/overview?namespace=admin',
'it redirects to overview'
);
});
test('it navigates to client list tab', async function (assert) {

View file

@ -67,10 +67,10 @@ module('Acceptance | clients | overview', function (hooks) {
.dom(CLIENT_COUNT.dateRange.dateDisplay('end'))
.hasText('January 2024', 'end month is correctly parsed from STATIC_NOW');
assert
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
.dom(CLIENT_COUNT.card('Client usage trends'))
.exists('Shows running totals with monthly breakdown charts');
assert
.dom(`${CLIENT_COUNT.card('Client usage trends for selected billing period')} ${CHARTS.xAxisLabel}`)
.dom(`${CLIENT_COUNT.card('Client usage trends')} ${CHARTS.xAxisLabel}`)
.hasText('7/23', 'x-axis labels start with billing start date');
assert.dom(CHARTS.xAxisLabel).exists({ count: 7 }, 'chart months matches query');
});
@ -95,7 +95,7 @@ module('Acceptance | clients | overview', function (hooks) {
.dom(CLIENT_COUNT.usageStats('Client usage'))
.exists('running total single month usage stats show');
assert
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
.dom(CLIENT_COUNT.card('Client usage trends'))
.doesNotExist('running total month over month charts do not show');
// change to start on month/year of upgrade to 1.10
@ -108,10 +108,10 @@ module('Acceptance | clients | overview', function (hooks) {
.dom(CLIENT_COUNT.dateRange.dateDisplay('start'))
.hasText('September 2023', 'client count start month is correctly parsed from start query');
assert
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
.dom(CLIENT_COUNT.card('Client usage trends'))
.exists('Shows running totals with monthly breakdown charts');
assert
.dom(`${CLIENT_COUNT.card('Client usage trends for selected billing period')} ${CHARTS.xAxisLabel}`)
.dom(`${CLIENT_COUNT.card('Client usage trends')} ${CHARTS.xAxisLabel}`)
.hasText('9/23', 'x-axis labels start with queried start month (upgrade date)');
assert.dom(CHARTS.xAxisLabel).exists({ count: 4 }, 'chart months matches query');
@ -121,7 +121,7 @@ module('Acceptance | clients | overview', function (hooks) {
await fillIn(CLIENT_COUNT.dateRange.editDate('end'), upgradeMonth);
await click(GENERAL.submitButton);
assert
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
.dom(CLIENT_COUNT.card('Client usage trends'))
.exists('running total month over month charts show');
// query historical date range (from September 2023 to December 2023)
@ -137,7 +137,7 @@ module('Acceptance | clients | overview', function (hooks) {
.dom(CLIENT_COUNT.dateRange.dateDisplay('end'))
.hasText('December 2023', 'it displays correct end time');
assert
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
.dom(CLIENT_COUNT.card('Client usage trends'))
.exists('Shows running totals with monthly breakdown charts');
assert.dom(CHARTS.xAxisLabel).exists({ count: 4 }, 'chart months matches query');
@ -154,6 +154,14 @@ module('Acceptance | clients | overview', function (hooks) {
});
});
test('it does not render client list links for HVD managed clusters', async function (assert) {
this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
assert
.dom(`${GENERAL.tableData(0, 'clients')} a`)
.doesNotExist('client counts do not render as hyperlinks');
});
// * FILTERING ASSERTIONS
// These tests use the static data from the ACTIVITY_RESPONSE_STUB to assert filtering
// Filtering tests are split between integration and acceptance tests
@ -336,21 +344,5 @@ module('Acceptance | clients | overview', function (hooks) {
'it renders legend in order that matches the stacked bar data and does not include secret sync'
);
});
test('it should show secrets sync stats for HVD managed clusters', async function (assert) {
// mock HVD managed cluster
this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
await login();
await visit('/vault/clients/counts/overview');
assert.dom(CLIENT_COUNT.statLegendValue('Secret sync clients')).exists();
await click(GENERAL.inputByAttr('toggle view'));
assert
.dom(CHARTS.legend)
.hasText(
'Entity clients Non-entity clients ACME clients Secret sync clients',
'it renders legend in order that matches the stacked bar data'
);
});
});
});

View file

@ -109,7 +109,7 @@ module('Integration | Component | clients/date-range', function (hooks) {
this.billingStartTime = '2018-01-01T14:15:30';
});
test('it billing start date dropdown for enterprise', async function (assert) {
test('it renders billing start date dropdown for enterprise', async function (assert) {
await this.renderComponent();
await click(DATE_RANGE.edit);
const expectedPeriods = [
@ -125,5 +125,32 @@ module('Integration | Component | clients/date-range', function (hooks) {
assert.dom(item).hasText(month, `dropdown index: ${idx} renders ${month}`);
});
});
test('it updates toggle text when a new date is selected', async function (assert) {
this.onChange = ({ start_time }) => this.set('startTimestamp', start_time);
await this.renderComponent();
assert.dom(DATE_RANGE.edit).hasText('January 2018').hasAttribute('aria-expanded', 'false');
await click(DATE_RANGE.edit);
assert.dom(DATE_RANGE.edit).hasAttribute('aria-expanded', 'true');
await click(DATE_RANGE.dropdownOption(1));
assert
.dom(DATE_RANGE.edit)
.hasText('January 2017')
.hasAttribute('aria-expanded', 'false', 'it closes dropdown after selection');
});
test('it renders billing period text', async function (assert) {
await this.renderComponent();
assert
.dom(this.element)
.hasText('Change billing period January 2018', 'it renders billing related text');
});
test('it renders data period text for HVD managed clusters', async function (assert) {
this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
await this.renderComponent();
assert.dom(this.element).hasText('Change data period January 2018');
});
});
});

View file

@ -34,6 +34,7 @@ module('Integration | Component | clients/page-header', function (hooks) {
this.renderComponent = async () => {
return render(hbs`
<Clients::PageHeader
@billingStartTime={{this.startTimestamp}}
@startTimestamp={{this.startTimestamp}}
@endTimestamp={{this.endTimestamp}}
@upgradesDuringActivity={{this.upgradesDuringActivity}}
@ -259,4 +260,24 @@ module('Integration | Component | clients/page-header', function (hooks) {
assert.strictEqual(filename, 'clients_export_bar');
});
});
module('enterprise', function (hooks) {
hooks.beforeEach(function () {
this.version = this.owner.lookup('service:version');
this.version.type = 'enterprise';
});
test('it renders billing period text', async function (assert) {
await this.renderComponent();
assert
.dom(this.element)
.hasTextContaining('Client Usage For billing period:', 'it renders billing related text');
});
test('it renders data period text for HVD managed clusters', async function (assert) {
this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
await this.renderComponent();
assert.dom(this.element).hasTextContaining('Client Usage For data period:');
});
});
});

View file

@ -9,7 +9,7 @@ import { click, find, findAll, render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { ACTIVITY_RESPONSE_STUB } from 'vault/tests/helpers/clients/client-count-helpers';
import { CLIENT_COUNT, FILTERS } from 'vault/tests/helpers/clients/client-count-selectors';
import { CHARTS, CLIENT_COUNT, FILTERS } from 'vault/tests/helpers/clients/client-count-selectors';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import sinon from 'sinon';
import { ClientFilters, flattenMounts } from 'core/utils/client-count-utils';
@ -143,6 +143,21 @@ module('Integration | Component | clients/page/overview', function (hooks) {
.hasText('No data found Clear or change filters to view client count data. Client count documentation');
});
test('it renders NEW monthly clients for self-managed clusters instead of total clients', async function (assert) {
this.filterQueryParams = {
month: this.mostRecentMonth.timestamp,
};
const topMount = this.mostRecentMonth.new_clients.namespaces
.find((ns) => ns.label === 'ns1/')
.mounts.find((m) => m.label === 'auth/userpass/0/');
await this.renderComponent();
assert.dom(CHARTS.legend).hasText('New clients');
assert
.dom(GENERAL.tableData(0, 'clients'))
.hasText(`${topMount.clients}`, 'table renders total monthly clients');
});
test('it filters data if @filterQueryParams specify a month', async function (assert) {
const filterKey = 'month';
const filterValue = this.mostRecentMonth.timestamp;
@ -209,4 +224,20 @@ module('Integration | Component | clients/page/overview', function (hooks) {
.dom(CLIENT_COUNT.card('table empty state'))
.hasText('No data found Clear or change filters to view client count data. Client count documentation');
});
test('it renders TOTAL monthly clients for HVD instead of new clients', async function (assert) {
this.owner.lookup('service:flags').featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
this.filterQueryParams = {
month: this.mostRecentMonth.timestamp,
};
const topMount = this.mostRecentMonth.namespaces
.find((ns) => ns.label === 'root')
.mounts.find((m) => m.label === 'acme/pki/0/');
await this.renderComponent();
assert.dom(CHARTS.legend).hasText('Clients');
assert
.dom(GENERAL.tableData(0, 'clients'))
.hasText(`${topMount.clients}`, 'table renders total monthly clients');
});
});

View file

@ -26,6 +26,7 @@ module('Integration | Component | clients/running-total', function (hooks) {
hooks.beforeEach(async function () {
this.flags = this.owner.lookup('service:flags');
this.version = this.owner.lookup('service:version');
this.flags.activatedFlags = ['secrets-sync'];
sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW));
clientsHandler(this.server);
@ -35,24 +36,53 @@ module('Integration | Component | clients/running-total', function (hooks) {
end_time: { timestamp: getUnixTime(timestamp.now()) },
};
this.activity = await store.queryRecord('clients/activity', activityQuery);
this.byMonthNewClients = this.activity.byMonth.map((d) => d.new_clients);
this.byMonthClients = this.activity.byMonth.map((d) => d.new_clients);
this.renderComponent = async () => {
await render(hbs`
<Clients::RunningTotal
@byMonthNewClients={{this.byMonthNewClients}}
@byMonthClients={{this.byMonthClients}}
@runningTotals={{this.activity.total}}
/>
`);
};
});
test('it text for community versions', async function (assert) {
this.version.type = 'community';
await this.renderComponent();
assert
.dom(`${CLIENT_COUNT.card('Client usage trends')} p`)
.hasText(
'Number of clients in the date range by client type, and a breakdown of new clients per month during the date range.'
);
});
// abbreviating "ent" so the test is not filtered out in CE repo runs
test('it renders text for ent versions', async function (assert) {
this.version.type = 'enterprise';
await this.renderComponent();
assert
.dom(`${CLIENT_COUNT.card('Client usage trends')} p`)
.hasText(
'Number of clients in the billing period by client type, and a breakdown of new clients per month during the billing period.'
);
});
test('it renders text for HVD managed versions', async function (assert) {
this.flags.featureFlags = ['VAULT_CLOUD_ADMIN_NAMESPACE'];
await this.renderComponent();
assert
.dom(`${CLIENT_COUNT.card('Client usage trends')} p`)
.hasText(
'Number of total unique clients in the data period by client type, and total number of unique clients per month. The monthly total is the relevant billing metric.'
);
});
test('it renders with full monthly activity data', async function (assert) {
await this.renderComponent();
assert
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
.exists('running total component renders');
assert.dom(CLIENT_COUNT.card('Client usage trends')).exists('running total component renders');
assert.dom(CHARTS.chart('Client usage by month')).exists('bar chart renders');
assert.dom(CHARTS.legend).hasText('New clients');
const expectedColor = 'rgb(28, 52, 95)';
@ -76,22 +106,20 @@ module('Integration | Component | clients/running-total', function (hooks) {
// assert bar chart is correct
findAll(CHARTS.xAxisLabel).forEach((e, i) => {
const timestamp = this.byMonthNewClients[i].timestamp;
const timestamp = this.byMonthClients[i].timestamp;
const displayMonth = parseAPITimestamp(timestamp, 'M/yy');
assert.dom(e).hasText(displayMonth, `renders x-axis labels for bar chart: ${displayMonth}`);
});
assert
.dom(CHARTS.verticalBar)
.exists({ count: this.byMonthNewClients.length }, 'renders correct number of bars ');
.exists({ count: this.byMonthClients.length }, 'renders correct number of bars ');
});
test('it toggles to split chart by client type', async function (assert) {
await this.renderComponent();
await click(GENERAL.inputByAttr('toggle view'));
assert
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
.exists('running total component renders');
assert.dom(CLIENT_COUNT.card('Client usage trends')).exists('running total component renders');
assert.dom(CHARTS.chart('Client usage by month')).exists('bar chart renders');
assert
.dom(CHARTS.legend)
@ -117,12 +145,12 @@ module('Integration | Component | clients/running-total', function (hooks) {
// assert bar chart is correct
findAll(CHARTS.xAxisLabel).forEach((e, i) => {
const timestamp = this.byMonthNewClients[i].timestamp;
const timestamp = this.byMonthClients[i].timestamp;
const displayMonth = parseAPITimestamp(timestamp, 'M/yy');
assert.dom(e).hasText(`${displayMonth}`, `renders x-axis labels for bar chart: ${displayMonth}`);
});
const months = this.byMonthNewClients.length;
const months = this.byMonthClients.length;
const barsPerMonth = expectedLegend.length;
assert
.dom(CHARTS.verticalBar)
@ -130,7 +158,7 @@ module('Integration | Component | clients/running-total', function (hooks) {
});
test('it renders when no monthly breakdown is available', async function (assert) {
this.byMonthNewClients = [];
this.byMonthClients = [];
await this.renderComponent();
const expectedStats = {
Entity: formatNumber([this.activity.total.entity_clients]),
@ -153,13 +181,11 @@ module('Integration | Component | clients/running-total', function (hooks) {
test('it hides secret sync totals when feature is not activated', async function (assert) {
this.flags.activatedFlags = [];
// reset secret sync clients to 0
this.byMonthNewClients = this.byMonthNewClients.map((obj) => ({ ...obj, secret_syncs: 0 }));
this.byMonthClients = this.byMonthClients.map((obj) => ({ ...obj, secret_syncs: 0 }));
await this.renderComponent();
assert
.dom(CLIENT_COUNT.card('Client usage trends for selected billing period'))
.exists('running total component renders');
assert.dom(CLIENT_COUNT.card('Client usage trends')).exists('running total component renders');
assert.dom(CHARTS.chart('Client usage by month')).exists('bar chart renders');
assert.dom(CLIENT_COUNT.statLegendValue('Entity clients')).exists();
assert.dom(CLIENT_COUNT.statLegendValue('Non-entity clients')).exists();
@ -187,7 +213,7 @@ module('Integration | Component | clients/running-total', function (hooks) {
assert.strictEqual(dotColor, color, `${label} - actual color: ${dotColor}, expected: ${color}`);
});
const months = this.byMonthNewClients.length;
const months = this.byMonthClients.length;
const barsPerMonth = expectedLegend.length;
assert
.dom(CHARTS.verticalBar)