UI: Replace Client Count Attribution Charts with Table (#30678)

* replace attribution charts with a table by month

* update tests to include mount_type

* fix another portion of tests that were missing secret sync stat and testing for old attribution charts

* add tests for attribution table

* add changelog and tidy

* remove remaining todos

* tidy

* reset month query param in ce

* fix tests missing month param

* add margin to pagination in accordance to helios rec

* remove query param, update change log, move table into own comp

* remove commented code

* remove month query params

* tidy

* update test mount paths

* remove unused client attribution component

* update tests
This commit is contained in:
lane-wetmore 2025-05-23 10:58:48 -05:00 committed by GitHub
parent 9de78a5136
commit 2e6d5b0703
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 399 additions and 434 deletions

5
changelog/30678.txt Normal file
View file

@ -0,0 +1,5 @@
```release-note:change
ui/activity: Replaces mount and namespace attribution charts with a table to allow sorting
client count data by `namespace`, `mount_path`, `mount_type` or number of clients for
a selected month.
```

View file

@ -1,39 +0,0 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<div class="chart-wrapper single-chart-grid" data-test-clients-attribution={{this.noun}}>
<div class="chart-header has-bottom-margin-m">
<h2 class="chart-title" data-test-attribution-title>{{capitalize this.noun}} attribution</h2>
<p class="chart-description" data-test-attribution-description>{{this.chartText.description}}</p>
</div>
{{#if @attribution}}
<div class="chart-container-wide" data-test-chart-container={{this.noun}}>
<Clients::HorizontalBarChart @dataset={{this.topTenAttribution}} @chartLegend={{this.attributionLegend}} />
</div>
<div class="chart-subTitle">
<p class="chart-subtext" data-test-attribution-subtext>{{this.chartText.subtext}}</p>
</div>
<div class="data-details-top" data-test-top-attribution>
<h3 class="data-details">Top {{this.noun}}</h3>
<p class="data-details is-word-break">{{this.topAttribution.label}}</p>
</div>
<div class="data-details-bottom" data-test-attribution-clients>
<h3 class="data-details">Clients in {{this.noun}}</h3>
<p class="data-details">{{format-number this.topAttribution.clients}}</p>
</div>
<div class="legend">
{{#each this.attributionLegend as |legend idx|}}
<span class="legend-colors dot-{{idx}}"></span><span class="legend-label">{{capitalize legend.label}}</span>
{{/each}}
</div>
{{else}}
<div class="chart-empty-state">
<EmptyState @icon="skip" @title="No data found" @bottomBorder={{true}} />
</div>
{{/if}}
</div>

View file

@ -1,77 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
/**
* @module Attribution
* Attribution components display the top 10 total client counts for namespaces or mounts during a billing period.
* A horizontal bar chart shows on the right, with the top namespace/mount and respective client totals on the left.
*
* @example
* <Clients::Attribution
* @noun="mount"
* @attribution={{array (hash label="my-kv" clients=100)}}
* @isSecretsSyncActivated={{true}}
* />
*
* @param {string} noun - noun which reflects the type of data and used in title. Should be "namespace" (default) or "mount"
* @param {array} attribution - array of objects containing a label and breakdown of client counts for total clients
* @param {boolean} isSecretsSyncActivated - boolean reflecting if secrets sync is activated. Determines the labels and data shown
*/
export default class Attribution extends Component {
get noun() {
return this.args.noun || 'namespace';
}
get attributionLegend() {
const attributionLegend = [
{ key: 'entity_clients', label: 'entity clients' },
{ key: 'non_entity_clients', label: 'non-entity clients' },
{ key: 'acme_clients', label: 'ACME clients' },
];
if (this.args.isSecretsSyncActivated) {
attributionLegend.push({ key: 'secret_syncs', label: 'secrets sync clients' });
}
return attributionLegend;
}
get sortedAttribution() {
if (this.args.attribution) {
// shallow copy so it doesn't mutate the data during tests
return this.args.attribution?.slice().sort((a, b) => b.clients - a.clients);
}
return [];
}
// truncate data before sending to chart component
get topTenAttribution() {
return this.sortedAttribution.slice(0, 10);
}
get topAttribution() {
// get top namespace or mount
return this.sortedAttribution[0] ?? null;
}
get chartText() {
if (this.noun === 'namespace') {
return {
subtext: 'This data shows the top ten namespaces by total clients for the date range selected.',
description:
'This data shows the top ten namespaces by total clients and can be used to understand where clients are originating. Namespaces are identified by path.',
};
} else {
return {
subtext:
'The total clients used by the mounts for this date range. This number is useful for identifying overall usage volume.',
description:
'This data shows the top ten mounts by client count within this namespace, and can be used to understand where clients are originating. Mounts are organized by path.',
};
}
}
}

View file

@ -18,6 +18,7 @@ interface OnChangeParams {
start_time: number | undefined;
end_time: number | undefined;
}
interface Args {
onChange: (callback: OnChangeParams) => void;
setEditModalVisible: (visible: boolean) => void;

View file

@ -14,16 +14,21 @@
/>
{{#if this.hasAttributionData}}
<Clients::Attribution
@noun="namespace"
@attribution={{@activity.byNamespace}}
@isSecretsSyncActivated={{this.flags.secretsSyncIsActivated}}
/>
{{#if this.namespaceMountAttribution}}
<Clients::Attribution
@noun="mount"
@attribution={{this.namespaceMountAttribution}}
@isSecretsSyncActivated={{this.flags.secretsSyncIsActivated}}
/>
{{/if}}
<Hds::Form::Select::Base
class="has-bottom-margin-m"
aria-label="Month"
name="month"
{{on "input" this.selectMonth}}
@width="200px"
data-test-select="attribution-month"
as |S|
>
<S.Options>
<option value="">Select month</option>
{{#each this.months as |m|}}
<option value={{m.value}} selected={{eq m.value this.selectedMonth}}>{{m.display}}</option>
{{/each}}
</S.Options>
</Hds::Form::Select::Base>
<Clients::Table @data={{this.tableData}} />
{{/if}}

View file

@ -5,23 +5,40 @@
import ActivityComponent from '../activity';
import { service } from '@ember/service';
import { sanitizePath } from 'core/utils/sanitize-path';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { HTMLElementEvent } from 'vault/forms';
import { parseAPITimestamp } from 'core/utils/date-formatters';
import { formatTableData, TableData } from 'core/utils/client-count-utils';
import type FlagsService from 'vault/services/flags';
import type RouterService from '@ember/routing/router-service';
export default class ClientsOverviewPageComponent extends ActivityComponent {
@service declare readonly flags: FlagsService;
@service('app-router') declare readonly router: RouterService;
@tracked selectedMonth = '';
get hasAttributionData() {
// we hide attribution data when mountPath filter present
// we hide attribution table when mountPath filter present
// or if there's no data
if (this.args.mountPath || !this.totalUsageCounts.clients) return false;
return true;
return !this.args.mountPath && this.totalUsageCounts.clients;
}
// mounts attribution
get namespaceMountAttribution() {
const { activity } = this.args;
const nsLabel = this.namespacePathForFilter;
return activity?.byNamespace?.find((ns) => sanitizePath(ns.label) === nsLabel)?.mounts || [];
get months() {
return this.byMonthNewClients.map((m) => ({
display: parseAPITimestamp(m.timestamp, 'MMMM yyyy'),
value: m.month,
}));
}
get tableData(): TableData[] | undefined {
if (!this.selectedMonth) return undefined;
return formatTableData(this.byMonthNewClients, this.selectedMonth);
}
@action
selectMonth(e: HTMLElementEvent<HTMLInputElement>) {
this.selectedMonth = e.target.value;
}
}

View file

@ -0,0 +1,59 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
{{#if @data}}
<Hds::Table
@model={{this.paginatedTableData}}
@columns={{array
(hash key="namespace" label="Namespace" isSortable=true)
(hash key="label" label="Mount path" isSortable=true)
(hash key="mount_type" label="Mount type" isSortable=true)
(hash key="clients" label="Counts" isSortable=true)
}}
@sortBy="clients"
@sortOrder="desc"
@onSort={{this.updateSort}}
data-test-clients-attribution-table
>
<:body as |B|>
<B.Tr data-test-attribution-table-row>
<B.Td>{{B.data.namespace}}</B.Td>
<B.Td>{{B.data.label}}</B.Td>
{{#if (eq B.data.mount_type "deleted mount")}}
<B.Td><Hds::Badge @text="Deleted" @type="outlined" /></B.Td>
{{else}}
<B.Td>{{B.data.mount_type}}</B.Td>
{{/if}}
<B.Td data-test-attribution-table-counts={{B.rowIndex}}>{{B.data.clients}}</B.Td>
</B.Tr>
</:body>
</Hds::Table>
<Hds::Pagination::Numbered
@showSizeSelector={{false}}
@totalItems={{@data.length}}
@currentPage={{this.currentPage}}
@pageSize={{this.pageSize}}
@currentPageSize={{this.pageSize}}
@onPageChange={{this.onPageChange}}
data-test-pagination
class="has-top-margin-m"
/>
{{else}}
<Hds::Card::Container @level="mid" @hasBorder={{true}} class="has-padding-l" data-test-card="attribution">
<Hds::ApplicationState as |A|>
<A.Header @title={{this.tableHeaderMessage}} />
<A.Body @text={{this.tableBodyMessage}} />
<A.Footer as |F|>
<F.LinkStandalone
@icon="file-text"
@text="Client count documentation"
@href="https://developer.hashicorp.com/vault/docs/concepts/client-count"
@iconPosition="trailing"
/>
</A.Footer>
</Hds::ApplicationState>
</Hds::Card::Container>
{{/if}}

View file

@ -0,0 +1,77 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import Ember from 'ember';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { paginate } from 'core/utils/paginate-list';
import { MountClients } from 'core/utils/client-count-utils';
interface TableData extends MountClients {
namespace: string;
}
interface Args {
data: TableData[];
}
type TableColumn = 'namespace' | 'label' | 'mount_type' | 'clients';
type SortDirection = 'asc' | 'desc';
export default class Table extends Component<Args> {
@tracked currentPage = 1;
@tracked sortColumn: TableColumn = 'clients';
@tracked sortDirection: SortDirection = 'desc';
pageSize = Ember.testing ? 3 : 10; // lower in tests to test pagination without seeding more data
get paginatedTableData(): TableData[] {
const sorted = this.sortTableData(this.args.data);
const paginated = paginate(sorted, {
page: this.currentPage,
pageSize: this.pageSize,
});
return paginated;
}
get tableHeaderMessage(): string {
return this.args.data
? 'No data is available for the selected month'
: 'Select a month to view client attribution';
}
get tableBodyMessage(): string {
return this.args.data
? 'View the namespace mount breakdown of clients by selecting another month.'
: 'View the namespace mount breakdown of clients by selecting a month.';
}
sortTableData(data: TableData[]): TableData[] {
if (this.sortColumn) {
data = [...data].sort((a, b) => {
const valA = a[this.sortColumn];
const valB = b[this.sortColumn];
if (valA < valB) return this.sortDirection === 'asc' ? -1 : 1;
if (valA > valB) return this.sortDirection === 'asc' ? 1 : -1;
return 0;
});
}
return data;
}
@action
onPageChange(page: number) {
this.currentPage = page;
}
@action
updateSort(column: TableColumn, direction: SortDirection) {
this.sortColumn = column;
this.sortDirection = direction;
}
}

View file

@ -205,7 +205,11 @@ export const formatByNamespace = (namespaceArray: NamespaceObject[] | null): ByN
// transform to an empty array for type consistency
let mounts: MountClients[] | [] = [];
if (Array.isArray(ns.mounts)) {
mounts = ns.mounts.map((m) => ({ label: m.mount_path, ...destructureClientCounts(m.counts) }));
mounts = ns.mounts.map((m) => ({
label: m.mount_path,
mount_type: m.mount_type,
...destructureClientCounts(m.counts),
}));
}
return {
label,
@ -215,6 +219,23 @@ export const formatByNamespace = (namespaceArray: NamespaceObject[] | null): ByN
});
};
export const formatTableData = (byMonthNewClients: ByMonthNewClients[], month: string): TableData[] => {
const monthData = byMonthNewClients.find((m) => m.month === month);
const namespaces = monthData?.namespaces;
let data: TableData[] = [];
// iterate over namespaces to add "namespace" to each mount object
namespaces?.forEach((n) => {
const mounts: TableData[] = n.mounts.map((m) => {
// add namespace to mount block
return { ...m, namespace: n.label };
});
data = [...data, ...mounts];
});
return data;
};
// This method returns only client types from the passed object, excluding other keys such as "label".
// when querying historical data the response will always contain the latest client type keys because the activity log is
// constructed based on the version of Vault the user is on (key values will be 0)
@ -281,6 +302,7 @@ export interface ByNamespaceClients extends TotalClients {
export interface MountClients extends TotalClients {
label: string;
mount_type: string;
}
export interface ByMonthClients extends TotalClients {
@ -322,13 +344,17 @@ export interface MountNewClients extends TotalClientsSometimes {
label: string;
}
export interface TableData extends MountClients {
namespace: string;
}
// API RESPONSE SHAPE (prior to serialization)
export interface NamespaceObject {
namespace_id: string;
namespace_path: string;
counts: Counts;
mounts: { mount_path: string; counts: Counts }[];
mounts: { mount_path: string; counts: Counts; mount_type: string }[];
}
type ActivityMonthStandard = {

View file

@ -130,7 +130,7 @@ module('Acceptance | clients | counts', function (hooks) {
counts: getCounts(),
mounts: [
{
mount_path: 'auth/authid/0',
mount_path: 'auth/userpass-0',
counts: getCounts(),
},
],
@ -145,7 +145,7 @@ module('Acceptance | clients | counts', function (hooks) {
counts: getCounts(),
mounts: [
{
mount_path: 'auth/authid/0',
mount_path: 'auth/userpass-0',
counts: getCounts(),
},
],

View file

@ -131,7 +131,7 @@ module('Acceptance | clients | counts | acme', function (hooks) {
assert.expect(3);
await visit('/vault/clients/counts/acme');
await selectChoose(CLIENT_COUNT.nsFilter, this.nsPath);
await selectChoose(CLIENT_COUNT.mountFilter, 'auth/authid/0');
await selectChoose(CLIENT_COUNT.mountFilter, 'auth/userpass-0');
// no data because this is an auth mount (acme_clients come from pki mounts)
assert.dom(CLIENT_COUNT.statText('Total ACME clients')).hasTextContaining('0');
assert.dom(`${CHARTS.chart('ACME usage')} ${CHARTS.verticalBar}`).isNotVisible();

View file

@ -58,7 +58,6 @@ module('Acceptance | clients | overview', function (hooks) {
assert
.dom(CLIENT_COUNT.dateRange.dateDisplay('end'))
.hasText('January 2024', 'billing start month is correctly parsed from license');
assert.dom(CLIENT_COUNT.attributionBlock()).exists({ count: 2 });
assert
.dom(CHARTS.container('Vault client counts'))
.exists('Shows running totals with monthly breakdown charts');
@ -87,9 +86,6 @@ module('Acceptance | clients | overview', function (hooks) {
assert
.dom(CHARTS.container('Vault client counts'))
.doesNotExist('running total month over month charts do not show');
assert.dom(CLIENT_COUNT.attributionBlock()).exists({ count: 2 });
assert.dom(CHARTS.container('namespace')).exists('namespace attribution chart shows');
assert.dom(CHARTS.container('mount')).exists('mount attribution chart shows');
// change to start on month/year of upgrade to 1.10
await click(CLIENT_COUNT.dateRange.edit);
@ -99,7 +95,6 @@ module('Acceptance | clients | overview', function (hooks) {
assert
.dom(CLIENT_COUNT.dateRange.dateDisplay('start'))
.hasText('September 2023', 'billing start month is correctly parsed from license');
assert.dom(CLIENT_COUNT.attributionBlock()).exists({ count: 2 });
assert
.dom(CHARTS.container('Vault client counts'))
.exists('Shows running totals with monthly breakdown charts');
@ -120,9 +115,6 @@ module('Acceptance | clients | overview', function (hooks) {
assert
.dom(CHARTS.container('Vault client counts'))
.doesNotExist('running total month over month charts do not show');
assert.dom(CLIENT_COUNT.attributionBlock()).exists({ count: 2 });
assert.dom(CHARTS.container('namespace')).exists('namespace attribution chart shows');
assert.dom(CHARTS.container('mount')).exists('mount attribution chart shows');
// query historical date range (from September 2023 to December 2023)
await click(CLIENT_COUNT.dateRange.edit);
@ -136,7 +128,6 @@ module('Acceptance | clients | overview', function (hooks) {
assert
.dom(CLIENT_COUNT.dateRange.dateDisplay('end'))
.hasText('December 2023', 'billing start month is correctly parsed from license');
assert.dom(CLIENT_COUNT.attributionBlock()).exists({ count: 2 });
assert
.dom(CHARTS.container('Vault client counts'))
.exists('Shows running totals with monthly breakdown charts');
@ -163,7 +154,6 @@ module('Acceptance | clients | overview', function (hooks) {
assert
.dom(CHARTS.container('Vault client counts'))
.exists('Shows running totals with monthly breakdown charts');
assert.dom(CLIENT_COUNT.attributionBlock()).exists({ count: 2 });
const response = await this.store.peekRecord('clients/activity', 'some-activity-id');
const orderedNs = response.byNamespace.sort((a, b) => b.clients - a.clients);
@ -172,23 +162,9 @@ module('Acceptance | clients | overview', function (hooks) {
const filterNamespace = topNamespace.label === 'root' ? orderedNs[1] : topNamespace;
const topMount = filterNamespace?.mounts.sort((a, b) => b.clients - a.clients)[0];
assert
.dom(`${CLIENT_COUNT.attributionBlock('namespace')} [data-test-top-attribution]`)
.includesText(`Top namespace ${topNamespace.label}`);
// this math works because there are no nested namespaces in the mirage data
assert
.dom(`${CLIENT_COUNT.attributionBlock('namespace')} [data-test-attribution-clients] p`)
.includesText(`${formatNumber([topNamespace.clients])}`, 'top attribution clients accurate');
// Filter by top namespace
await selectChoose(CLIENT_COUNT.nsFilter, filterNamespace.label);
assert.dom(CLIENT_COUNT.selectedNs).hasText(filterNamespace.label, 'selects top namespace');
assert
.dom(`${CLIENT_COUNT.attributionBlock('mount')} [data-test-top-attribution]`)
.includesText('Top mount');
assert
.dom(`${CLIENT_COUNT.attributionBlock('mount')} [data-test-attribution-clients] p`)
.includesText(`${formatNumber([topMount.clients])}`, 'top attribution clients accurate');
let expectedStats = {
Entity: formatNumber([filterNamespace.entity_clients]),
@ -205,7 +181,6 @@ module('Acceptance | clients | overview', function (hooks) {
// FILTER BY AUTH METHOD
await selectChoose(CLIENT_COUNT.mountFilter, topMount.label);
assert.dom(CLIENT_COUNT.selectedAuthMount).hasText(topMount.label, 'selects top mount');
assert.dom(CLIENT_COUNT.attributionBlock()).doesNotExist('Does not show attribution block');
expectedStats = {
Entity: formatNumber([topMount.entity_clients]),
@ -222,13 +197,6 @@ module('Acceptance | clients | overview', function (hooks) {
// Remove namespace filter without first removing auth method filter
await click(GENERAL.searchSelect.removeSelected);
assert.strictEqual(currentURL(), '/vault/clients/counts/overview', 'removes both query params');
assert.dom('[data-test-top-attribution]').includesText('Top namespace');
assert
.dom('[data-test-attribution-clients]')
.hasTextContaining(
`${formatNumber([topNamespace.clients])}`,
'top attribution clients back to unfiltered value'
);
expectedStats = {
Entity: formatNumber([response.total.entity_clients]),

View file

@ -45,7 +45,8 @@ export const ACTIVITY_RESPONSE_STUB = {
},
mounts: [
{
mount_path: 'auth/authid/0',
mount_path: 'auth/userpass-0',
mount_type: 'userpass',
counts: {
acme_clients: 0,
clients: 8394,
@ -56,6 +57,7 @@ export const ACTIVITY_RESPONSE_STUB = {
},
{
mount_path: 'kvv2-engine-0',
mount_type: 'kv',
counts: {
acme_clients: 0,
clients: 4810,
@ -66,6 +68,7 @@ export const ACTIVITY_RESPONSE_STUB = {
},
{
mount_path: 'pki-engine-0',
mount_type: 'pki',
counts: {
acme_clients: 5699,
clients: 5699,
@ -88,7 +91,8 @@ export const ACTIVITY_RESPONSE_STUB = {
},
mounts: [
{
mount_path: 'auth/authid/0',
mount_path: 'auth/userpass-0',
mount_type: 'userpass',
counts: {
acme_clients: 0,
clients: 8091,
@ -99,6 +103,7 @@ export const ACTIVITY_RESPONSE_STUB = {
},
{
mount_path: 'kvv2-engine-0',
mount_type: 'kv',
counts: {
acme_clients: 0,
clients: 4290,
@ -109,6 +114,7 @@ export const ACTIVITY_RESPONSE_STUB = {
},
{
mount_path: 'pki-engine-0',
mount_type: 'pki',
counts: {
acme_clients: 4003,
clients: 4003,
@ -150,6 +156,7 @@ export const ACTIVITY_RESPONSE_STUB = {
mounts: [
{
mount_path: 'pki-engine-0',
mount_type: 'pki',
counts: {
acme_clients: 100,
clients: 100,
@ -159,7 +166,8 @@ export const ACTIVITY_RESPONSE_STUB = {
},
},
{
mount_path: 'auth/authid/0',
mount_path: 'auth/userpass-0',
mount_type: 'userpass',
counts: {
acme_clients: 0,
clients: 200,
@ -170,6 +178,7 @@ export const ACTIVITY_RESPONSE_STUB = {
},
{
mount_path: 'kvv2-engine-0',
mount_type: 'kv',
counts: {
acme_clients: 0,
clients: 100,
@ -203,6 +212,7 @@ export const ACTIVITY_RESPONSE_STUB = {
mounts: [
{
mount_path: 'pki-engine-0',
mount_type: 'pki',
counts: {
acme_clients: 100,
clients: 100,
@ -212,7 +222,8 @@ export const ACTIVITY_RESPONSE_STUB = {
},
},
{
mount_path: 'auth/authid/0',
mount_path: 'auth/userpass-0',
mount_type: 'userpass',
counts: {
acme_clients: 0,
clients: 200,
@ -223,6 +234,7 @@ export const ACTIVITY_RESPONSE_STUB = {
},
{
mount_path: 'kvv2-engine-0',
mount_type: 'kv',
counts: {
acme_clients: 0,
clients: 100,
@ -259,6 +271,7 @@ export const ACTIVITY_RESPONSE_STUB = {
mounts: [
{
mount_path: 'pki-engine-0',
mount_type: 'pki',
counts: {
acme_clients: 100,
clients: 100,
@ -268,7 +281,8 @@ export const ACTIVITY_RESPONSE_STUB = {
},
},
{
mount_path: 'auth/authid/0',
mount_path: 'auth/userpass-0',
mount_type: 'userpass',
counts: {
acme_clients: 0,
clients: 200,
@ -279,6 +293,7 @@ export const ACTIVITY_RESPONSE_STUB = {
},
{
mount_path: 'kvv2-engine-0',
mount_type: 'kv',
counts: {
acme_clients: 0,
clients: 100,
@ -327,7 +342,7 @@ export const ACTIVITY_RESPONSE_STUB = {
},
},
{
mount_path: 'auth/authid/0',
mount_path: 'auth/userpass-0',
counts: {
acme_clients: 0,
clients: 890,
@ -370,7 +385,7 @@ export const ACTIVITY_RESPONSE_STUB = {
},
},
{
mount_path: 'auth/authid/0',
mount_path: 'auth/userpass-0',
counts: {
acme_clients: 0,
clients: 872,
@ -423,7 +438,7 @@ export const ACTIVITY_RESPONSE_STUB = {
},
},
{
mount_path: 'auth/authid/0',
mount_path: 'auth/userpass-0',
counts: {
acme_clients: 0,
clients: 75,
@ -456,7 +471,7 @@ export const ACTIVITY_RESPONSE_STUB = {
},
mounts: [
{
mount_path: 'auth/authid/0',
mount_path: 'auth/userpass-0',
counts: {
acme_clients: 0,
clients: 96,
@ -530,6 +545,7 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = {
secret_syncs: 0,
},
mount_path: 'no mount accessor (pre-1.10 upgrade?)',
mount_type: 'no mount path (pre-1.10 upgrade?)',
},
{
counts: {
@ -539,7 +555,8 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = {
non_entity_clients: 0,
secret_syncs: 0,
},
mount_path: 'auth/u/',
mount_path: 'auth/userpass-0',
mount_type: 'userpass',
},
],
namespace_id: 'root',
@ -580,6 +597,7 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = {
secret_syncs: 0,
},
mount_path: 'no mount accessor (pre-1.10 upgrade?)',
mount_type: 'no mount path (pre-1.10 upgrade?)',
},
{
counts: {
@ -589,7 +607,8 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = {
non_entity_clients: 0,
secret_syncs: 0,
},
mount_path: 'auth/u/',
mount_path: 'auth/userpass-0',
mount_type: 'userpass',
},
],
namespace_id: 'root',
@ -623,6 +642,7 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = {
secret_syncs: 0,
},
mount_path: 'no mount accessor (pre-1.10 upgrade?)',
mount_type: 'no mount path (pre-1.10 upgrade?)',
},
{
counts: {
@ -632,7 +652,8 @@ export const MIXED_ACTIVITY_RESPONSE_STUB = {
non_entity_clients: 0,
secret_syncs: 0,
},
mount_path: 'auth/u/',
mount_path: 'auth/userpass-0',
mount_type: 'userpass',
},
],
namespace_id: 'root',
@ -664,7 +685,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
secret_syncs: 4810,
mounts: [
{
label: 'auth/authid/0',
label: 'auth/userpass-0',
mount_type: 'userpass',
acme_clients: 0,
clients: 8394,
entity_clients: 4256,
@ -673,6 +695,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
},
{
label: 'kvv2-engine-0',
mount_type: 'kv',
acme_clients: 0,
clients: 4810,
entity_clients: 0,
@ -681,6 +704,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
},
{
label: 'pki-engine-0',
mount_type: 'pki',
acme_clients: 5699,
clients: 5699,
entity_clients: 0,
@ -698,7 +722,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
secret_syncs: 4290,
mounts: [
{
label: 'auth/authid/0',
label: 'auth/userpass-0',
mount_type: 'userpass',
acme_clients: 0,
clients: 8091,
entity_clients: 4002,
@ -707,6 +732,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
},
{
label: 'kvv2-engine-0',
mount_type: 'kv',
acme_clients: 0,
clients: 4290,
entity_clients: 0,
@ -715,6 +741,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
},
{
label: 'pki-engine-0',
mount_type: 'pki',
acme_clients: 4003,
clients: 4003,
entity_clients: 0,
@ -754,6 +781,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
mounts: [
{
label: 'pki-engine-0',
mount_type: 'pki',
acme_clients: 100,
clients: 100,
entity_clients: 0,
@ -761,7 +789,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
secret_syncs: 0,
},
{
label: 'auth/authid/0',
label: 'auth/userpass-0',
mount_type: 'userpass',
acme_clients: 0,
clients: 200,
entity_clients: 100,
@ -770,6 +799,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
},
{
label: 'kvv2-engine-0',
mount_type: 'kv',
acme_clients: 0,
clients: 100,
entity_clients: 0,
@ -798,6 +828,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
mounts: [
{
label: 'pki-engine-0',
mount_type: 'pki',
acme_clients: 100,
clients: 100,
entity_clients: 0,
@ -805,7 +836,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
secret_syncs: 0,
},
{
label: 'auth/authid/0',
label: 'auth/userpass-0',
mount_type: 'userpass',
acme_clients: 0,
clients: 200,
entity_clients: 100,
@ -814,6 +846,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
},
{
label: 'kvv2-engine-0',
mount_type: 'kv',
acme_clients: 0,
clients: 100,
entity_clients: 0,
@ -844,6 +877,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
mounts: [
{
label: 'pki-engine-0',
mount_type: 'pki',
acme_clients: 100,
clients: 100,
entity_clients: 0,
@ -851,7 +885,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
secret_syncs: 0,
},
{
label: 'auth/authid/0',
label: 'auth/userpass-0',
mount_type: 'userpass',
acme_clients: 0,
clients: 200,
entity_clients: 100,
@ -860,6 +895,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
},
{
label: 'kvv2-engine-0',
mount_type: 'kv',
acme_clients: 0,
clients: 100,
entity_clients: 0,
@ -901,7 +937,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
secret_syncs: 0,
},
{
label: 'auth/authid/0',
label: 'auth/userpass-0',
acme_clients: 0,
clients: 890,
entity_clients: 708,
@ -935,7 +971,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
secret_syncs: 0,
},
{
label: 'auth/authid/0',
label: 'auth/userpass-0',
acme_clients: 0,
clients: 872,
entity_clients: 124,
@ -972,6 +1008,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
mounts: [
{
label: 'pki-engine-0',
mount_type: 'pki',
acme_clients: 91,
clients: 91,
entity_clients: 0,
@ -979,7 +1016,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
secret_syncs: 0,
},
{
label: 'auth/authid/0',
label: 'auth/userpass-0',
mount_type: 'userpass',
acme_clients: 0,
clients: 75,
entity_clients: 25,
@ -988,6 +1026,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
},
{
label: 'kvv2-engine-0',
mount_type: 'kv',
acme_clients: 0,
clients: 25,
entity_clients: 0,
@ -1005,7 +1044,8 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
secret_syncs: 24,
mounts: [
{
label: 'auth/authid/0',
label: 'auth/userpass-0',
mount_type: 'userpass',
acme_clients: 0,
clients: 96,
entity_clients: 34,
@ -1014,6 +1054,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
},
{
label: 'pki-engine-0',
mount_type: 'pki',
acme_clients: 53,
clients: 53,
entity_clients: 0,
@ -1022,6 +1063,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
},
{
label: 'kvv2-engine-0',
mount_type: 'kv',
acme_clients: 0,
clients: 24,
entity_clients: 0,

View file

@ -26,8 +26,14 @@ export const CLIENT_COUNT = {
statTextValue: (label: string) =>
label ? `[data-test-stat-text="${label}"] .stat-value` : '[data-test-stat-text]',
usageStats: (title: string) => `[data-test-usage-stats="${title}"]`,
attributionBlock: (type: string) =>
type ? `[data-test-clients-attribution="${type}"]` : '[data-test-clients-attribution]',
attribution: {
card: '[data-test-card="attribution"]',
table: '[data-test-clients-attribution-table]',
row: '[data-test-attribution-table-row',
counts: (index: number) => `[data-test-attribution-table-counts="${index}"]`,
pagination: '[data-test-pagination',
paginationInfo: '.hds-pagination-info',
},
filterBar: '[data-test-clients-filter-bar]',
nsFilter: '#namespace-search-select',
mountFilter: '#mounts-search-select',

View file

@ -1,181 +0,0 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import sinon from 'sinon';
import { setupRenderingTest } from 'ember-qunit';
import { render } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { formatRFC3339 } from 'date-fns';
import subMonths from 'date-fns/subMonths';
import timestamp from 'core/utils/timestamp';
import { SERIALIZED_ACTIVITY_RESPONSE } from 'vault/tests/helpers/clients/client-count-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
import { CLIENT_TYPES } from 'core/utils/client-count-utils';
const CLIENTS_ATTRIBUTION = {
title: '[data-test-attribution-title]',
description: '[data-test-attribution-description]',
subtext: '[data-test-attribution-subtext]',
timestamp: '[data-test-attribution-timestamp]',
chart: '[data-test-horizontal-bar-chart]',
topItem: '[data-test-top-attribution]',
topItemCount: '[data-test-attribution-clients]',
yLabel: '[data-test-group="y-labels"]',
yLabels: '[data-test-group="y-labels"] text',
};
module('Integration | Component | clients/attribution', function (hooks) {
setupRenderingTest(hooks);
setupMirage(hooks);
hooks.before(function () {
this.timestampStub = sinon.replace(timestamp, 'now', sinon.fake.returns(new Date('2018-04-03T14:15:30')));
});
hooks.beforeEach(function () {
const mockNow = this.timestampStub();
this.mockNow = mockNow;
this.startTimestamp = formatRFC3339(subMonths(mockNow, 6));
this.selectedNamespace = null;
this.namespaceAttribution = SERIALIZED_ACTIVITY_RESPONSE.by_namespace;
this.authMountAttribution = SERIALIZED_ACTIVITY_RESPONSE.by_namespace.find(
(ns) => ns.label === 'ns1'
).mounts;
});
test('it renders empty state with no data', async function (assert) {
await render(hbs`
<Clients::Attribution />
`);
assert.dom(GENERAL.emptyStateTitle).hasText('No data found');
assert.dom(CLIENTS_ATTRIBUTION.title).hasText('Namespace attribution', 'uses default noun');
});
test('it updates language based on noun', async function (assert) {
this.noun = '';
await render(hbs`
<Clients::Attribution
@noun={{this.noun}}
@attribution={{this.namespaceAttribution}}
/>
`);
// when noun is blank, uses default
assert.dom(CLIENTS_ATTRIBUTION.title).hasText('Namespace attribution');
assert
.dom(CLIENTS_ATTRIBUTION.description)
.hasText(
'This data shows the top ten namespaces by total clients and can be used to understand where clients are originating. Namespaces are identified by path.'
);
assert
.dom(CLIENTS_ATTRIBUTION.subtext)
.hasText('This data shows the top ten namespaces by total clients for the date range selected.');
// when noun is mount
this.set('noun', 'mount');
assert.dom(CLIENTS_ATTRIBUTION.title).hasText('Mount attribution');
assert
.dom(CLIENTS_ATTRIBUTION.description)
.hasText(
'This data shows the top ten mounts by client count within this namespace, and can be used to understand where clients are originating. Mounts are organized by path.'
);
assert
.dom(CLIENTS_ATTRIBUTION.subtext)
.hasText(
'The total clients used by the mounts for this date range. This number is useful for identifying overall usage volume.'
);
// when noun is namespace
this.set('noun', 'namespace');
assert.dom(CLIENTS_ATTRIBUTION.title).hasText('Namespace attribution');
assert
.dom(CLIENTS_ATTRIBUTION.description)
.hasText(
'This data shows the top ten namespaces by total clients and can be used to understand where clients are originating. Namespaces are identified by path.'
);
assert
.dom(CLIENTS_ATTRIBUTION.subtext)
.hasText('This data shows the top ten namespaces by total clients for the date range selected.');
});
test('it renders with data for namespaces', async function (assert) {
await render(hbs`
<Clients::Attribution
@attribution={{this.namespaceAttribution}}
/>
`);
assert.dom(GENERAL.emptyStateTitle).doesNotExist();
assert.dom(CLIENTS_ATTRIBUTION.chart).exists();
assert.dom(CLIENTS_ATTRIBUTION.topItem).includesText('namespace').includesText('ns1');
assert.dom(CLIENTS_ATTRIBUTION.topItemCount).includesText('namespace').includesText('18,903');
assert
.dom(CLIENTS_ATTRIBUTION.yLabels)
.exists({ count: 2 }, 'bars reflect number of namespaces in single month');
assert.dom(CLIENTS_ATTRIBUTION.yLabel).hasText('ns1root');
});
test('it renders with data for mounts', async function (assert) {
await render(hbs`
<Clients::Attribution
@noun="mount"
@attribution={{this.authMountAttribution}}
/>
`);
assert.dom(GENERAL.emptyStateTitle).doesNotExist();
assert.dom(CLIENTS_ATTRIBUTION.chart).exists();
assert.dom(CLIENTS_ATTRIBUTION.topItem).includesText('mount').includesText('auth/authid/0');
assert.dom(CLIENTS_ATTRIBUTION.topItemCount).includesText('mount').includesText('8,394');
assert
.dom(CLIENTS_ATTRIBUTION.yLabels)
.exists({ count: 3 }, 'bars reflect number of mounts in single month');
assert.dom(CLIENTS_ATTRIBUTION.yLabel).hasText('auth/authid/0pki-engine-0kvv2-engine-0');
});
test('it shows secret syncs when flag is on', async function (assert) {
this.isSecretsSyncActivated = true;
await render(hbs`
<Clients::Attribution
@attribution={{this.namespaceAttribution}}
@isSecretsSyncActivated={{true}}
/>
`);
assert.dom('[data-test-group="secret_syncs"] rect').exists({ count: 2 });
});
test('it hids secret syncs when flag is off or missing', async function (assert) {
this.isSecretsSyncActivated = true;
await render(hbs`
<Clients::Attribution
@attribution={{this.namespaceAttribution}}
/>
`);
assert.dom('[data-test-group="secret_syncs"]').doesNotExist();
});
test('it sorts and limits before rendering bars', async function (assert) {
this.tooManyAttributions = Array(15)
.fill(null)
.map((_, idx) => {
const attr = { label: `ns${idx}` };
CLIENT_TYPES.forEach((type) => {
attr[type] = 10 + idx;
});
return attr;
});
await render(hbs`
<Clients::Attribution
@attribution={{this.tooManyAttributions}}
/>
`);
assert.dom(CLIENTS_ATTRIBUTION.yLabels).exists({ count: 10 }, 'only 10 bars are shown');
assert.dom(CLIENTS_ATTRIBUTION.topItem).includesText('ns14');
});
});

View file

@ -5,13 +5,14 @@
import { module, test } from 'qunit';
import { setupRenderingTest } from 'vault/tests/helpers';
import { render } from '@ember/test-helpers';
import { click, fillIn, findAll, render, triggerEvent } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupMirage } from 'ember-cli-mirage/test-support';
import { setRunOptions } from 'ember-a11y-testing/test-support';
import { ACTIVITY_RESPONSE_STUB } from 'vault/tests/helpers/clients/client-count-helpers';
import { filterActivityResponse } from 'vault/mirage/handlers/clients';
import { CHARTS, CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors';
import { GENERAL } from 'vault/tests/helpers/general-selectors';
module('Integration | Component | clients/page/overview', function (hooks) {
setupRenderingTest(hooks);
@ -48,71 +49,118 @@ module('Integration | Component | clients/page/overview', function (hooks) {
this.mountPath = '';
this.namespace = '';
this.versionHistory = '';
this.activity = await this.store.queryRecord('clients/activity', {});
// Fails on #ember-testing-container
setRunOptions({
rules: {
'scrollable-region-focusable': { enabled: false },
'aria-prohibited-attr': { enabled: false },
},
});
});
test('it hides attribution data when mount filter applied', async function (assert) {
this.mountPath = '';
test('it shows empty state message upon initial load', async function (assert) {
await render(hbs`<Clients::Page::Overview @activity={{this.activity}}/>`);
assert.dom(GENERAL.selectByAttr('attribution-month')).exists('shows month selection dropdown');
assert.dom(CLIENT_COUNT.attribution.card).exists('shows card for table state');
assert
.dom(CLIENT_COUNT.attribution.card)
.hasText(
'Select a month to view client attribution View the namespace mount breakdown of clients by selecting a month. Client count documentation',
'Show initial table state message'
);
});
test('it shows correct state message when month selection has no data', async function (assert) {
await render(hbs`<Clients::Page::Overview @activity={{this.activity}} />`);
assert.dom(GENERAL.selectByAttr('attribution-month')).exists('shows month selection dropdown');
await fillIn(GENERAL.selectByAttr('attribution-month'), '6/23');
assert
.dom(CLIENT_COUNT.attribution.card)
.hasText(
'No data is available for the selected month View the namespace mount breakdown of clients by selecting another month. Client count documentation',
'Shows correct message for a month selection with no data'
);
});
test('it shows table when month selection has data', async function (assert) {
await render(hbs`<Clients::Page::Overview @activity={{this.activity}} />`);
assert.dom(GENERAL.selectByAttr('attribution-month')).exists('shows month selection dropdown');
await fillIn(GENERAL.selectByAttr('attribution-month'), '9/23');
assert.dom(CLIENT_COUNT.attribution.card).doesNotExist('does not show card when table has data');
assert.dom(CLIENT_COUNT.attribution.table).exists('shows table');
assert.dom(CLIENT_COUNT.attribution.paginationInfo).hasText('13 of 6', 'shows correct pagination info');
});
test('it filters the table when a namespace filter is applied', async function (assert) {
this.namespace = 'ns1';
this.activity = await this.store.queryRecord('clients/activity', {
namespace: 'ns1',
namespace: this.namespace,
});
await render(hbs`<Clients::Page::Overview @activity={{this.activity}} @namespace={{this.namespace}} />`);
await fillIn(GENERAL.selectByAttr('attribution-month'), '9/23');
assert.dom(CLIENT_COUNT.attribution.card).doesNotExist('does not show card when table has data');
assert.dom(CLIENT_COUNT.attribution.table).exists();
assert.dom(CLIENT_COUNT.attribution.paginationInfo).hasText('13 of 3', 'shows correct pagination info');
});
test('it hides the table when a mount filter is applied', async function (assert) {
this.namespace = 'ns1';
this.mountPath = 'auth/userpass-0';
this.activity = await this.store.queryRecord('clients/activity', {
namespace: this.namespace,
mountPath: this.mountPath,
});
await render(
hbs`<Clients::Page::Overview @activity={{this.activity}} @namespace="ns1" @mountPath={{this.mountPath}} />`
hbs`<Clients::Page::Overview @activity={{this.activity}} @namespace={{this.namespace}} @mountPath={{this.mountPath}}/>`
);
assert.dom(CHARTS.container('Vault client counts')).exists('shows running totals');
assert.dom(CLIENT_COUNT.attributionBlock('namespace')).exists();
assert.dom(CLIENT_COUNT.attributionBlock('mount')).exists();
this.set('mountPath', 'auth/authid/0');
assert.dom(CHARTS.container('Vault client counts')).exists('shows running totals');
assert.dom(CLIENT_COUNT.attributionBlock('namespace')).doesNotExist();
assert.dom(CLIENT_COUNT.attributionBlock('mount')).doesNotExist();
assert.dom(CLIENT_COUNT.attribution.card).doesNotExist('does not show card when table has data');
assert
.dom(CLIENT_COUNT.attribution.table)
.doesNotExist('does not show table when a mount filter is applied');
});
test('it hides attribution data when no data returned', async function (assert) {
this.mountPath = '';
this.activity = await this.store.queryRecord('clients/activity', {
namespace: 'no-data',
});
await render(hbs`<Clients::Page::Overview @activity={{this.activity}} />`);
assert.dom(CLIENT_COUNT.usageStats('Total usage')).exists();
assert.dom(CHARTS.container('Vault client counts')).doesNotExist('usage stats instead of running totals');
assert.dom(CLIENT_COUNT.attributionBlock('namespace')).doesNotExist();
assert.dom(CLIENT_COUNT.attributionBlock('mount')).doesNotExist();
test('it paginates table data', async function (assert) {
await render(hbs`<Clients::Page::Overview @activity={{this.activity}} />`);
await fillIn(GENERAL.selectByAttr('attribution-month'), '9/23');
assert
.dom(CLIENT_COUNT.attribution.row)
.exists({ count: 3 }, 'Correct number of table rows render based on page size');
assert.dom(CLIENT_COUNT.attribution.counts(0)).hasText('96', 'First page shows data');
assert.dom(CLIENT_COUNT.attribution.pagination).exists('shows pagination');
assert.dom(CLIENT_COUNT.attribution.paginationInfo).hasText('13 of 6', 'shows correct pagination info');
await click(GENERAL.pagination.next);
assert.dom(CLIENT_COUNT.attribution.counts(0)).hasText('53', 'Second page shows new data');
assert.dom(CLIENT_COUNT.attribution.paginationInfo).hasText('46 of 6', 'shows correct pagination info');
});
test('it shows the correct mount attributions', async function (assert) {
this.nsService = this.owner.lookup('service:namespace');
const rootActivity = await this.store.queryRecord('clients/activity', {});
this.activity = rootActivity;
test('it shows correct month options for billing period', async function (assert) {
await render(hbs`<Clients::Page::Overview @activity={{this.activity}} />`);
// start at "root" namespace
let expectedMounts = rootActivity.byNamespace.find((ns) => ns.label === 'root').mounts;
assert
.dom(`${CLIENT_COUNT.attributionBlock('mount')} [data-test-group="y-labels"] text`)
.exists({ count: expectedMounts.length });
assert
.dom(`${CLIENT_COUNT.attributionBlock('mount')} [data-test-group="y-labels"]`)
.includesText(expectedMounts[0].label);
// now pretend we're querying within a child namespace
this.nsService.path = 'ns1';
this.activity = await this.store.queryRecord('clients/activity', {
namespace: 'ns1',
});
expectedMounts = rootActivity.byNamespace.find((ns) => ns.label === 'ns1').mounts;
assert
.dom(`${CLIENT_COUNT.attributionBlock('mount')} [data-test-group="y-labels"] text`)
.exists({ count: expectedMounts.length });
assert
.dom(`${CLIENT_COUNT.attributionBlock('mount')} [data-test-group="y-labels"]`)
.includesText(expectedMounts[0].label);
assert.dom(GENERAL.selectByAttr('attribution-month')).exists('shows month selection dropdown');
await fillIn(GENERAL.selectByAttr('attribution-month'), '');
await triggerEvent(GENERAL.selectByAttr('attribution-month'), 'change');
// assert that months options in select are those of selected billing period
const expectedMonths = this.activity.byMonth.map((m) => m.month);
// '' represents default state of 'Select month'
const expectedOptions = ['', ...expectedMonths];
const actualOptions = findAll(`${GENERAL.selectByAttr('attribution-month')} option`).map(
(option) => option.value
);
assert.deepEqual(actualOptions, expectedOptions, 'All <option> values match expected list');
});
});

View file

@ -217,6 +217,7 @@ module('Integration | Util | client count utils', function (hooks) {
clients: 30,
},
mount_path: 'no mount accessor (pre-1.10 upgrade?)',
mount_type: '',
},
],
},
@ -233,6 +234,7 @@ module('Integration | Util | client count utils', function (hooks) {
clients: 30,
entity_clients: 10,
label: 'no mount accessor (pre-1.10 upgrade?)',
mount_type: '',
non_entity_clients: 20,
secret_syncs: 0,
},
@ -264,6 +266,7 @@ module('Integration | Util | client count utils', function (hooks) {
clients: 2,
entity_clients: 2,
label: 'no mount accessor (pre-1.10 upgrade?)',
mount_type: 'no mount path (pre-1.10 upgrade?)',
non_entity_clients: 0,
secret_syncs: 0,
},
@ -271,7 +274,8 @@ module('Integration | Util | client count utils', function (hooks) {
acme_clients: 0,
clients: 1,
entity_clients: 1,
label: 'auth/u/',
label: 'auth/userpass-0',
mount_type: 'userpass',
non_entity_clients: 0,
secret_syncs: 0,
},
@ -301,6 +305,7 @@ module('Integration | Util | client count utils', function (hooks) {
clients: 2,
entity_clients: 2,
label: 'no mount accessor (pre-1.10 upgrade?)',
mount_type: 'no mount path (pre-1.10 upgrade?)',
non_entity_clients: 0,
secret_syncs: 0,
},
@ -308,7 +313,8 @@ module('Integration | Util | client count utils', function (hooks) {
acme_clients: 0,
clients: 1,
entity_clients: 1,
label: 'auth/u/',
label: 'auth/userpass-0',
mount_type: 'userpass',
non_entity_clients: 0,
secret_syncs: 0,
},
@ -502,7 +508,7 @@ module('Integration | Util | client count utils', function (hooks) {
when: 'no namespace filter passed',
result: 'it returns empty counts',
ns: '',
mount: 'auth/authid/0',
mount: 'auth/userpass-0',
expected: emptyCounts,
},
{
@ -516,16 +522,17 @@ module('Integration | Util | client count utils', function (hooks) {
when: 'no matching ns/mount exists',
result: 'it returns empty counts',
ns: 'ns1',
mount: 'auth/authid/1',
mount: 'auth/userpass-1',
expected: emptyCounts,
},
{
when: 'mount and label have extra slashes',
result: 'it returns the data sanitized',
ns: 'ns1/',
mount: 'auth/authid/0/',
mount: 'auth/userpass-0',
expected: {
label: 'auth/authid/0',
label: 'auth/userpass-0',
mount_type: 'userpass',
acme_clients: 0,
clients: 8394,
entity_clients: 4256,
@ -540,6 +547,7 @@ module('Integration | Util | client count utils', function (hooks) {
mount: 'kvv2-engine-0',
expected: {
label: 'kvv2-engine-0',
mount_type: 'kv',
acme_clients: 0,
clients: 4290,
entity_clients: 0,