diff --git a/ui/app/components/clients/page/acme.hbs b/ui/app/components/clients/page/acme.hbs index 5f3e3ad2ac..074306d3b7 100644 --- a/ui/app/components/clients/page/acme.hbs +++ b/ui/app/components/clients/page/acme.hbs @@ -3,12 +3,97 @@ SPDX-License-Identifier: BUSL-1.1 ~}} -
-
-

ACME usage

-

This data can be used to understand how many ACME clients have been - used for the queried - {{if this.isDateRange "date range" "month"}}. Each ACME request is counted as one client.

+{{! ACME clients added in 1.17 }} + +{{#if (not this.byMonthActivityData)}} + {{! byMonthActivityData is an empty array if there is no monthly data (monthly breakdown was added in 1.11) + this means the user has queried dates before ACME clients existed. we render an empty state instead of + "0 acme clients" (which is what the activity response returns) to be more explicit }} + +{{else if this.isDateRange}} + + <:subTitle> + + + + <:stats> + {{#let (this.average this.byMonthActivityData "acme_clients") as |avg|}} + {{! 0 is falsy, intentionally hide 0 averages }} + {{#if avg}} + + {{/if}} + {{/let}} + + + <:chart> + + + + + {{#if this.totalUsageCounts.acme_clients}} + {{! no need to render two empty charts! hide this one if there are no acme clients }} + + <:stats> + {{#let (this.average this.byMonthNewClients "acme_clients") as |avg|}} + {{#if avg}} + + {{/if}} + {{/let}} + + + <:chart> + + + + {{/if}} +{{else}} +
+
+

{{this.title}}

+

{{this.description}}

+
+
- -
\ No newline at end of file +{{/if}} \ No newline at end of file diff --git a/ui/app/components/clients/page/acme.ts b/ui/app/components/clients/page/acme.ts index 5d3a3b434c..9b4cb8fd0a 100644 --- a/ui/app/components/clients/page/acme.ts +++ b/ui/app/components/clients/page/acme.ts @@ -5,4 +5,11 @@ import ActivityComponent from '../activity'; -export default class ClientsAcmePageComponent extends ActivityComponent {} +export default class ClientsAcmePageComponent extends ActivityComponent { + title = 'ACME usage'; + get description() { + return `This data can be used to understand how many ACME clients have been used for the queried ${ + this.isDateRange ? 'date range' : 'month' + }. Each ACME request is counted as one client.`; + } +} diff --git a/ui/app/components/clients/page/counts.hbs b/ui/app/components/clients/page/counts.hbs index bd62c6cc09..779441db5f 100644 --- a/ui/app/components/clients/page/counts.hbs +++ b/ui/app/components/clients/page/counts.hbs @@ -92,7 +92,7 @@ {{/if}} {{#if (or @mountPath this.mountPaths)}} @@ -57,7 +55,6 @@ @value={{this.average this.byMonthNewClients "entity_clients"}} @size="m" class="chart-subTitle has-top-padding-l" - data-test-chart-stat="entity" /> diff --git a/ui/app/components/clients/usage-stats.hbs b/ui/app/components/clients/usage-stats.hbs index 3bd8eb6089..e45e17c3a8 100644 --- a/ui/app/components/clients/usage-stats.hbs +++ b/ui/app/components/clients/usage-stats.hbs @@ -24,7 +24,6 @@ @value={{@totalUsageCounts.clients}} @size="l" @subText="The number of clients which interacted with Vault during this month. This is Vault’s primary billing metric." - data-test-stat-text="total-clients" />
@@ -34,7 +33,6 @@ @value={{@totalUsageCounts.entity_clients}} @size="l" @subText="Representations of a particular user, client, or application that created a token via login." - data-test-stat-text="entity-clients" />
@@ -44,7 +42,6 @@ @value={{@totalUsageCounts.non_entity_clients}} @size="l" @subText="Clients created with a shared set of permissions, but not associated with an entity." - data-test-stat-text="non-entity-clients" />
{{#if @showSecretSyncs}} @@ -55,7 +52,6 @@ @value={{@totalUsageCounts.secret_syncs}} @size="l" @subText="A secret with a configured sync destination qualifies as a unique and active client." - data-test-stat-text="secret-syncs" /> {{/if}} diff --git a/ui/app/controllers/vault/cluster/clients/counts/acme.ts b/ui/app/controllers/vault/cluster/clients/counts/acme.ts new file mode 100644 index 0000000000..e176bf4c76 --- /dev/null +++ b/ui/app/controllers/vault/cluster/clients/counts/acme.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Controller, { inject as controller } from '@ember/controller'; +import type ClientsCountsController from '../counts'; + +export default class ClientsCountsAcmeController extends Controller { + // not sure why this needs to be cast to never but this definitely accepts a string to point to the controller + @controller('vault.cluster.clients.counts' as never) + declare readonly countsController: ClientsCountsController; +} diff --git a/ui/lib/core/addon/components/stat-text.hbs b/ui/lib/core/addon/components/stat-text.hbs index 63ea9c6d17..65da05776c 100644 --- a/ui/lib/core/addon/components/stat-text.hbs +++ b/ui/lib/core/addon/components/stat-text.hbs @@ -5,7 +5,7 @@
{{@label}}
diff --git a/ui/lib/core/addon/utils/client-count-utils.ts b/ui/lib/core/addon/utils/client-count-utils.ts index fe5dc06908..0dabba2c50 100644 --- a/ui/lib/core/addon/utils/client-count-utils.ts +++ b/ui/lib/core/addon/utils/client-count-utils.ts @@ -167,7 +167,7 @@ export const namespaceArrayToObject = ( ...mount, timestamp, month, - new_clients: { month, ...newMountClients }, + new_clients: { month, timestamp, ...newMountClients }, }; } return mountObj; @@ -177,7 +177,7 @@ export const namespaceArrayToObject = ( ...destructureClientCounts(ns), timestamp, month, - new_clients: { month, ...newNsClients }, + new_clients: { month, timestamp, ...newNsClients }, mounts_by_key, }; } @@ -241,6 +241,7 @@ export interface NamespaceByKey extends TotalClients { export interface NamespaceNewClients extends TotalClients { month: string; + timestamp: string; label: string; mounts: MountClients[]; } @@ -254,6 +255,7 @@ export interface MountByKey extends TotalClients { export interface MountNewClients extends TotalClients { month: string; + timestamp: string; label: string; } diff --git a/ui/mirage/handlers/clients.js b/ui/mirage/handlers/clients.js index 2e9d27e982..fb26db6ee0 100644 --- a/ui/mirage/handlers/clients.js +++ b/ui/mirage/handlers/clients.js @@ -72,16 +72,18 @@ function generateMounts(pathPrefix, counts) { }); } -function generateNamespaceBlock(idx = 0, isLowerCounts = false, ns) { +function generateNamespaceBlock(idx = 0, isLowerCounts = false, ns, skipCounts = false) { const min = isLowerCounts ? 10 : 50; - const max = isLowerCounts ? 100 : 5000; + const max = isLowerCounts ? 100 : 1000; const nsBlock = { namespace_id: ns?.namespace_id || (idx === 0 ? 'root' : Math.random().toString(36).slice(2, 7) + idx), - namespace_path: ns?.namespace_path || (idx === 0 ? '' : `ns/${idx}`), + namespace_path: ns?.namespace_path || (idx === 0 ? '' : `ns${idx}`), counts: {}, mounts: {}, }; + if (skipCounts) return nsBlock; // skip counts to generate empty ns block with namespace ids and paths + // * ADD NEW CLIENT TYPES HERE and pass to a new generateMounts() function below const [acme_clients, entity_clients, non_entity_clients, secret_syncs] = CLIENT_TYPES.map(() => randomBetween(min, max) @@ -144,12 +146,34 @@ function generateMonths(startDate, endDate, namespaces) { return months; } -function generateActivityResponse(namespaces, startDate, endDate) { +function generateActivityResponse(startDate, endDate) { + let namespaces = Array.from(Array(12)).map((v, idx) => generateNamespaceBlock(idx, null, null, true)); + const months = generateMonths(startDate, endDate, namespaces); + if (months.length) { + const monthlyCounts = months.filter((m) => m.counts); + // sum namespace counts from monthly data + namespaces.forEach((ns) => { + const nsData = monthlyCounts.map((d) => + d.namespaces.find((n) => n.namespace_path === ns.namespace_path) + ); + const mountCounts = nsData.flatMap((d) => d.mounts); + const paths = nsData[0].mounts.map(({ mount_path }) => mount_path); + ns.mounts = paths.map((path) => { + const counts = getTotalCounts(mountCounts.filter((m) => m.mount_path === path)); + return { mount_path: path, counts }; + }); + ns.counts = getTotalCounts(nsData); + }); + } else { + // if no monthly data due to upgrade stuff, generate counts + namespaces = Array.from(Array(12)).map((v, idx) => generateNamespaceBlock(idx)); + } + namespaces.sort((a, b) => b.counts.clients - a.counts.clients); return { start_time: isAfter(new Date(startDate), COUNTS_START) ? startDate : formatRFC3339(COUNTS_START), end_time: endDate, - by_namespace: namespaces.sort((a, b) => b.counts.clients - a.counts.clients), - months: generateMonths(startDate, endDate, namespaces), + by_namespace: namespaces, + months, total: getTotalCounts(namespaces), }; } @@ -186,13 +210,12 @@ export default function (server) { // backend returns a timestamp if given unix time, so first convert to timestamp string here if (!start_time.includes('T')) start_time = fromUnixTime(start_time).toISOString(); if (!end_time.includes('T')) end_time = fromUnixTime(end_time).toISOString(); - const namespaces = Array.from(Array(12)).map((v, idx) => generateNamespaceBlock(idx)); return { request_id: 'some-activity-id', lease_id: '', renewable: false, lease_duration: 0, - data: generateActivityResponse(namespaces, start_time, end_time), + data: generateActivityResponse(start_time, end_time), wrap_info: null, warnings: null, auth: null, diff --git a/ui/tests/acceptance/clients/counts/acme-test.js b/ui/tests/acceptance/clients/counts/acme-test.js new file mode 100644 index 0000000000..24a15044f4 --- /dev/null +++ b/ui/tests/acceptance/clients/counts/acme-test.js @@ -0,0 +1,152 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupApplicationTest } from 'ember-qunit'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { visit, click, currentURL } from '@ember/test-helpers'; +import { getUnixTime } from 'date-fns'; +import sinon from 'sinon'; +import timestamp from 'core/utils/timestamp'; +import authPage from 'vault/tests/pages/auth'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors'; +import { ACTIVITY_RESPONSE_STUB, assertChart } from 'vault/tests/helpers/clients/client-count-helpers'; +import { formatNumber } from 'core/helpers/format-number'; +import { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients'; + +const { searchSelect } = GENERAL; + +// integration test handle general display assertions, acceptance handles nav + filtering +module('Acceptance | clients | counts | acme', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + hooks.before(function () { + sinon.stub(timestamp, 'now').callsFake(() => STATIC_NOW); + }); + + hooks.beforeEach(async function () { + this.server.get('sys/internal/counters/activity', () => { + return { + request_id: 'some-activity-id', + data: ACTIVITY_RESPONSE_STUB, + }; + }); + // store serialized activity data for value comparison + const { byMonth, byNamespace } = await this.owner + .lookup('service:store') + .queryRecord('clients/activity', { + start_time: { timestamp: getUnixTime(LICENSE_START) }, + end_time: { timestamp: getUnixTime(STATIC_NOW) }, + }); + this.nsPath = 'ns1'; + this.mountPath = 'pki-engine-0'; + this.expectedValues = { + nsTotals: byNamespace.find((ns) => ns.label === this.nsPath), + nsMonthlyUsage: byMonth.map((m) => m?.namespaces_by_key[this.nsPath]).filter((d) => !!d), + nsMonthActivity: byMonth.find(({ month }) => month === '9/23').namespaces_by_key[this.nsPath], + }; + + await authPage.login(); + return visit('/vault'); + }); + + hooks.after(function () { + timestamp.now.restore(); + }); + + test('it navigates to acme tab', async function (assert) { + assert.expect(3); + await click(GENERAL.navLink('Client Count')); + await click(GENERAL.tab('acme')); + assert.strictEqual(currentURL(), '/vault/clients/counts/acme', 'it navigates to acme tab'); + assert.dom(GENERAL.tab('acme')).hasClass('active'); + await click(GENERAL.navLink('Back to main navigation')); + assert.strictEqual(currentURL(), '/vault/dashboard', 'it navigates back to dashboard'); + }); + + test('it filters by namespace data and renders charts', async function (assert) { + const { nsTotals, nsMonthlyUsage, nsMonthActivity } = this.expectedValues; + const nsMonthlyNew = nsMonthlyUsage.map((m) => m?.new_clients); + assert.expect(7 + nsMonthlyUsage.length + nsMonthlyNew.length); + + await visit('/vault/clients/counts/acme'); + await click(searchSelect.trigger('namespace-search-select')); + await click(searchSelect.option(searchSelect.optionIndex(this.nsPath))); + + // each chart assertion count is data array length + 2 + assertChart(assert, 'ACME usage', nsMonthlyUsage); + assertChart(assert, 'Monthly new', nsMonthlyNew); + assert.strictEqual( + currentURL(), + `/vault/clients/counts/acme?ns=${this.nsPath}`, + 'namespace filter updates URL query param' + ); + assert + .dom(CLIENT_COUNT.statText('Total ACME clients')) + .hasTextContaining( + `${formatNumber([nsTotals.acme_clients])}`, + 'renders total acme clients for namespace' + ); + // there is only one month in the stubbed data, so in this case the average is the same as the total new clients + assert + .dom(CLIENT_COUNT.statText('Average new ACME clients per month')) + .hasTextContaining( + `${formatNumber([nsMonthActivity.new_clients.acme_clients])}`, + 'renders average acme clients for namespace' + ); + }); + + test('it filters by mount data and renders charts', async function (assert) { + const { nsTotals, nsMonthlyUsage, nsMonthActivity } = this.expectedValues; + const mountTotals = nsTotals.mounts.find((m) => m.label === this.mountPath); + const mountMonthlyUsage = nsMonthlyUsage.map((ns) => ns.mounts_by_key[this.mountPath]).filter((d) => !!d); + const mountMonthlyNew = mountMonthlyUsage.map((m) => m?.new_clients); + assert.expect(7 + mountMonthlyUsage.length + mountMonthlyNew.length); + + await visit('/vault/clients/counts/acme'); + await click(searchSelect.trigger('namespace-search-select')); + await click(searchSelect.option(searchSelect.optionIndex(this.nsPath))); + await click(searchSelect.trigger('mounts-search-select')); + await click(searchSelect.option(searchSelect.optionIndex(this.mountPath))); + + // each chart assertion count is data array length + 2 + assertChart(assert, 'ACME usage', mountMonthlyUsage); + assertChart(assert, 'Monthly new', mountMonthlyNew); + assert.strictEqual( + currentURL(), + `/vault/clients/counts/acme?mountPath=${this.mountPath}&ns=${this.nsPath}`, + 'mount filter updates URL query param' + ); + assert + .dom(CLIENT_COUNT.statText('Total ACME clients')) + .hasTextContaining( + `${formatNumber([mountTotals.acme_clients])}`, + 'renders total acme clients for mount' + ); + // there is only one month in the stubbed data, so in this case the average is the same as the total new clients + const mountMonthActivity = nsMonthActivity.mounts_by_key[this.mountPath]; + assert + .dom(CLIENT_COUNT.statText('Average new ACME clients per month')) + .hasTextContaining( + `${formatNumber([mountMonthActivity.new_clients.acme_clients])}`, + 'renders average acme clients for mount' + ); + }); + + test('it renders empty chart for no mount data ', async function (assert) { + assert.expect(3); + await visit('/vault/clients/counts/acme'); + await click(searchSelect.trigger('namespace-search-select')); + await click(searchSelect.option(searchSelect.optionIndex(this.nsPath))); + await click(searchSelect.trigger('mounts-search-select')); + // no data because this is an auth mount (acme_clients come from pki mounts) + await click(searchSelect.option(searchSelect.optionIndex('auth/authid/0'))); + assert.dom(CLIENT_COUNT.statText('Total ACME clients')).hasTextContaining('0'); + assert.dom(`${CLIENT_COUNT.charts.chart('ACME usage')} ${CLIENT_COUNT.charts.dataBar}`).isNotVisible(); + assert.dom(CLIENT_COUNT.charts.chart('Monthly new')).doesNotExist(); + }); +}); diff --git a/ui/tests/acceptance/clients/counts/sync-test.js b/ui/tests/acceptance/clients/counts/sync-test.js index 87bfe27258..3b7c79b04e 100644 --- a/ui/tests/acceptance/clients/counts/sync-test.js +++ b/ui/tests/acceptance/clients/counts/sync-test.js @@ -35,11 +35,10 @@ module('Acceptance | clients | sync | activated', function (hooks) { test('it should render charts when secrets sync is activated', async function (assert) { syncHandler(this.server); - assert .dom(CLIENT_COUNT.charts.chart('Secrets sync usage')) .exists('Secrets sync usage chart is rendered'); - assert.dom(CLIENT_COUNT.syncTab.total).exists('Total sync clients chart is rendered'); + assert.dom(CLIENT_COUNT.statText('Total sync clients')).exists('Total sync clients chart is rendered'); assert.dom(GENERAL.emptyStateTitle).doesNotExist(); }); }); diff --git a/ui/tests/helpers/clients/client-count-helpers.js b/ui/tests/helpers/clients/client-count-helpers.js index 4cab376307..392785cf65 100644 --- a/ui/tests/helpers/clients/client-count-helpers.js +++ b/ui/tests/helpers/clients/client-count-helpers.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import { click } from '@ember/test-helpers'; +import { click, findAll } from '@ember/test-helpers'; import { LICENSE_START } from 'vault/mirage/handlers/clients'; import { addMonths } from 'date-fns'; @@ -19,108 +19,131 @@ export async function dateDropdownSelect(month, year) { await click(dateDropdown.submit); } +export function assertChart(assert, chartName, byMonthData) { + // assertion count is byMonthData.length + 2 + const chart = CLIENT_COUNT.charts.chart(chartName); + const dataBars = findAll(`${chart} ${CLIENT_COUNT.charts.dataBar}`).filter((b) => b.hasAttribute('height')); + const xAxisLabels = findAll(`${chart} ${CLIENT_COUNT.charts.xAxisLabel}`); + + assert.strictEqual( + dataBars.length, + byMonthData.filter((m) => m.clients).length, + `${chartName}: it renders bars for each non-zero month` + ); + + assert.strictEqual( + xAxisLabels.length, + byMonthData.length, + `${chartName}: it renders a label for each month` + ); + + xAxisLabels.forEach((e, i) => { + assert.dom(e).hasText(`${byMonthData[i].month}`, `renders x-axis label: ${byMonthData[i].month}`); + }); +} + export const ACTIVITY_RESPONSE_STUB = { start_time: '2023-08-01T00:00:00Z', end_time: '2023-09-30T23:59:59Z', // is always the last day and hour of the month queried by_namespace: [ { - namespace_id: 'root', - namespace_path: '', + namespace_id: 'e67m31', + namespace_path: 'ns1', counts: { - distinct_entities: 1033, - entity_clients: 1033, - non_entity_tokens: 1924, - non_entity_clients: 1924, - secret_syncs: 2397, - acme_clients: 75, - clients: 5429, + acme_clients: 5699, + clients: 18903, + entity_clients: 4256, + non_entity_clients: 4138, + secret_syncs: 4810, + distinct_entities: 4256, + non_entity_tokens: 4138, }, mounts: [ { - mount_path: 'auth/authid0', + mount_path: 'auth/authid/0', counts: { - clients: 2957, - entity_clients: 1033, - non_entity_clients: 1924, - distinct_entities: 1033, - non_entity_tokens: 1924, - secret_syncs: 0, acme_clients: 0, + clients: 8394, + entity_clients: 4256, + non_entity_clients: 4138, + secret_syncs: 0, + distinct_entities: 4256, + non_entity_tokens: 4138, }, }, { mount_path: 'kvv2-engine-0', counts: { - clients: 2397, + acme_clients: 0, + clients: 4810, entity_clients: 0, non_entity_clients: 0, + secret_syncs: 4810, distinct_entities: 0, non_entity_tokens: 0, - secret_syncs: 2397, - acme_clients: 0, }, }, { mount_path: 'pki-engine-0', counts: { - clients: 75, + acme_clients: 5699, + clients: 5699, entity_clients: 0, non_entity_clients: 0, + secret_syncs: 0, distinct_entities: 0, non_entity_tokens: 0, - secret_syncs: 0, - acme_clients: 75, }, }, ], }, { - namespace_id: '81ry61', - namespace_path: 'ns1', + namespace_id: 'root', + namespace_path: '', counts: { - distinct_entities: 783, - entity_clients: 783, - non_entity_tokens: 1193, - non_entity_clients: 1193, - secret_syncs: 275, - acme_clients: 125, - clients: 2376, + acme_clients: 4003, + clients: 16384, + entity_clients: 4002, + non_entity_clients: 4089, + secret_syncs: 4290, + distinct_entities: 4002, + non_entity_tokens: 4089, }, mounts: [ { - mount_path: 'auth/authid0', + mount_path: 'auth/authid/0', counts: { - clients: 1976, - entity_clients: 783, - non_entity_clients: 1193, - distinct_entities: 783, - non_entity_tokens: 1193, - secret_syncs: 0, acme_clients: 0, + clients: 8091, + entity_clients: 4002, + non_entity_clients: 4089, + secret_syncs: 0, + distinct_entities: 4002, + non_entity_tokens: 4089, }, }, { mount_path: 'kvv2-engine-0', counts: { - clients: 275, + acme_clients: 0, + clients: 4290, entity_clients: 0, non_entity_clients: 0, + secret_syncs: 4290, distinct_entities: 0, non_entity_tokens: 0, - secret_syncs: 275, - acme_clients: 0, }, }, { mount_path: 'pki-engine-0', counts: { - clients: 125, + acme_clients: 4003, + clients: 4003, entity_clients: 0, non_entity_clients: 0, + secret_syncs: 0, distinct_entities: 0, non_entity_tokens: 0, - secret_syncs: 0, - acme_clients: 125, }, }, ], @@ -136,113 +159,113 @@ export const ACTIVITY_RESPONSE_STUB = { { timestamp: '2023-09-01T00:00:00Z', counts: { - distinct_entities: 1329, - entity_clients: 1329, - non_entity_tokens: 1738, - non_entity_clients: 1738, - secret_syncs: 5525, - acme_clients: 200, - clients: 8792, + acme_clients: 1928, + clients: 3928, + entity_clients: 832, + non_entity_clients: 930, + secret_syncs: 238, + distinct_entities: 832, + non_entity_tokens: 930, }, namespaces: [ { - namespace_id: 'root', - namespace_path: '', + namespace_id: 'e67m31', + namespace_path: 'ns1', counts: { - distinct_entities: 1279, - entity_clients: 1279, - non_entity_tokens: 1598, - non_entity_clients: 1598, - secret_syncs: 2755, - acme_clients: 75, - clients: 5707, + acme_clients: 934, + clients: 1981, + entity_clients: 708, + non_entity_clients: 182, + secret_syncs: 157, + distinct_entities: 708, + non_entity_tokens: 182, }, mounts: [ { - mount_path: 'auth/authid0', + mount_path: 'pki-engine-0', counts: { - clients: 2877, - entity_clients: 1279, - non_entity_clients: 1598, - distinct_entities: 1279, - non_entity_tokens: 1598, + acme_clients: 934, + clients: 934, + entity_clients: 0, + non_entity_clients: 0, secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'auth/authid/0', + counts: { acme_clients: 0, + clients: 890, + entity_clients: 708, + non_entity_clients: 182, + secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 0, }, }, { mount_path: 'kvv2-engine-0', counts: { - clients: 2755, - entity_clients: 0, - non_entity_clients: 0, - distinct_entities: 0, - non_entity_tokens: 0, - secret_syncs: 2755, acme_clients: 0, - }, - }, - { - mount_path: 'pki-engine-0', - counts: { - clients: 75, + clients: 157, entity_clients: 0, non_entity_clients: 0, + secret_syncs: 157, distinct_entities: 0, non_entity_tokens: 0, - secret_syncs: 0, - acme_clients: 75, }, }, ], }, { - namespace_id: '81ry61', - namespace_path: 'ns1', + namespace_id: 'root', + namespace_path: '', counts: { - distinct_entities: 50, - entity_clients: 50, - non_entity_tokens: 140, - non_entity_clients: 140, - secret_syncs: 2770, - acme_clients: 125, - clients: 3085, + acme_clients: 994, + clients: 1947, + entity_clients: 124, + non_entity_clients: 748, + secret_syncs: 81, + distinct_entities: 124, + non_entity_tokens: 748, }, mounts: [ - { - mount_path: 'kvv2-engine-0', - counts: { - clients: 2770, - entity_clients: 0, - non_entity_clients: 0, - distinct_entities: 0, - non_entity_tokens: 0, - secret_syncs: 2770, - acme_clients: 0, - }, - }, - { - mount_path: 'auth/authid0', - counts: { - clients: 190, - entity_clients: 50, - non_entity_clients: 140, - distinct_entities: 50, - non_entity_tokens: 140, - secret_syncs: 0, - acme_clients: 0, - }, - }, { mount_path: 'pki-engine-0', counts: { - clients: 125, + acme_clients: 994, + clients: 994, entity_clients: 0, non_entity_clients: 0, + secret_syncs: 0, distinct_entities: 0, non_entity_tokens: 0, + }, + }, + { + mount_path: 'auth/authid/0', + counts: { + acme_clients: 0, + clients: 872, + entity_clients: 124, + non_entity_clients: 748, secret_syncs: 0, - acme_clients: 125, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'kvv2-engine-0', + counts: { + acme_clients: 0, + clients: 81, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 81, + distinct_entities: 0, + non_entity_tokens: 0, }, }, ], @@ -250,113 +273,113 @@ export const ACTIVITY_RESPONSE_STUB = { ], new_clients: { counts: { - distinct_entities: 39, - entity_clients: 39, - non_entity_tokens: 81, - non_entity_clients: 81, - secret_syncs: 166, - acme_clients: 50, - clients: 336, + acme_clients: 144, + clients: 364, + entity_clients: 59, + non_entity_clients: 112, + secret_syncs: 49, + distinct_entities: 59, + non_entity_tokens: 112, }, namespaces: [ { - namespace_id: '81ry61', - namespace_path: 'ns1', + namespace_id: 'root', + namespace_path: '', counts: { - distinct_entities: 30, - entity_clients: 30, - non_entity_tokens: 62, - non_entity_clients: 62, - secret_syncs: 100, - acme_clients: 30, - clients: 222, + acme_clients: 91, + clients: 191, + entity_clients: 25, + non_entity_clients: 50, + secret_syncs: 25, + distinct_entities: 25, + non_entity_tokens: 50, }, mounts: [ - { - mount_path: 'kvv2-engine-0', - counts: { - clients: 100, - entity_clients: 0, - non_entity_clients: 0, - distinct_entities: 0, - non_entity_tokens: 0, - secret_syncs: 100, - acme_clients: 0, - }, - }, - { - mount_path: 'auth/authid0', - counts: { - clients: 92, - entity_clients: 30, - non_entity_clients: 62, - distinct_entities: 30, - non_entity_tokens: 62, - secret_syncs: 0, - acme_clients: 0, - }, - }, { mount_path: 'pki-engine-0', counts: { - clients: 30, + acme_clients: 91, + clients: 91, entity_clients: 0, non_entity_clients: 0, + secret_syncs: 0, distinct_entities: 0, non_entity_tokens: 0, + }, + }, + { + mount_path: 'auth/authid/0', + counts: { + acme_clients: 0, + clients: 75, + entity_clients: 25, + non_entity_clients: 50, secret_syncs: 0, - acme_clients: 30, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'kvv2-engine-0', + counts: { + acme_clients: 0, + clients: 25, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 25, + distinct_entities: 0, + non_entity_tokens: 0, }, }, ], }, { - namespace_id: 'root', - namespace_path: '', + namespace_id: 'e67m31', + namespace_path: 'ns1', counts: { - distinct_entities: 9, - entity_clients: 9, - non_entity_tokens: 19, - non_entity_clients: 19, - secret_syncs: 66, - acme_clients: 20, - clients: 114, + acme_clients: 53, + clients: 173, + entity_clients: 34, + non_entity_clients: 62, + secret_syncs: 24, + distinct_entities: 34, + non_entity_tokens: 62, }, mounts: [ { - mount_path: 'kvv2-engine-0', + mount_path: 'auth/authid/0', counts: { - clients: 66, - entity_clients: 0, - non_entity_clients: 0, + acme_clients: 0, + clients: 96, + entity_clients: 34, + non_entity_clients: 62, + secret_syncs: 0, distinct_entities: 0, non_entity_tokens: 0, - secret_syncs: 66, - acme_clients: 0, - }, - }, - { - mount_path: 'auth/authid0', - counts: { - clients: 28, - entity_clients: 9, - non_entity_clients: 19, - distinct_entities: 9, - non_entity_tokens: 19, - secret_syncs: 0, - acme_clients: 0, }, }, { mount_path: 'pki-engine-0', counts: { - clients: 20, + acme_clients: 53, + clients: 53, entity_clients: 0, non_entity_clients: 0, + secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'kvv2-engine-0', + counts: { + acme_clients: 0, + clients: 24, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 24, distinct_entities: 0, non_entity_tokens: 0, - secret_syncs: 0, - acme_clients: 20, }, }, ], @@ -366,13 +389,13 @@ export const ACTIVITY_RESPONSE_STUB = { }, ], total: { - distinct_entities: 1816, - entity_clients: 1816, - non_entity_tokens: 3117, - non_entity_clients: 3117, - secret_syncs: 2672, - acme_clients: 200, - clients: 7805, + acme_clients: 9702, + clients: 35287, + entity_clients: 8258, + non_entity_clients: 8227, + secret_syncs: 9100, + distinct_entities: 8258, + non_entity_tokens: 8227, }, }; @@ -577,67 +600,67 @@ export const VERSION_HISTORY = [ export const SERIALIZED_ACTIVITY_RESPONSE = { by_namespace: [ { - label: 'root', - clients: 5429, - entity_clients: 1033, - non_entity_clients: 1924, - secret_syncs: 2397, - acme_clients: 75, + label: 'ns1', + acme_clients: 5699, + clients: 18903, + entity_clients: 4256, + non_entity_clients: 4138, + secret_syncs: 4810, mounts: [ { + label: 'auth/authid/0', acme_clients: 0, - clients: 2957, - entity_clients: 1033, - label: 'auth/authid0', - non_entity_clients: 1924, + clients: 8394, + entity_clients: 4256, + non_entity_clients: 4138, secret_syncs: 0, }, { - acme_clients: 0, - clients: 2397, - entity_clients: 0, label: 'kvv2-engine-0', + acme_clients: 0, + clients: 4810, + entity_clients: 0, non_entity_clients: 0, - secret_syncs: 2397, + secret_syncs: 4810, }, { - acme_clients: 75, - clients: 75, - entity_clients: 0, label: 'pki-engine-0', + acme_clients: 5699, + clients: 5699, + entity_clients: 0, non_entity_clients: 0, secret_syncs: 0, }, ], }, { - label: 'ns1', - clients: 2376, - entity_clients: 783, - non_entity_clients: 1193, - secret_syncs: 275, - acme_clients: 125, + label: 'root', + acme_clients: 4003, + clients: 16384, + entity_clients: 4002, + non_entity_clients: 4089, + secret_syncs: 4290, mounts: [ { - label: 'auth/authid0', - clients: 1976, - entity_clients: 783, - non_entity_clients: 1193, - secret_syncs: 0, + label: 'auth/authid/0', acme_clients: 0, + clients: 8091, + entity_clients: 4002, + non_entity_clients: 4089, + secret_syncs: 0, }, { label: 'kvv2-engine-0', - clients: 275, + acme_clients: 0, + clients: 4290, entity_clients: 0, non_entity_clients: 0, - secret_syncs: 275, - acme_clients: 0, + secret_syncs: 4290, }, { label: 'pki-engine-0', - acme_clients: 125, - clients: 125, + acme_clients: 4003, + clients: 4003, entity_clients: 0, non_entity_clients: 0, secret_syncs: 0, @@ -650,372 +673,381 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { month: '8/23', timestamp: '2023-08-01T00:00:00Z', namespaces: [], + namespaces_by_key: {}, new_clients: { month: '8/23', timestamp: '2023-08-01T00:00:00Z', namespaces: [], }, - namespaces_by_key: {}, }, { month: '9/23', timestamp: '2023-09-01T00:00:00Z', - clients: 8592, - entity_clients: 1329, - non_entity_clients: 1738, - secret_syncs: 5525, + acme_clients: 1928, + clients: 3928, + entity_clients: 832, + non_entity_clients: 930, + secret_syncs: 238, namespaces: [ { - label: 'root', - clients: 5707, - entity_clients: 1279, - non_entity_clients: 1598, - secret_syncs: 2755, - acme_clients: 75, + label: 'ns1', + acme_clients: 934, + clients: 1981, + entity_clients: 708, + non_entity_clients: 182, + secret_syncs: 157, mounts: [ { - label: 'auth/authid0', - clients: 2877, - entity_clients: 1279, - non_entity_clients: 1598, + label: 'pki-engine-0', + acme_clients: 934, + clients: 934, + entity_clients: 0, + non_entity_clients: 0, secret_syncs: 0, + }, + { + label: 'auth/authid/0', acme_clients: 0, + clients: 890, + entity_clients: 708, + non_entity_clients: 182, + secret_syncs: 0, }, { label: 'kvv2-engine-0', - clients: 2755, - entity_clients: 0, - non_entity_clients: 0, - secret_syncs: 2755, acme_clients: 0, - }, - { - label: 'pki-engine-0', - acme_clients: 75, - clients: 75, + clients: 157, entity_clients: 0, non_entity_clients: 0, - secret_syncs: 0, + secret_syncs: 157, }, ], }, { - label: 'ns1', - clients: 3085, - entity_clients: 50, - non_entity_clients: 140, - secret_syncs: 2770, - acme_clients: 125, + label: 'root', + acme_clients: 994, + clients: 1947, + entity_clients: 124, + non_entity_clients: 748, + secret_syncs: 81, mounts: [ - { - label: 'kvv2-engine-0', - clients: 2770, - entity_clients: 0, - non_entity_clients: 0, - secret_syncs: 2770, - acme_clients: 0, - }, - { - label: 'auth/authid0', - clients: 190, - entity_clients: 50, - non_entity_clients: 140, - secret_syncs: 0, - acme_clients: 0, - }, { label: 'pki-engine-0', - acme_clients: 125, - clients: 125, + acme_clients: 994, + clients: 994, entity_clients: 0, non_entity_clients: 0, secret_syncs: 0, }, + { + label: 'auth/authid/0', + acme_clients: 0, + clients: 872, + entity_clients: 124, + non_entity_clients: 748, + secret_syncs: 0, + }, + { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 81, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 81, + }, ], }, ], namespaces_by_key: { - root: { - month: '9/23', + ns1: { + acme_clients: 934, + clients: 1981, + entity_clients: 708, + non_entity_clients: 182, + secret_syncs: 157, timestamp: '2023-09-01T00:00:00Z', - clients: 5707, - entity_clients: 1279, - non_entity_clients: 1598, - secret_syncs: 2755, - acme_clients: 75, + month: '9/23', new_clients: { month: '9/23', - label: 'root', - clients: 114, - entity_clients: 9, - non_entity_clients: 19, - secret_syncs: 66, - acme_clients: 20, + timestamp: '2023-09-01T00:00:00Z', + label: 'ns1', + acme_clients: 53, + clients: 173, + entity_clients: 34, + non_entity_clients: 62, + secret_syncs: 24, mounts: [ { - label: 'kvv2-engine-0', - clients: 66, - entity_clients: 0, - non_entity_clients: 0, - secret_syncs: 66, + label: 'auth/authid/0', acme_clients: 0, - }, - { - label: 'auth/authid0', - clients: 28, - entity_clients: 9, - non_entity_clients: 19, + clients: 96, + entity_clients: 34, + non_entity_clients: 62, secret_syncs: 0, - acme_clients: 0, }, { label: 'pki-engine-0', - clients: 20, + acme_clients: 53, + clients: 53, entity_clients: 0, non_entity_clients: 0, secret_syncs: 0, - acme_clients: 20, + }, + { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 24, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 24, }, ], }, mounts_by_key: { - 'auth/authid0': { - month: '9/23', - timestamp: '2023-09-01T00:00:00Z', - label: 'auth/authid0', - clients: 2877, - entity_clients: 1279, - non_entity_clients: 1598, + 'pki-engine-0': { + label: 'pki-engine-0', + acme_clients: 934, + clients: 934, + entity_clients: 0, + non_entity_clients: 0, secret_syncs: 0, - acme_clients: 0, + timestamp: '2023-09-01T00:00:00Z', + month: '9/23', new_clients: { month: '9/23', - label: 'auth/authid0', - clients: 28, - entity_clients: 9, - non_entity_clients: 19, + timestamp: '2023-09-01T00:00:00Z', + label: 'pki-engine-0', + acme_clients: 53, + clients: 53, + entity_clients: 0, + non_entity_clients: 0, secret_syncs: 0, + }, + }, + 'auth/authid/0': { + label: 'auth/authid/0', + acme_clients: 0, + clients: 890, + entity_clients: 708, + non_entity_clients: 182, + secret_syncs: 0, + timestamp: '2023-09-01T00:00:00Z', + month: '9/23', + new_clients: { + month: '9/23', + timestamp: '2023-09-01T00:00:00Z', + label: 'auth/authid/0', acme_clients: 0, + clients: 96, + entity_clients: 34, + non_entity_clients: 62, + secret_syncs: 0, }, }, 'kvv2-engine-0': { - month: '9/23', - timestamp: '2023-09-01T00:00:00Z', label: 'kvv2-engine-0', - clients: 2755, - entity_clients: 0, - non_entity_clients: 0, - secret_syncs: 2755, acme_clients: 0, - new_clients: { - month: '9/23', - label: 'kvv2-engine-0', - clients: 66, - entity_clients: 0, - non_entity_clients: 0, - secret_syncs: 66, - acme_clients: 0, - }, - }, - 'pki-engine-0': { - month: '9/23', - timestamp: '2023-09-01T00:00:00Z', - label: 'pki-engine-0', - clients: 75, + clients: 157, entity_clients: 0, non_entity_clients: 0, - secret_syncs: 0, - acme_clients: 75, + secret_syncs: 157, + timestamp: '2023-09-01T00:00:00Z', + month: '9/23', new_clients: { month: '9/23', - label: 'pki-engine-0', - acme_clients: 20, - clients: 20, + timestamp: '2023-09-01T00:00:00Z', + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 24, entity_clients: 0, non_entity_clients: 0, - secret_syncs: 0, + secret_syncs: 24, }, }, }, }, - ns1: { - month: '9/23', + root: { + acme_clients: 994, + clients: 1947, + entity_clients: 124, + non_entity_clients: 748, + secret_syncs: 81, timestamp: '2023-09-01T00:00:00Z', - clients: 3085, - entity_clients: 50, - non_entity_clients: 140, - secret_syncs: 2770, - acme_clients: 125, + month: '9/23', new_clients: { month: '9/23', - label: 'ns1', - clients: 222, - entity_clients: 30, - non_entity_clients: 62, - secret_syncs: 100, - acme_clients: 30, + timestamp: '2023-09-01T00:00:00Z', + label: 'root', + acme_clients: 91, + clients: 191, + entity_clients: 25, + non_entity_clients: 50, + secret_syncs: 25, mounts: [ - { - label: 'kvv2-engine-0', - clients: 100, - entity_clients: 0, - non_entity_clients: 0, - secret_syncs: 100, - acme_clients: 0, - }, - { - label: 'auth/authid0', - clients: 92, - entity_clients: 30, - non_entity_clients: 62, - secret_syncs: 0, - acme_clients: 0, - }, { label: 'pki-engine-0', - acme_clients: 30, - clients: 30, + acme_clients: 91, + clients: 91, entity_clients: 0, non_entity_clients: 0, secret_syncs: 0, }, + { + label: 'auth/authid/0', + acme_clients: 0, + clients: 75, + entity_clients: 25, + non_entity_clients: 50, + secret_syncs: 0, + }, + { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 25, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 25, + }, ], }, mounts_by_key: { - 'kvv2-engine-0': { - month: '9/23', - timestamp: '2023-09-01T00:00:00Z', - label: 'kvv2-engine-0', - clients: 2770, - entity_clients: 0, - non_entity_clients: 0, - secret_syncs: 2770, - acme_clients: 0, - new_clients: { - month: '9/23', - label: 'kvv2-engine-0', - clients: 100, - entity_clients: 0, - non_entity_clients: 0, - secret_syncs: 100, - acme_clients: 0, - }, - }, - 'auth/authid0': { - month: '9/23', - timestamp: '2023-09-01T00:00:00Z', - label: 'auth/authid0', - clients: 190, - entity_clients: 50, - non_entity_clients: 140, - secret_syncs: 0, - acme_clients: 0, - new_clients: { - month: '9/23', - label: 'auth/authid0', - clients: 92, - entity_clients: 30, - non_entity_clients: 62, - secret_syncs: 0, - acme_clients: 0, - }, - }, 'pki-engine-0': { - month: '9/23', - timestamp: '2023-09-01T00:00:00Z', - clients: 125, - acme_clients: 125, - entity_clients: 0, label: 'pki-engine-0', + acme_clients: 994, + clients: 994, + entity_clients: 0, non_entity_clients: 0, secret_syncs: 0, + timestamp: '2023-09-01T00:00:00Z', + month: '9/23', new_clients: { - acme_clients: 30, - clients: 30, - entity_clients: 0, - label: 'pki-engine-0', month: '9/23', + timestamp: '2023-09-01T00:00:00Z', + label: 'pki-engine-0', + acme_clients: 91, + clients: 91, + entity_clients: 0, non_entity_clients: 0, secret_syncs: 0, }, }, + 'auth/authid/0': { + label: 'auth/authid/0', + acme_clients: 0, + clients: 872, + entity_clients: 124, + non_entity_clients: 748, + secret_syncs: 0, + timestamp: '2023-09-01T00:00:00Z', + month: '9/23', + new_clients: { + month: '9/23', + timestamp: '2023-09-01T00:00:00Z', + label: 'auth/authid/0', + acme_clients: 0, + clients: 75, + entity_clients: 25, + non_entity_clients: 50, + secret_syncs: 0, + }, + }, + 'kvv2-engine-0': { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 81, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 81, + timestamp: '2023-09-01T00:00:00Z', + month: '9/23', + new_clients: { + month: '9/23', + timestamp: '2023-09-01T00:00:00Z', + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 25, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 25, + }, + }, }, }, }, new_clients: { month: '9/23', timestamp: '2023-09-01T00:00:00Z', - clients: 336, - entity_clients: 39, - non_entity_clients: 81, - secret_syncs: 166, - acme_clients: 50, + acme_clients: 144, + clients: 364, + entity_clients: 59, + non_entity_clients: 112, + secret_syncs: 49, namespaces: [ { - label: 'ns1', - clients: 222, - entity_clients: 30, - non_entity_clients: 62, - secret_syncs: 100, - acme_clients: 30, + label: 'root', + acme_clients: 91, + clients: 191, + entity_clients: 25, + non_entity_clients: 50, + secret_syncs: 25, mounts: [ - { - label: 'kvv2-engine-0', - clients: 100, - entity_clients: 0, - non_entity_clients: 0, - secret_syncs: 100, - acme_clients: 0, - }, - { - label: 'auth/authid0', - clients: 92, - entity_clients: 30, - non_entity_clients: 62, - secret_syncs: 0, - acme_clients: 0, - }, { label: 'pki-engine-0', - clients: 30, + acme_clients: 91, + clients: 91, entity_clients: 0, non_entity_clients: 0, secret_syncs: 0, - acme_clients: 30, + }, + { + label: 'auth/authid/0', + acme_clients: 0, + clients: 75, + entity_clients: 25, + non_entity_clients: 50, + secret_syncs: 0, + }, + { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 25, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 25, }, ], }, { - label: 'root', - clients: 114, - entity_clients: 9, - non_entity_clients: 19, - secret_syncs: 66, - acme_clients: 20, + label: 'ns1', + acme_clients: 53, + clients: 173, + entity_clients: 34, + non_entity_clients: 62, + secret_syncs: 24, mounts: [ { - label: 'kvv2-engine-0', - clients: 66, - entity_clients: 0, - non_entity_clients: 0, - secret_syncs: 66, + label: 'auth/authid/0', acme_clients: 0, - }, - { - label: 'auth/authid0', - clients: 28, - entity_clients: 9, - non_entity_clients: 19, + clients: 96, + entity_clients: 34, + non_entity_clients: 62, secret_syncs: 0, - acme_clients: 0, }, { label: 'pki-engine-0', - clients: 20, + acme_clients: 53, + clients: 53, entity_clients: 0, non_entity_clients: 0, secret_syncs: 0, - acme_clients: 20, + }, + { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 24, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 24, }, ], }, diff --git a/ui/tests/helpers/clients/client-count-selectors.ts b/ui/tests/helpers/clients/client-count-selectors.ts index 3cf35befd8..ae76ff680c 100644 --- a/ui/tests/helpers/clients/client-count-selectors.ts +++ b/ui/tests/helpers/clients/client-count-selectors.ts @@ -16,19 +16,11 @@ export const CLIENT_COUNT = { mountPaths: '[data-test-counts-auth-mounts]', startDiscrepancy: '[data-test-counts-start-discrepancy]', }, - tokenTab: { - entity: '[data-test-monthly-new-entity]', - nonentity: '[data-test-monthly-new-nonentity]', - legend: '[data-test-monthly-new-legend]', - }, - syncTab: { - total: '[data-test-total-sync-clients]', - average: '[data-test-average-sync-clients]', - }, + statText: (label: string) => `[data-test-stat-text="${label}"]`, charts: { chart: (title: string) => `[data-test-chart="${title}"]`, // newer lineal charts statTextValue: (label: string) => - label ? `[data-test-stat-text-container="${label}"] .stat-value` : '[data-test-stat-text-container]', + label ? `[data-test-stat-text="${label}"] .stat-value` : '[data-test-stat-text]', legend: '[data-test-chart-container-legend]', legendLabel: (nth: number) => `.legend-label:nth-child(${nth * 2})`, // nth * 2 accounts for dots in legend timestamp: '[data-test-chart-container-timestamp]', @@ -72,7 +64,7 @@ export const CLIENT_COUNT = { }, runningTotalMonthStats: '[data-test-running-total="single-month-stats"]', runningTotalMonthlyCharts: '[data-test-running-total="monthly-charts"]', - selectedAuthMount: 'div#auth-method-search-select [data-test-selected-option] div', + selectedAuthMount: 'div#mounts-search-select [data-test-selected-option] div', selectedNs: 'div#namespace-search-select [data-test-selected-option] div', upgradeWarning: '[data-test-clients-upgrade-warning]', }; diff --git a/ui/tests/helpers/general-selectors.ts b/ui/tests/helpers/general-selectors.ts index a72efdf277..1617ff59be 100644 --- a/ui/tests/helpers/general-selectors.ts +++ b/ui/tests/helpers/general-selectors.ts @@ -48,6 +48,7 @@ export const GENERAL = { deleteRow: (idx = 0) => `[data-test-kv-delete-row="${idx}"]`, }, searchSelect: { + trigger: (id: string) => `[data-test-component="search-select"]#${id} .ember-basic-dropdown-trigger`, options: '.ember-power-select-option', optionIndex: (text: string) => findAll('.ember-power-select-options li').findIndex((e) => e.textContent?.trim() === text), diff --git a/ui/tests/integration/components/clients/page/acme-test.js b/ui/tests/integration/components/clients/page/acme-test.js new file mode 100644 index 0000000000..89ab7c7d68 --- /dev/null +++ b/ui/tests/integration/components/clients/page/acme-test.js @@ -0,0 +1,196 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { render, findAll } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; +import clientsHandler, { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients'; +import { getUnixTime } from 'date-fns'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors'; +import { formatNumber } from 'core/helpers/format-number'; +import { calculateAverage } from 'vault/utils/chart-helpers'; +import { dateFormat } from 'core/helpers/date-format'; +import { assertChart } from 'vault/tests/helpers/clients/client-count-helpers'; + +const START_TIME = getUnixTime(LICENSE_START); +const END_TIME = getUnixTime(STATIC_NOW); +const { statText, charts, usageStats } = CLIENT_COUNT; + +module('Integration | Component | clients | Clients::Page::Acme', function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function () { + clientsHandler(this.server); + this.store = this.owner.lookup('service:store'); + const activityQuery = { + start_time: { timestamp: START_TIME }, + end_time: { timestamp: END_TIME }, + }; + // set this to 0 + this.activity = await this.store.queryRecord('clients/activity', activityQuery); + this.startTimestamp = START_TIME; + this.endTimestamp = END_TIME; + this.isSecretsSyncActivated = true; + + this.renderComponent = () => + render(hbs` + + `); + }); + + test('it should render with full month activity data charts', async function (assert) { + const monthCount = this.activity.byMonth.length; + assert.expect(8 + monthCount * 2); + const expectedTotal = formatNumber([this.activity.total.acme_clients]); + const expectedAvg = formatNumber([calculateAverage(this.activity.byMonth, 'acme_clients')]); + const expectedNewAvg = formatNumber([ + calculateAverage( + this.activity.byMonth.map((m) => m?.new_clients), + 'acme_clients' + ), + ]); + await this.renderComponent(); + assert + .dom(statText('Total ACME clients')) + .hasText( + `Total ACME clients The total number of ACME requests made to Vault during this time period. ${expectedTotal}`, + `renders correct total acme stat ${expectedTotal}` + ); + assert.dom(statText('Average ACME clients per month')).hasTextContaining(`${expectedAvg}`); + assert.dom(statText('Average new ACME clients per month')).hasTextContaining(`${expectedNewAvg}`); + + const formattedTimestamp = dateFormat([this.activity.responseTimestamp, 'MMM d yyyy, h:mm:ss aaa'], { + withTimeZone: true, + }); + assert.dom(charts.timestamp).hasText(`Updated ${formattedTimestamp}`, 'renders response timestamp'); + + assertChart(assert, 'ACME usage', this.activity.byMonth); + assertChart(assert, 'Monthly new', this.activity.byMonth); + }); + + test('it should render stats without chart for a single month', async function (assert) { + assert.expect(5); + const activityQuery = { start_time: { timestamp: END_TIME }, end_time: { timestamp: END_TIME } }; + this.activity = await this.store.queryRecord('clients/activity', activityQuery); + const expectedTotal = formatNumber([this.activity.total.acme_clients]); + await this.renderComponent(); + + assert.dom(charts.chart('ACME usage')).doesNotExist('total usage chart does not render'); + assert.dom(charts.chart('Monthly new')).doesNotExist('monthly new chart does not render'); + assert.dom(statText('Average ACME clients per month')).doesNotExist(); + assert.dom(statText('Average new ACME clients per month')).doesNotExist(); + assert + .dom(usageStats) + .hasText( + `ACME usage This data can be used to understand how many ACME clients have been used for the queried month. Each ACME request is counted as one client. Total ACME clients ${expectedTotal}`, + 'it renders usage stats with single month copy' + ); + }); + + // EMPTY STATES + test('it should render empty state when ACME data does not exist for a date range', async function (assert) { + assert.expect(8); + // this happens when a user queries historical data that predates the monthly breakdown (added in 1.11) + // only entity + non-entity clients existed then, so we show an empty state for ACME clients + // because the activity response just returns { acme_clients: 0 } which isn't very clear + this.activity.byMonth = []; + + await this.renderComponent(); + + assert.dom(GENERAL.emptyStateTitle).hasText('No ACME clients'); + assert + .dom(GENERAL.emptyStateMessage) + .hasText('There is no ACME client data available for this date range.'); + + assert.dom(charts.chart('ACME usage')).doesNotExist('vertical bar chart does not render'); + assert.dom(charts.chart('Monthly new')).doesNotExist('monthly new chart does not render'); + assert.dom(statText('Total ACME clients')).doesNotExist(); + assert.dom(statText('Average ACME clients per month')).doesNotExist(); + assert.dom(statText('Average new ACME clients per month')).doesNotExist(); + assert.dom(usageStats).doesNotExist(); + }); + + test('it should render empty state when ACME data does not exist for a single month', async function (assert) { + assert.expect(1); + const activityQuery = { start_time: { timestamp: START_TIME }, end_time: { timestamp: START_TIME } }; + this.activity = await this.store.queryRecord('clients/activity', activityQuery); + this.activity.byMonth = []; + + await this.renderComponent(); + + assert.dom(GENERAL.emptyStateMessage).hasText('There is no ACME client data available for this month.'); + }); + + test('it should render empty total usage chart when monthly counts are null or 0', async function (assert) { + assert.expect(9); + // manually stub because mirage isn't setup to handle mixed data yet + const counts = { + acme_clients: 0, + clients: 19, + entity_clients: 0, + non_entity_clients: 19, + secret_syncs: 0, + }; + this.activity.byMonth = [ + { + month: '3/24', + timestamp: '2024-03-01T00:00:00Z', + namespaces: [], + namespaces_by_key: {}, + new_clients: { + month: '3/24', + timestamp: '2024-03-01T00:00:00Z', + namespaces: [], + }, + }, + { + month: '4/24', + timestamp: '2024-04-01T00:00:00Z', + ...counts, + namespaces: [], + namespaces_by_key: {}, + new_clients: { + month: '4/24', + timestamp: '2024-04-01T00:00:00Z', + namespaces: [], + }, + }, + ]; + this.activity.total = counts; + + await this.renderComponent(); + + assert.dom(charts.chart('ACME usage')).exists('renders empty ACME usage chart'); + assert + .dom(statText('Total ACME clients')) + .hasTextContaining('The total number of ACME requests made to Vault during this time period. 0'); + findAll(`${charts.chart('ACME usage')} ${charts.xAxisLabel}`).forEach((e, i) => { + assert + .dom(e) + .hasText( + `${this.activity.byMonth[i].month}`, + `renders x-axis labels for empty bar chart: ${this.activity.byMonth[i].month}` + ); + }); + findAll(`${charts.chart('ACME usage')} ${charts.dataBar}`).forEach((e, i) => { + assert.dom(e).isNotVisible(`does not render data bar for: ${this.activity.byMonth[i].month}`); + }); + + assert.dom(charts.chart('Monthly new')).doesNotExist('empty monthly new chart does not render at all'); + assert.dom(statText('Average ACME clients per month')).doesNotExist(); + assert.dom(statText('Average new ACME clients per month')).doesNotExist(); + }); +}); diff --git a/ui/tests/integration/components/clients/page/counts-test.js b/ui/tests/integration/components/clients/page/counts-test.js index d3c06d41a7..6085f3f1e5 100644 --- a/ui/tests/integration/components/clients/page/counts-test.js +++ b/ui/tests/integration/components/clients/page/counts-test.js @@ -183,8 +183,7 @@ module('Integration | Component | clients | Page::Counts', function (hooks) { // in the app the component is rerender on query param change assertion = null; await click(`${CLIENT_COUNT.counts.mountPaths} button`); - - assertion = (params) => assert.true(params.ns.includes('ns/'), 'Namespace value sent on change'); + assertion = (params) => assert.true(params.ns.includes('ns'), 'Namespace value sent on change'); await selectChoose(CLIENT_COUNT.counts.namespaces, '.ember-power-select-option', 0); assertion = (params) => diff --git a/ui/tests/integration/components/clients/page/sync-test.js b/ui/tests/integration/components/clients/page/sync-test.js index b7a326f98e..bf64df84cd 100644 --- a/ui/tests/integration/components/clients/page/sync-test.js +++ b/ui/tests/integration/components/clients/page/sync-test.js @@ -18,7 +18,7 @@ import { dateFormat } from 'core/helpers/date-format'; const START_TIME = getUnixTime(LICENSE_START); const END_TIME = getUnixTime(STATIC_NOW); -const { syncTab, charts, usageStats } = CLIENT_COUNT; +const { statText, charts, usageStats } = CLIENT_COUNT; module('Integration | Component | clients | Clients::Page::Sync', function (hooks) { setupRenderingTest(hooks); @@ -57,13 +57,13 @@ module('Integration | Component | clients | Clients::Page::Sync', function (hook const expectedAvg = formatNumber([calculateAverage(this.activity.byMonth, 'secret_syncs')]); await this.renderComponent(); assert - .dom(syncTab.total) + .dom(statText('Total sync clients')) .hasText( `Total sync clients The total number of secrets synced from Vault to other destinations during this date range. ${expectedTotal}`, `renders correct total sync stat ${expectedTotal}` ); assert - .dom(syncTab.average) + .dom(statText('Average sync clients per month')) .hasText( `Average sync clients per month ${expectedAvg}`, `renders correct average sync stat ${expectedAvg}` @@ -104,8 +104,10 @@ module('Integration | Component | clients | Clients::Page::Sync', function (hook withTimeZone: true, }); assert.dom(charts.timestamp).hasText(`Updated ${formattedTimestamp}`, 'renders timestamp'); - assert.dom(syncTab.total).doesNotExist('total sync counts does not exist'); - assert.dom(syncTab.average).doesNotExist('average sync client counts does not exist'); + assert.dom(statText('Total sync clients')).doesNotExist('total sync counts does not exist'); + assert + .dom(statText('Average sync clients per month')) + .doesNotExist('average sync client counts does not exist'); }); test('it should render stats without chart for a single month', async function (assert) { @@ -122,8 +124,10 @@ module('Integration | Component | clients | Clients::Page::Sync', function (hook `Secrets sync usage This data can be used to understand how many secrets sync clients have been used for this date range. Each Vault secret that is synced to at least one destination counts as one Vault client. Total sync clients ${total}`, 'renders sync stats instead of chart' ); - assert.dom(syncTab.total).doesNotExist('total sync counts does not exist'); - assert.dom(syncTab.average).doesNotExist('average sync client counts does not exist'); + assert.dom(statText('Average total clients per month')).doesNotExist('total sync counts does not exist'); + assert + .dom(statText('Average sync clients per month')) + .doesNotExist('average sync client counts does not exist'); }); test('it should render an empty state if secrets sync is not activated', async function (assert) { @@ -138,8 +142,8 @@ module('Integration | Component | clients | Clients::Page::Sync', function (hook assert.dom(GENERAL.emptyStateActions).hasText('Activate Secrets Sync'); assert.dom(charts.chart('Secrets sync usage')).doesNotExist(); - assert.dom(syncTab.total).doesNotExist(); - assert.dom(syncTab.average).doesNotExist(); + assert.dom(statText('Total sync clients')).doesNotExist(); + assert.dom(statText('Average sync clients per month')).doesNotExist(); }); test('it should render an empty chart if secrets sync is activated but no secrets synced', async function (assert) { @@ -180,10 +184,12 @@ module('Integration | Component | clients | Clients::Page::Sync', function (hook await this.renderComponent(); assert - .dom(syncTab.total) + .dom(statText('Total sync clients')) .hasText( 'Total sync clients The total number of secrets synced from Vault to other destinations during this date range. 0' ); - assert.dom(syncTab.average).doesNotExist('Does not render average if the calculation is 0'); + assert + .dom(statText('Average sync clients per month')) + .doesNotExist('Does not render average if the calculation is 0'); }); }); diff --git a/ui/tests/integration/components/clients/page/token-test.js b/ui/tests/integration/components/clients/page/token-test.js index a9ef898c9a..5a644380cf 100644 --- a/ui/tests/integration/components/clients/page/token-test.js +++ b/ui/tests/integration/components/clients/page/token-test.js @@ -161,8 +161,12 @@ module('Integration | Component | clients | Page::Token', function (hooks) { assert.dom(`${chart} ${CLIENT_COUNT.charts.verticalBar}`).doesNotExist('Chart does not render'); assert.dom(`${chart} ${CLIENT_COUNT.charts.legend}`).doesNotExist('Legend does not render'); assert.dom(GENERAL.emptyStateTitle).hasText('No new clients'); - assert.dom(CLIENT_COUNT.tokenTab.entity).doesNotExist('New client counts does not exist'); - assert.dom(CLIENT_COUNT.tokenTab.nonentity).doesNotExist('Average new client counts does not exist'); + assert + .dom(CLIENT_COUNT.statText('Average new entity clients per month')) + .doesNotExist('New client counts does not exist'); + assert + .dom(CLIENT_COUNT.statText('Average new non-entity clients per month')) + .doesNotExist('Average new client counts does not exist'); }); test('it should render usage stats', async function (assert) { diff --git a/ui/tests/integration/components/clients/usage-stats-test.js b/ui/tests/integration/components/clients/usage-stats-test.js index 76d2a3d83a..62a238e29c 100644 --- a/ui/tests/integration/components/clients/usage-stats-test.js +++ b/ui/tests/integration/components/clients/usage-stats-test.js @@ -25,15 +25,15 @@ module('Integration | Component | clients/usage-stats', function (hooks) { await this.renderComponent(); assert.dom('[data-test-stat-text]').exists({ count: 3 }, 'Renders 3 Stat texts even with no data passed'); - assert.dom('[data-test-stat-text="total-clients"]').exists('Total clients exists'); - assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('-', 'renders dash when no data'); - assert.dom('[data-test-stat-text="entity-clients"]').exists('Entity clients exists'); + assert.dom('[data-test-stat-text="Total clients"]').exists('Total clients exists'); + assert.dom('[data-test-stat-text="Total clients"] .stat-value').hasText('-', 'renders dash when no data'); + assert.dom('[data-test-stat-text="Entity clients"]').exists('Entity clients exists'); assert - .dom('[data-test-stat-text="entity-clients"] .stat-value') + .dom('[data-test-stat-text="Entity clients"] .stat-value') .hasText('-', 'renders dash when no data'); - assert.dom('[data-test-stat-text="non-entity-clients"]').exists('Non entity clients exists'); + assert.dom('[data-test-stat-text="Non-entity clients"]').exists('Non entity clients exists'); assert - .dom('[data-test-stat-text="non-entity-clients"] .stat-value') + .dom('[data-test-stat-text="Non-entity clients"] .stat-value') .hasText('-', 'renders dash when no data'); assert .dom('a') @@ -51,13 +51,13 @@ module('Integration | Component | clients/usage-stats', function (hooks) { assert.dom('[data-test-stat-text]').exists({ count: 3 }, 'Renders 3 Stat texts'); assert - .dom('[data-test-stat-text="total-clients"] .stat-value') + .dom('[data-test-stat-text="Total clients"] .stat-value') .hasText('17', 'Total clients shows passed value'); assert - .dom('[data-test-stat-text="entity-clients"] .stat-value') + .dom('[data-test-stat-text="Entity clients"] .stat-value') .hasText('7', 'entity clients shows passed value'); assert - .dom('[data-test-stat-text="non-entity-clients"] .stat-value') + .dom('[data-test-stat-text="Non-entity clients"] .stat-value') .hasText('10', 'non entity clients shows passed value'); }); @@ -78,7 +78,7 @@ module('Integration | Component | clients/usage-stats', function (hooks) { assert.dom('[data-test-stat-text]').exists({ count: 4 }, 'Renders 4 Stat texts'); assert - .dom('[data-test-stat-text="secret-syncs"] .stat-value') + .dom('[data-test-stat-text="Secrets sync clients"] .stat-value') .hasText('5', 'secrets sync clients shows passed value'); }); @@ -88,7 +88,7 @@ module('Integration | Component | clients/usage-stats', function (hooks) { await this.renderComponent(); assert.dom('[data-test-stat-text]').exists({ count: 3 }, 'Renders 3 Stat texts'); - assert.dom('[data-test-stat-text="secret-syncs"] .stat-value').doesNotExist(); + assert.dom('[data-test-stat-text="Secrets sync clients"] .stat-value').doesNotExist(); }); }); }); diff --git a/ui/tests/integration/components/dashboard/client-count-card-test.js b/ui/tests/integration/components/dashboard/client-count-card-test.js index 3b0550b1cb..e3e6445f57 100644 --- a/ui/tests/integration/components/dashboard/client-count-card-test.js +++ b/ui/tests/integration/components/dashboard/client-count-card-test.js @@ -12,6 +12,7 @@ import sinon from 'sinon'; import { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients'; import timestamp from 'core/utils/timestamp'; import { ACTIVITY_RESPONSE_STUB } from 'vault/tests/helpers/clients/client-count-helpers'; +import { formatNumber } from 'core/helpers/format-number'; module('Integration | Component | dashboard/client-count-card', function (hooks) { setupRenderingTest(hooks); @@ -33,6 +34,8 @@ module('Integration | Component | dashboard/client-count-card', function (hooks) test('it should display client count information', async function (assert) { assert.expect(9); + const { months, total } = ACTIVITY_RESPONSE_STUB; + const [latestMonth] = months.slice(-1); this.server.get('sys/internal/counters/activity', () => { // this assertion should be hit twice, once initially and then again clicking 'refresh' assert.true(true, 'makes request to sys/internal/counters/activity'); @@ -48,13 +51,16 @@ module('Integration | Component | dashboard/client-count-card', function (hooks) assert .dom('[data-test-stat-text="total-clients"] .stat-text') .hasText('The number of clients in this billing period (Jul 2023 - Jan 2024).'); - assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('7,805'); + assert + .dom('[data-test-stat-text="total-clients"] .stat-value') + .hasText(`${formatNumber([total.clients])}`); assert.dom('[data-test-stat-text="new-clients"] .stat-label').hasText('New'); assert .dom('[data-test-stat-text="new-clients"] .stat-text') .hasText('The number of clients new to Vault in the current month.'); - assert.dom('[data-test-stat-text="new-clients"] .stat-value').hasText('336'); - + assert + .dom('[data-test-stat-text="new-clients"] .stat-value') + .hasText(`${formatNumber([latestMonth.new_clients.counts.clients])}`); // fires second request to /activity await click('[data-test-refresh]'); }); diff --git a/ui/tests/integration/components/stat-text-test.js b/ui/tests/integration/components/stat-text-test.js index d3e398dfd4..0f422bb99e 100644 --- a/ui/tests/integration/components/stat-text-test.js +++ b/ui/tests/integration/components/stat-text-test.js @@ -14,7 +14,7 @@ module('Integration | Component | StatText', function (hooks) { test('it renders', async function (assert) { await render(hbs``); - assert.dom('[data-test-stat-text-container]').exists('renders element'); + assert.dom('[data-test-stat-text]').exists('renders element'); }); test('it renders passed in attributes', async function (assert) { diff --git a/ui/tests/integration/utils/client-count-utils-test.js b/ui/tests/integration/utils/client-count-utils-test.js index f9c1c256e0..51da8a37af 100644 --- a/ui/tests/integration/utils/client-count-utils-test.js +++ b/ui/tests/integration/utils/client-count-utils-test.js @@ -98,11 +98,11 @@ module('Integration | Util | client count utils', function (hooks) { assert.expect(2); const original = { ...RESPONSE.total }; const expected = { - entity_clients: 1816, - non_entity_clients: 3117, - secret_syncs: 2672, - acme_clients: 200, - clients: 7805, + acme_clients: 9702, + clients: 35287, + entity_clients: 8258, + non_entity_clients: 8227, + secret_syncs: 9100, }; assert.propEqual(destructureClientCounts(RESPONSE.total), expected); assert.propEqual(RESPONSE.total, original, 'it does not modify original object'); @@ -302,6 +302,7 @@ module('Integration | Util | client count utils', function (hooks) { entity_clients: 1, label: 'auth/u/', month: '4/24', + timestamp: '2024-04-01T00:00:00Z', non_entity_clients: 0, secret_syncs: 0, }, @@ -321,6 +322,7 @@ module('Integration | Util | client count utils', function (hooks) { entity_clients: 2, label: 'no mount accessor (pre-1.10 upgrade?)', month: '4/24', + timestamp: '2024-04-01T00:00:00Z', non_entity_clients: 0, secret_syncs: 0, }, @@ -335,6 +337,7 @@ module('Integration | Util | client count utils', function (hooks) { entity_clients: 3, label: 'root', month: '4/24', + timestamp: '2024-04-01T00:00:00Z', mounts: [ { acme_clients: 0,