diff --git a/changelog/30678.txt b/changelog/30678.txt
new file mode 100644
index 0000000000..35763ca4e3
--- /dev/null
+++ b/changelog/30678.txt
@@ -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.
+```
diff --git a/ui/app/components/clients/attribution.hbs b/ui/app/components/clients/attribution.hbs
deleted file mode 100644
index 10abd62946..0000000000
--- a/ui/app/components/clients/attribution.hbs
+++ /dev/null
@@ -1,39 +0,0 @@
-{{!
- Copyright (c) HashiCorp, Inc.
- SPDX-License-Identifier: BUSL-1.1
-}}
-
-
-
- {{#if @attribution}}
-
-
-
-
-
{{this.chartText.subtext}}
-
-
-
-
Top {{this.noun}}
-
{{this.topAttribution.label}}
-
-
-
-
Clients in {{this.noun}}
-
{{format-number this.topAttribution.clients}}
-
-
-
- {{#each this.attributionLegend as |legend idx|}}
- {{capitalize legend.label}}
- {{/each}}
-
- {{else}}
-
-
-
- {{/if}}
-
\ No newline at end of file
diff --git a/ui/app/components/clients/attribution.js b/ui/app/components/clients/attribution.js
deleted file mode 100644
index d12692ca74..0000000000
--- a/ui/app/components/clients/attribution.js
+++ /dev/null
@@ -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
- *
- *
- * @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.',
- };
- }
- }
-}
diff --git a/ui/app/components/clients/date-range.ts b/ui/app/components/clients/date-range.ts
index b450dbb4a7..31fabee972 100644
--- a/ui/app/components/clients/date-range.ts
+++ b/ui/app/components/clients/date-range.ts
@@ -18,6 +18,7 @@ interface OnChangeParams {
start_time: number | undefined;
end_time: number | undefined;
}
+
interface Args {
onChange: (callback: OnChangeParams) => void;
setEditModalVisible: (visible: boolean) => void;
diff --git a/ui/app/components/clients/page/overview.hbs b/ui/app/components/clients/page/overview.hbs
index 816a02d273..d983768f43 100644
--- a/ui/app/components/clients/page/overview.hbs
+++ b/ui/app/components/clients/page/overview.hbs
@@ -14,16 +14,21 @@
/>
{{#if this.hasAttributionData}}
-
- {{#if this.namespaceMountAttribution}}
-
- {{/if}}
+
+
+
+ {{#each this.months as |m|}}
+
+ {{/each}}
+
+
+
{{/if}}
\ No newline at end of file
diff --git a/ui/app/components/clients/page/overview.ts b/ui/app/components/clients/page/overview.ts
index e4292161e3..33047e6467 100644
--- a/ui/app/components/clients/page/overview.ts
+++ b/ui/app/components/clients/page/overview.ts
@@ -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) {
+ this.selectedMonth = e.target.value;
}
}
diff --git a/ui/app/components/clients/table.hbs b/ui/app/components/clients/table.hbs
new file mode 100644
index 0000000000..a37c90bdc1
--- /dev/null
+++ b/ui/app/components/clients/table.hbs
@@ -0,0 +1,59 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+{{#if @data}}
+
+ <:body as |B|>
+
+ {{B.data.namespace}}
+ {{B.data.label}}
+ {{#if (eq B.data.mount_type "deleted mount")}}
+
+ {{else}}
+ {{B.data.mount_type}}
+ {{/if}}
+ {{B.data.clients}}
+
+
+
+
+
+{{else}}
+
+
+
+
+
+
+
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/app/components/clients/table.ts b/ui/app/components/clients/table.ts
new file mode 100644
index 0000000000..ae020ac0eb
--- /dev/null
+++ b/ui/app/components/clients/table.ts
@@ -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 {
+ @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;
+ }
+}
diff --git a/ui/lib/core/addon/utils/client-count-utils.ts b/ui/lib/core/addon/utils/client-count-utils.ts
index 502a296563..114e3b73f4 100644
--- a/ui/lib/core/addon/utils/client-count-utils.ts
+++ b/ui/lib/core/addon/utils/client-count-utils.ts
@@ -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 = {
diff --git a/ui/tests/acceptance/clients/counts-test.js b/ui/tests/acceptance/clients/counts-test.js
index 3fd21ef05a..2788331e41 100644
--- a/ui/tests/acceptance/clients/counts-test.js
+++ b/ui/tests/acceptance/clients/counts-test.js
@@ -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(),
},
],
diff --git a/ui/tests/acceptance/clients/counts/acme-test.js b/ui/tests/acceptance/clients/counts/acme-test.js
index 8d7de1b9fc..19f84f5ae2 100644
--- a/ui/tests/acceptance/clients/counts/acme-test.js
+++ b/ui/tests/acceptance/clients/counts/acme-test.js
@@ -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();
diff --git a/ui/tests/acceptance/clients/counts/overview-test.js b/ui/tests/acceptance/clients/counts/overview-test.js
index a590b5e9ed..26a8b2ce85 100644
--- a/ui/tests/acceptance/clients/counts/overview-test.js
+++ b/ui/tests/acceptance/clients/counts/overview-test.js
@@ -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]),
diff --git a/ui/tests/helpers/clients/client-count-helpers.js b/ui/tests/helpers/clients/client-count-helpers.js
index 292c5b4274..b43e60b3cf 100644
--- a/ui/tests/helpers/clients/client-count-helpers.js
+++ b/ui/tests/helpers/clients/client-count-helpers.js
@@ -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,
diff --git a/ui/tests/helpers/clients/client-count-selectors.ts b/ui/tests/helpers/clients/client-count-selectors.ts
index 8cdf9affad..c68f95a73e 100644
--- a/ui/tests/helpers/clients/client-count-selectors.ts
+++ b/ui/tests/helpers/clients/client-count-selectors.ts
@@ -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',
diff --git a/ui/tests/integration/components/clients/attribution-test.js b/ui/tests/integration/components/clients/attribution-test.js
deleted file mode 100644
index 98eee690ba..0000000000
--- a/ui/tests/integration/components/clients/attribution-test.js
+++ /dev/null
@@ -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`
-
- `);
-
- 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`
-
- `);
-
- // 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`
-
- `);
-
- 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`
-
- `);
-
- 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`
-
- `);
-
- 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`
-
- `);
-
- 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`
-
- `);
- assert.dom(CLIENTS_ATTRIBUTION.yLabels).exists({ count: 10 }, 'only 10 bars are shown');
- assert.dom(CLIENTS_ATTRIBUTION.topItem).includesText('ns14');
- });
-});
diff --git a/ui/tests/integration/components/clients/page/overview-test.js b/ui/tests/integration/components/clients/page/overview-test.js
index a24e70af55..4de5d576ac 100644
--- a/ui/tests/integration/components/clients/page/overview-test.js
+++ b/ui/tests/integration/components/clients/page/overview-test.js
@@ -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``);
+
+ 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``);
+
+ 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``);
+
+ 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('1–3 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``);
+
+ 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('1–3 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``
+ hbs``
);
-
- 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``);
- 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``);
+
+ 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('1–3 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('4–6 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``);
- // 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