diff --git a/changelog/26729.txt b/changelog/26729.txt new file mode 100644 index 000000000000..4277b50a98ef --- /dev/null +++ b/changelog/26729.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui (enterprise): Update dashboard to make activity log query using the same start time as the metrics overview +``` \ No newline at end of file diff --git a/ui/app/components/clients/page/token.hbs b/ui/app/components/clients/page/token.hbs index c386cf965f04..198fd78783f0 100644 --- a/ui/app/components/clients/page/token.hbs +++ b/ui/app/components/clients/page/token.hbs @@ -15,7 +15,7 @@ @@ -100,21 +100,21 @@ diff --git a/ui/app/components/clients/page/token.ts b/ui/app/components/clients/page/token.ts index 4d653ec3eff9..716e32943e26 100644 --- a/ui/app/components/clients/page/token.ts +++ b/ui/app/components/clients/page/token.ts @@ -37,11 +37,11 @@ export default class ClientsTokenPageComponent extends ActivityComponent { return this.calculateClientAverages(this.byMonthNewClients); } - get tokenUsageCounts() { + get tokenStats() { if (this.totalUsageCounts) { const { entity_clients, non_entity_clients } = this.totalUsageCounts; return { - clients: entity_clients + non_entity_clients, + total: entity_clients + non_entity_clients, entity_clients, non_entity_clients, }; diff --git a/ui/app/components/dashboard/client-count-card.hbs b/ui/app/components/dashboard/client-count-card.hbs index bdaea93f5b3d..3690e19ab0e2 100644 --- a/ui/app/components/dashboard/client-count-card.hbs +++ b/ui/app/components/dashboard/client-count-card.hbs @@ -23,23 +23,8 @@ {{else}}
- - + +
@@ -55,7 +40,7 @@ /> Updated - {{date-format this.updatedAt "MMM dd, yyyy hh:mm:ss"}} + {{date-format this.updatedAt "MMM d yyyy, h:mm:ss aaa" withTimeZone=true}}
{{/if}} diff --git a/ui/app/components/dashboard/client-count-card.js b/ui/app/components/dashboard/client-count-card.js index 6df142b6ebab..e9934acbd6b7 100644 --- a/ui/app/components/dashboard/client-count-card.js +++ b/ui/app/components/dashboard/client-count-card.js @@ -4,43 +4,52 @@ */ import Component from '@glimmer/component'; -import getStorage from 'vault/lib/token-storage'; import timestamp from 'core/utils/timestamp'; import { task } from 'ember-concurrency'; import { waitFor } from '@ember/test-waiters'; import { tracked } from '@glimmer/tracking'; import { service } from '@ember/service'; +import { setStartTimeQuery } from 'core/utils/client-count-utils'; +import { dateFormat } from 'core/helpers/date-format'; /** * @module DashboardClientCountCard * DashboardClientCountCard component are used to display total and new client count information * * @example - * ```js - * - * ``` - * @param {object} license - license object passed from the parent + * + * + * + * @param {boolean} isEnterprise - used for setting the start time for the activity log query */ export default class DashboardClientCountCard extends Component { @service store; + clientConfig = null; + licenseStartTime = null; @tracked activityData = null; - @tracked clientConfig = null; @tracked updatedAt = timestamp.now().toISOString(); constructor() { super(...arguments); this.fetchClientActivity.perform(); - this.clientConfig = this.store.queryRecord('clients/config', {}).catch(() => {}); } get currentMonthActivityTotalCount() { return this.activityData?.byMonth?.lastObject?.new_clients.clients; } - get licenseStartTime() { - return this.args.license.startTime || getStorage().getItem('vault:ui-inputted-start-date') || null; + get statSubText() { + const format = (date) => dateFormat([date, 'MMM yyyy'], {}); + return this.licenseStartTime + ? { + total: `The number of clients in this billing period (${format(this.licenseStartTime)} - ${format( + this.updatedAt + )}).`, + new: 'The number of clients new to Vault in the current month.', + } + : { total: 'No total client data available.', new: 'No new client data available.' }; } @task @@ -48,6 +57,13 @@ export default class DashboardClientCountCard extends Component { *fetchClientActivity(e) { if (e) e.preventDefault(); this.updatedAt = timestamp.now().toISOString(); + + if (!this.clientConfig) { + // set config and license start time when component initializes + this.clientConfig = yield this.store.queryRecord('clients/config', {}).catch(() => {}); + this.licenseStartTime = setStartTimeQuery(this.args.isEnterprise, this.clientConfig); + } + // only make the network request if we have a start_time if (!this.licenseStartTime) return {}; try { diff --git a/ui/app/components/dashboard/overview.hbs b/ui/app/components/dashboard/overview.hbs index f87059d5fe8d..f2cdf2695fdc 100644 --- a/ui/app/components/dashboard/overview.hbs +++ b/ui/app/components/dashboard/overview.hbs @@ -7,11 +7,9 @@
- {{#if (and @version.isEnterprise (or @license @isRootNamespace))}} + {{#if (and @version.isEnterprise @isRootNamespace)}}
- {{#if @license}} - - {{/if}} + {{#if (and @isRootNamespace (has-permission "status" routeParams="replication") (not (is-empty-value @replication))) }} diff --git a/ui/app/routes/vault/cluster/clients/counts.ts b/ui/app/routes/vault/cluster/clients/counts.ts index 54012bb6cdc4..e5be73f97e67 100644 --- a/ui/app/routes/vault/cluster/clients/counts.ts +++ b/ui/app/routes/vault/cluster/clients/counts.ts @@ -14,9 +14,10 @@ import type VersionService from 'vault/services/version'; import type { ModelFrom } from 'vault/vault/route'; import type ClientsRoute from '../clients'; -import type ClientsConfigModel from 'vault/models/clients/config'; import type ClientsActivityModel from 'vault/models/clients/activity'; import type ClientsCountsController from 'vault/controllers/vault/cluster/clients/counts'; +import { setStartTimeQuery } from 'core/utils/client-count-utils'; + export interface ClientsCountsRouteParams { start_time?: string | number | undefined; end_time?: string | number | undefined; @@ -86,10 +87,7 @@ export default class ClientsCountsRoute extends Route { async model(params: ClientsCountsRouteParams) { const { config, versionHistory } = this.modelFor('vault.cluster.clients') as ModelFrom; // only enterprise versions will have a relevant billing start date, if null users must select initial start time - let startTime = null; - if (this.version.isEnterprise && this._hasConfig(config)) { - startTime = getUnixTime(config.billingStartTimestamp); - } + const startTime = setStartTimeQuery(this.version.isEnterprise, config); const startTimestamp = Number(params.start_time) || startTime; const endTimestamp = Number(params.end_time) || getUnixTime(timestamp.now()); @@ -118,8 +116,4 @@ export default class ClientsCountsRoute extends Route { }); } } - - _hasConfig(model: ClientsConfigModel | object): model is ClientsConfigModel { - return 'billingStartTimestamp' in model; - } } diff --git a/ui/app/routes/vault/cluster/dashboard.js b/ui/app/routes/vault/cluster/dashboard.js index a8af612f1720..c7871ab2cdda 100644 --- a/ui/app/routes/vault/cluster/dashboard.js +++ b/ui/app/routes/vault/cluster/dashboard.js @@ -40,7 +40,6 @@ export default class VaultClusterDashboardRoute extends Route.extend(ClusterRout return hash({ replication, secretsEngines: this.store.query('secret-engine', {}), - license: this.store.queryRecord('license', {}).catch(() => null), isRootNamespace: this.namespace.inRootNamespace && !hasChroot, version: this.version, vaultConfiguration: hasChroot ? null : this.getVaultConfiguration(), diff --git a/ui/app/templates/vault/cluster/dashboard.hbs b/ui/app/templates/vault/cluster/dashboard.hbs index b7f212674a0d..52ce5adcf197 100644 --- a/ui/app/templates/vault/cluster/dashboard.hbs +++ b/ui/app/templates/vault/cluster/dashboard.hbs @@ -6,7 +6,6 @@ +) => { + // CE versions have no license and so the start time defaults to "0001-01-01T00:00:00Z" + if (isEnterprise && _hasConfig(config)) { + return getUnixTime(config.billingStartTimestamp); + } + return null; +}; + +// METHODS FOR SERIALIZING ACTIVITY RESPONSE export const formatDateObject = (dateObj: { monthIdx: number; year: number }, isEnd: boolean) => { const { year, monthIdx } = dateObj; // day=0 for Date.UTC() returns the last day of the month before @@ -188,6 +201,11 @@ export const namespaceArrayToObject = ( }; // type guards for conditionals +function _hasConfig(model: ClientsConfigModel | object): model is ClientsConfigModel { + if (!model) return false; + return 'billingStartTimestamp' in model; +} + export function hasMountsKey( obj: ByMonthNewClients | NamespaceNewClients | MountNewClients ): obj is NamespaceNewClients { @@ -201,7 +219,6 @@ export function hasNamespacesKey( } // TYPES RETURNED BY UTILS (serialized) - export interface TotalClients { clients: number; entity_clients: number; diff --git a/ui/tests/acceptance/dashboard-test.js b/ui/tests/acceptance/dashboard-test.js index acdb8e984c20..e3753c4deea1 100644 --- a/ui/tests/acceptance/dashboard-test.js +++ b/ui/tests/acceptance/dashboard-test.js @@ -404,16 +404,14 @@ module('Acceptance | landing page dashboard', function (hooks) { assert.dom(DASHBOARD.cardName('client-count')).exists(); const response = await this.store.peekRecord('clients/activity', 'some-activity-id'); assert.dom('[data-test-client-count-title]').hasText('Client count'); - assert.dom('[data-test-stat-text="total-clients"] .stat-label').hasText('Total'); + assert.dom('[data-test-stat-text="Total"] .stat-label').hasText('Total'); + assert.dom('[data-test-stat-text="Total"] .stat-value').hasText(formatNumber([response.total.clients])); + assert.dom('[data-test-stat-text="New"] .stat-label').hasText('New'); assert - .dom('[data-test-stat-text="total-clients"] .stat-value') - .hasText(formatNumber([response.total.clients])); - assert.dom('[data-test-stat-text="new-clients"] .stat-label').hasText('New'); - assert - .dom('[data-test-stat-text="new-clients"] .stat-text') + .dom('[data-test-stat-text="New"] .stat-text') .hasText('The number of clients new to Vault in the current month.'); assert - .dom('[data-test-stat-text="new-clients"] .stat-value') + .dom('[data-test-stat-text="New"] .stat-value') .hasText(formatNumber([response.byMonth.lastObject.new_clients.clients])); }); }); diff --git a/ui/tests/integration/components/clients/page/token-test.js b/ui/tests/integration/components/clients/page/token-test.js index a0553ac11f62..60e562282adf 100644 --- a/ui/tests/integration/components/clients/page/token-test.js +++ b/ui/tests/integration/components/clients/page/token-test.js @@ -69,21 +69,25 @@ module('Integration | Component | clients | Clients::Page::Token', function (hoo test('it should render monthly total chart', async function (assert) { const count = this.activity.byMonth.length; - assert.expect(count + 7); + const { entity_clients, non_entity_clients } = this.activity.total; + assert.expect(count + 8); const getAverage = (data) => { const average = ['entity_clients', 'non_entity_clients'].reduce((count, key) => { return (count += calculateAverage(data, key) || 0); }, 0); return formatNumber([average]); }; - const expectedTotal = getAverage(this.activity.byMonth); + const expectedAvg = getAverage(this.activity.byMonth); + const expectedTotal = formatNumber([entity_clients + non_entity_clients]); const chart = CHARTS.container('Entity/Non-entity clients usage'); - await this.renderComponent(); assert - .dom(CLIENT_COUNT.statTextValue('Average total clients per month')) + .dom(CLIENT_COUNT.statTextValue('Total clients')) .hasText(expectedTotal, 'renders correct total clients'); + assert + .dom(CLIENT_COUNT.statTextValue('Average total clients per month')) + .hasText(expectedAvg, 'renders correct average clients'); // assert bar chart is correct assert.dom(`${chart} ${CHARTS.xAxis}`).hasText('7/23 8/23 9/23 10/23 11/23 12/23 1/24'); 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 e3e6445f5716..0cc75566de04 100644 --- a/ui/tests/integration/components/dashboard/client-count-card-test.js +++ b/ui/tests/integration/components/dashboard/client-count-card-test.js @@ -13,6 +13,7 @@ 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'; +import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors'; module('Integration | Component | dashboard/client-count-card', function (hooks) { setupRenderingTest(hooks); @@ -22,18 +23,12 @@ module('Integration | Component | dashboard/client-count-card', function (hooks) sinon.stub(timestamp, 'now').callsFake(() => STATIC_NOW); }); - hooks.beforeEach(function () { - this.license = { - startTime: LICENSE_START.toISOString(), - }; - }); - hooks.after(function () { timestamp.now.restore(); }); test('it should display client count information', async function (assert) { - assert.expect(9); + assert.expect(6); const { months, total } = ACTIVITY_RESPONSE_STUB; const [latestMonth] = months.slice(-1); this.server.get('sys/internal/counters/activity', () => { @@ -44,24 +39,62 @@ module('Integration | Component | dashboard/client-count-card', function (hooks) data: ACTIVITY_RESPONSE_STUB, }; }); + this.server.get('sys/internal/counters/config', function () { + assert.true(true, 'sys/internal/counters/config'); + return { + request_id: 'some-config-id', + data: { + billing_start_timestamp: LICENSE_START.toISOString(), + }, + }; + }); - await render(hbs``); + await render(hbs``); assert.dom('[data-test-client-count-title]').hasText('Client count'); - assert.dom('[data-test-stat-text="total-clients"] .stat-label').hasText('Total'); 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(`${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.'); + .dom(CLIENT_COUNT.statText('Total')) + .hasText( + `Total The number of clients in this billing period (Jul 2023 - Jan 2024). ${formatNumber([ + total.clients, + ])}` + ); + assert - .dom('[data-test-stat-text="new-clients"] .stat-value') - .hasText(`${formatNumber([latestMonth.new_clients.counts.clients])}`); + .dom(CLIENT_COUNT.statText('New')) + .hasText( + `New The number of clients new to Vault in the current month. ${formatNumber([ + latestMonth.new_clients.counts.clients, + ])}` + ); + // fires second request to /activity await click('[data-test-refresh]'); }); + + test('it does not query activity for community edition', async function (assert) { + assert.expect(3); + // in the template this component is wrapped in an isEnterprise conditional so this + // state is currently not possible, adding a test to safeguard against introducing + // regressions during future refactors + this.server.get( + 'sys/internal/counters/activity', + () => new Error('uh oh! a request was made to sys/internal/counters/activity') + ); + this.server.get('sys/internal/counters/config', function () { + assert.true(true, 'sys/internal/counters/config'); + return { + request_id: 'some-config-id', + data: { + billing_start_timestamp: '0001-01-01T00:00:00Z', + }, + }; + }); + + await render(hbs``); + assert.dom(CLIENT_COUNT.statText('Total')).hasText('Total No total client data available. -'); + assert.dom(CLIENT_COUNT.statText('New')).hasText('New No new client data available. -'); + + // attempt second request to /activity but component task should return instead of hitting endpoint + await click('[data-test-refresh]'); + }); }); diff --git a/ui/tests/integration/components/dashboard/overview-test.js b/ui/tests/integration/components/dashboard/overview-test.js index 6670e4d2ea4a..a367e1c2813f 100644 --- a/ui/tests/integration/components/dashboard/overview-test.js +++ b/ui/tests/integration/components/dashboard/overview-test.js @@ -9,6 +9,7 @@ import { render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { DASHBOARD } from 'vault/tests/helpers/components/dashboard/dashboard-selectors'; +import { LICENSE_START } from 'vault/mirage/handlers/clients'; module('Integration | Component | dashboard/overview', function (hooks) { setupRenderingTest(hooks); @@ -60,6 +61,14 @@ module('Integration | Component | dashboard/overview', function (hooks) { ], }; this.refreshModel = () => {}; + this.server.get('sys/internal/counters/config', function () { + return { + request_id: 'some-config-id', + data: { + billing_start_timestamp: LICENSE_START.toISOString(), + }, + }; + }); }); test('it should show dashboard empty states', async function (assert) { @@ -129,7 +138,6 @@ module('Integration | Component | dashboard/overview', function (hooks) { @replication={{this.replication}} @version={{this.version}} @isRootNamespace={{true}} - @license={{this.license}} @refreshModel={{this.refreshModel}} />` ); assert.dom(DASHBOARD.cardHeader('Vault version')).exists(); @@ -140,43 +148,11 @@ module('Integration | Component | dashboard/overview', function (hooks) { assert.dom(DASHBOARD.cardName('client-count')).exists(); }); - test('it should hide client count on enterprise w/o license ', async function (assert) { - this.version = this.owner.lookup('service:version'); - this.version.version = '1.13.1+ent'; - this.version.type = 'enterprise'; - this.isRootNamespace = true; - - await render( - hbs` - ` - ); - - assert.dom(DASHBOARD.cardHeader('Vault version')).exists(); - assert.dom('[data-test-badge-namespace]').exists(); - assert.dom(DASHBOARD.cardName('secrets-engines')).exists(); - assert.dom(DASHBOARD.cardName('learn-more')).exists(); - assert.dom(DASHBOARD.cardName('quick-actions')).exists(); - assert.dom(DASHBOARD.cardName('configuration-details')).exists(); - assert.dom(DASHBOARD.cardName('client-count')).doesNotExist(); - }); - test('it should hide replication on enterprise not on root namespace', async function (assert) { this.version = this.owner.lookup('service:version'); this.version.version = '1.13.1+ent'; this.version.type = 'enterprise'; this.isRootNamespace = false; - this.license = { - autoloaded: { - license_id: '7adbf1f4-56ef-35cd-3a6c-50ef2627865d', - }, - }; await render( hbs` @@ -186,7 +162,6 @@ module('Integration | Component | dashboard/overview', function (hooks) { @secretsEngines={{this.secretsEngines}} @vaultConfiguration={{this.vaultConfiguration}} @replication={{this.replication}} - @license={{this.license}} @refreshModel={{this.refreshModel}} />` ); @@ -197,7 +172,7 @@ module('Integration | Component | dashboard/overview', function (hooks) { assert.dom(DASHBOARD.cardName('quick-actions')).exists(); assert.dom(DASHBOARD.cardName('configuration-details')).exists(); assert.dom(DASHBOARD.cardName('replication')).doesNotExist(); - assert.dom(DASHBOARD.cardName('client-count')).exists(); + assert.dom(DASHBOARD.cardName('client-count')).doesNotExist(); }); module('learn more card', function () { @@ -238,7 +213,6 @@ module('Integration | Component | dashboard/overview', function (hooks) {