mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-01 19:17:58 +00:00
UI: Update dashboard client count query to use billing_start_timestamp (#26729)
* remvoe request tolicense in dashboard client count card * cleanup jsdoc * add changelog * use helper to set start time * update component tests * update overview test * update util tests * throw error instead, add comment to util file * fix accidentally removed type from import * remove typo arg from test component * rename token stat getter to avoid future typos
This commit is contained in:
3
changelog/26729.txt
Normal file
3
changelog/26729.txt
Normal file
@@ -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
|
||||||
|
```
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<StatText
|
<StatText
|
||||||
@label="Total clients"
|
@label="Total clients"
|
||||||
@subText="The total number of entity and non-entity clients for this date range."
|
@subText="The total number of entity and non-entity clients for this date range."
|
||||||
@value={{this.totalUsageCounts.clients}}
|
@value={{this.tokenStats.total}}
|
||||||
@tooltipText="This number is the total for the queried date range. The chart displays a monthly breakdown of total clients per month."
|
@tooltipText="This number is the total for the queried date range. The chart displays a monthly breakdown of total clients per month."
|
||||||
@size="l"
|
@size="l"
|
||||||
/>
|
/>
|
||||||
@@ -100,21 +100,21 @@
|
|||||||
<StatText
|
<StatText
|
||||||
class="column"
|
class="column"
|
||||||
@label="Total clients"
|
@label="Total clients"
|
||||||
@value={{this.tokenUsageCounts.clients}}
|
@value={{this.tokenStats.total}}
|
||||||
@size="l"
|
@size="l"
|
||||||
@subText="The number of clients which interacted with Vault during this month. This is Vault’s primary billing metric."
|
@subText="The number of clients which interacted with Vault during this month. This is Vault’s primary billing metric."
|
||||||
/>
|
/>
|
||||||
<StatText
|
<StatText
|
||||||
class="column"
|
class="column"
|
||||||
@label="Entity"
|
@label="Entity"
|
||||||
@value={{this.tokenUsageCounts.entity_clients}}
|
@value={{this.tokenStats.entity_clients}}
|
||||||
@size="l"
|
@size="l"
|
||||||
@subText="Representations of a particular user, client, or application that created a token via login."
|
@subText="Representations of a particular user, client, or application that created a token via login."
|
||||||
/>
|
/>
|
||||||
<StatText
|
<StatText
|
||||||
class="column"
|
class="column"
|
||||||
@label="Non-entity"
|
@label="Non-entity"
|
||||||
@value={{this.tokenUsageCounts.non_entity_clients}}
|
@value={{this.tokenStats.non_entity_clients}}
|
||||||
@size="l"
|
@size="l"
|
||||||
@subText="Clients created with a shared set of permissions, but not associated with an entity."
|
@subText="Clients created with a shared set of permissions, but not associated with an entity."
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ export default class ClientsTokenPageComponent extends ActivityComponent {
|
|||||||
return this.calculateClientAverages(this.byMonthNewClients);
|
return this.calculateClientAverages(this.byMonthNewClients);
|
||||||
}
|
}
|
||||||
|
|
||||||
get tokenUsageCounts() {
|
get tokenStats() {
|
||||||
if (this.totalUsageCounts) {
|
if (this.totalUsageCounts) {
|
||||||
const { entity_clients, non_entity_clients } = this.totalUsageCounts;
|
const { entity_clients, non_entity_clients } = this.totalUsageCounts;
|
||||||
return {
|
return {
|
||||||
clients: entity_clients + non_entity_clients,
|
total: entity_clients + non_entity_clients,
|
||||||
entity_clients,
|
entity_clients,
|
||||||
non_entity_clients,
|
non_entity_clients,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -23,23 +23,8 @@
|
|||||||
<VaultLogoSpinner />
|
<VaultLogoSpinner />
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="is-grid grid-2-columns grid-gap-2 has-top-margin-m grid-align-items-start is-flex-v-centered">
|
<div class="is-grid grid-2-columns grid-gap-2 has-top-margin-m grid-align-items-start is-flex-v-centered">
|
||||||
<StatText
|
<StatText @label="Total" @value={{this.activityData.total.clients}} @size="l" @subText={{this.statSubText.total}} />
|
||||||
@label="Total"
|
<StatText @label="New" @value={{this.currentMonthActivityTotalCount}} @size="l" @subText={{this.statSubText.new}} />
|
||||||
@value={{this.activityData.total.clients}}
|
|
||||||
@size="l"
|
|
||||||
@subText="The number of clients in this billing period ({{date-format
|
|
||||||
this.licenseStartTime
|
|
||||||
'MMM yyyy'
|
|
||||||
}} - {{date-format this.updatedAt 'MMM yyyy'}})."
|
|
||||||
data-test-stat-text="total-clients"
|
|
||||||
/>
|
|
||||||
<StatText
|
|
||||||
@label="New"
|
|
||||||
@value={{this.currentMonthActivityTotalCount}}
|
|
||||||
@size="l"
|
|
||||||
@subText="The number of clients new to Vault in the current month."
|
|
||||||
data-test-stat-text="new-clients"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="has-top-margin-l is-flex-center">
|
<div class="has-top-margin-l is-flex-center">
|
||||||
@@ -55,7 +40,7 @@
|
|||||||
/>
|
/>
|
||||||
<small class="has-left-margin-xs has-text-grey">
|
<small class="has-left-margin-xs has-text-grey">
|
||||||
Updated
|
Updated
|
||||||
{{date-format this.updatedAt "MMM dd, yyyy hh:mm:ss"}}
|
{{date-format this.updatedAt "MMM d yyyy, h:mm:ss aaa" withTimeZone=true}}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|||||||
@@ -4,43 +4,52 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import Component from '@glimmer/component';
|
import Component from '@glimmer/component';
|
||||||
import getStorage from 'vault/lib/token-storage';
|
|
||||||
import timestamp from 'core/utils/timestamp';
|
import timestamp from 'core/utils/timestamp';
|
||||||
import { task } from 'ember-concurrency';
|
import { task } from 'ember-concurrency';
|
||||||
import { waitFor } from '@ember/test-waiters';
|
import { waitFor } from '@ember/test-waiters';
|
||||||
import { tracked } from '@glimmer/tracking';
|
import { tracked } from '@glimmer/tracking';
|
||||||
import { service } from '@ember/service';
|
import { service } from '@ember/service';
|
||||||
|
import { setStartTimeQuery } from 'core/utils/client-count-utils';
|
||||||
|
import { dateFormat } from 'core/helpers/date-format';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module DashboardClientCountCard
|
* @module DashboardClientCountCard
|
||||||
* DashboardClientCountCard component are used to display total and new client count information
|
* DashboardClientCountCard component are used to display total and new client count information
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```js
|
*
|
||||||
* <Dashboard::ClientCountCard @license={{@model.license}} />
|
* <Dashboard::ClientCountCard @isEnterprise={{@version.isEnterprise}} />
|
||||||
* ```
|
*
|
||||||
* @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 {
|
export default class DashboardClientCountCard extends Component {
|
||||||
@service store;
|
@service store;
|
||||||
|
|
||||||
|
clientConfig = null;
|
||||||
|
licenseStartTime = null;
|
||||||
@tracked activityData = null;
|
@tracked activityData = null;
|
||||||
@tracked clientConfig = null;
|
|
||||||
@tracked updatedAt = timestamp.now().toISOString();
|
@tracked updatedAt = timestamp.now().toISOString();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(...arguments);
|
super(...arguments);
|
||||||
this.fetchClientActivity.perform();
|
this.fetchClientActivity.perform();
|
||||||
this.clientConfig = this.store.queryRecord('clients/config', {}).catch(() => {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get currentMonthActivityTotalCount() {
|
get currentMonthActivityTotalCount() {
|
||||||
return this.activityData?.byMonth?.lastObject?.new_clients.clients;
|
return this.activityData?.byMonth?.lastObject?.new_clients.clients;
|
||||||
}
|
}
|
||||||
|
|
||||||
get licenseStartTime() {
|
get statSubText() {
|
||||||
return this.args.license.startTime || getStorage().getItem('vault:ui-inputted-start-date') || null;
|
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
|
@task
|
||||||
@@ -48,6 +57,13 @@ export default class DashboardClientCountCard extends Component {
|
|||||||
*fetchClientActivity(e) {
|
*fetchClientActivity(e) {
|
||||||
if (e) e.preventDefault();
|
if (e) e.preventDefault();
|
||||||
this.updatedAt = timestamp.now().toISOString();
|
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
|
// only make the network request if we have a start_time
|
||||||
if (!this.licenseStartTime) return {};
|
if (!this.licenseStartTime) return {};
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -7,11 +7,9 @@
|
|||||||
|
|
||||||
<div class="has-bottom-margin-xl">
|
<div class="has-bottom-margin-xl">
|
||||||
<div class="is-flex-row gap-24">
|
<div class="is-flex-row gap-24">
|
||||||
{{#if (and @version.isEnterprise (or @license @isRootNamespace))}}
|
{{#if (and @version.isEnterprise @isRootNamespace)}}
|
||||||
<div class="is-flex-column is-flex-1 gap-24">
|
<div class="is-flex-column is-flex-1 gap-24">
|
||||||
{{#if @license}}
|
<Dashboard::ClientCountCard @isEnterprise={{@version.isEnterprise}} />
|
||||||
<Dashboard::ClientCountCard @license={{@license}} />
|
|
||||||
{{/if}}
|
|
||||||
{{#if
|
{{#if
|
||||||
(and @isRootNamespace (has-permission "status" routeParams="replication") (not (is-empty-value @replication)))
|
(and @isRootNamespace (has-permission "status" routeParams="replication") (not (is-empty-value @replication)))
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ import type VersionService from 'vault/services/version';
|
|||||||
|
|
||||||
import type { ModelFrom } from 'vault/vault/route';
|
import type { ModelFrom } from 'vault/vault/route';
|
||||||
import type ClientsRoute from '../clients';
|
import type ClientsRoute from '../clients';
|
||||||
import type ClientsConfigModel from 'vault/models/clients/config';
|
|
||||||
import type ClientsActivityModel from 'vault/models/clients/activity';
|
import type ClientsActivityModel from 'vault/models/clients/activity';
|
||||||
import type ClientsCountsController from 'vault/controllers/vault/cluster/clients/counts';
|
import type ClientsCountsController from 'vault/controllers/vault/cluster/clients/counts';
|
||||||
|
import { setStartTimeQuery } from 'core/utils/client-count-utils';
|
||||||
|
|
||||||
export interface ClientsCountsRouteParams {
|
export interface ClientsCountsRouteParams {
|
||||||
start_time?: string | number | undefined;
|
start_time?: string | number | undefined;
|
||||||
end_time?: string | number | undefined;
|
end_time?: string | number | undefined;
|
||||||
@@ -86,10 +87,7 @@ export default class ClientsCountsRoute extends Route {
|
|||||||
async model(params: ClientsCountsRouteParams) {
|
async model(params: ClientsCountsRouteParams) {
|
||||||
const { config, versionHistory } = this.modelFor('vault.cluster.clients') as ModelFrom<ClientsRoute>;
|
const { config, versionHistory } = this.modelFor('vault.cluster.clients') as ModelFrom<ClientsRoute>;
|
||||||
// only enterprise versions will have a relevant billing start date, if null users must select initial start time
|
// only enterprise versions will have a relevant billing start date, if null users must select initial start time
|
||||||
let startTime = null;
|
const startTime = setStartTimeQuery(this.version.isEnterprise, config);
|
||||||
if (this.version.isEnterprise && this._hasConfig(config)) {
|
|
||||||
startTime = getUnixTime(config.billingStartTimestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
const startTimestamp = Number(params.start_time) || startTime;
|
const startTimestamp = Number(params.start_time) || startTime;
|
||||||
const endTimestamp = Number(params.end_time) || getUnixTime(timestamp.now());
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ export default class VaultClusterDashboardRoute extends Route.extend(ClusterRout
|
|||||||
return hash({
|
return hash({
|
||||||
replication,
|
replication,
|
||||||
secretsEngines: this.store.query('secret-engine', {}),
|
secretsEngines: this.store.query('secret-engine', {}),
|
||||||
license: this.store.queryRecord('license', {}).catch(() => null),
|
|
||||||
isRootNamespace: this.namespace.inRootNamespace && !hasChroot,
|
isRootNamespace: this.namespace.inRootNamespace && !hasChroot,
|
||||||
version: this.version,
|
version: this.version,
|
||||||
vaultConfiguration: hasChroot ? null : this.getVaultConfiguration(),
|
vaultConfiguration: hasChroot ? null : this.getVaultConfiguration(),
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
<Dashboard::Overview
|
<Dashboard::Overview
|
||||||
@replication={{this.model.replication}}
|
@replication={{this.model.replication}}
|
||||||
@secretsEngines={{this.model.secretsEngines}}
|
@secretsEngines={{this.model.secretsEngines}}
|
||||||
@license={{this.model.license}}
|
|
||||||
@isRootNamespace={{this.model.isRootNamespace}}
|
@isRootNamespace={{this.model.isRootNamespace}}
|
||||||
@version={{this.model.version}}
|
@version={{this.model.version}}
|
||||||
@vaultConfiguration={{this.model.vaultConfiguration}}
|
@vaultConfiguration={{this.model.vaultConfiguration}}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||||
import { compareAsc, getUnixTime, isWithinInterval } from 'date-fns';
|
import { compareAsc, getUnixTime, isWithinInterval } from 'date-fns';
|
||||||
|
|
||||||
|
import type ClientsConfigModel from 'vault/models/clients/config';
|
||||||
import type ClientsVersionHistoryModel from 'vault/vault/models/clients/version-history';
|
import type ClientsVersionHistoryModel from 'vault/vault/models/clients/version-history';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -59,6 +60,18 @@ export const filterVersionHistory = (
|
|||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const setStartTimeQuery = (
|
||||||
|
isEnterprise: boolean,
|
||||||
|
config: ClientsConfigModel | Record<string, never>
|
||||||
|
) => {
|
||||||
|
// 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) => {
|
export const formatDateObject = (dateObj: { monthIdx: number; year: number }, isEnd: boolean) => {
|
||||||
const { year, monthIdx } = dateObj;
|
const { year, monthIdx } = dateObj;
|
||||||
// day=0 for Date.UTC() returns the last day of the month before
|
// day=0 for Date.UTC() returns the last day of the month before
|
||||||
@@ -188,6 +201,11 @@ export const namespaceArrayToObject = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
// type guards for conditionals
|
// type guards for conditionals
|
||||||
|
function _hasConfig(model: ClientsConfigModel | object): model is ClientsConfigModel {
|
||||||
|
if (!model) return false;
|
||||||
|
return 'billingStartTimestamp' in model;
|
||||||
|
}
|
||||||
|
|
||||||
export function hasMountsKey(
|
export function hasMountsKey(
|
||||||
obj: ByMonthNewClients | NamespaceNewClients | MountNewClients
|
obj: ByMonthNewClients | NamespaceNewClients | MountNewClients
|
||||||
): obj is NamespaceNewClients {
|
): obj is NamespaceNewClients {
|
||||||
@@ -201,7 +219,6 @@ export function hasNamespacesKey(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TYPES RETURNED BY UTILS (serialized)
|
// TYPES RETURNED BY UTILS (serialized)
|
||||||
|
|
||||||
export interface TotalClients {
|
export interface TotalClients {
|
||||||
clients: number;
|
clients: number;
|
||||||
entity_clients: number;
|
entity_clients: number;
|
||||||
|
|||||||
@@ -404,16 +404,14 @@ module('Acceptance | landing page dashboard', function (hooks) {
|
|||||||
assert.dom(DASHBOARD.cardName('client-count')).exists();
|
assert.dom(DASHBOARD.cardName('client-count')).exists();
|
||||||
const response = await this.store.peekRecord('clients/activity', 'some-activity-id');
|
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-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
|
assert
|
||||||
.dom('[data-test-stat-text="total-clients"] .stat-value')
|
.dom('[data-test-stat-text="New"] .stat-text')
|
||||||
.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')
|
|
||||||
.hasText('The number of clients new to Vault in the current month.');
|
.hasText('The number of clients new to Vault in the current month.');
|
||||||
assert
|
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]));
|
.hasText(formatNumber([response.byMonth.lastObject.new_clients.clients]));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -69,21 +69,25 @@ module('Integration | Component | clients | Clients::Page::Token', function (hoo
|
|||||||
|
|
||||||
test('it should render monthly total chart', async function (assert) {
|
test('it should render monthly total chart', async function (assert) {
|
||||||
const count = this.activity.byMonth.length;
|
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 getAverage = (data) => {
|
||||||
const average = ['entity_clients', 'non_entity_clients'].reduce((count, key) => {
|
const average = ['entity_clients', 'non_entity_clients'].reduce((count, key) => {
|
||||||
return (count += calculateAverage(data, key) || 0);
|
return (count += calculateAverage(data, key) || 0);
|
||||||
}, 0);
|
}, 0);
|
||||||
return formatNumber([average]);
|
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');
|
const chart = CHARTS.container('Entity/Non-entity clients usage');
|
||||||
|
|
||||||
await this.renderComponent();
|
await this.renderComponent();
|
||||||
|
|
||||||
assert
|
assert
|
||||||
.dom(CLIENT_COUNT.statTextValue('Average total clients per month'))
|
.dom(CLIENT_COUNT.statTextValue('Total clients'))
|
||||||
.hasText(expectedTotal, 'renders correct 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 bar chart is correct
|
||||||
assert.dom(`${chart} ${CHARTS.xAxis}`).hasText('7/23 8/23 9/23 10/23 11/23 12/23 1/24');
|
assert.dom(`${chart} ${CHARTS.xAxis}`).hasText('7/23 8/23 9/23 10/23 11/23 12/23 1/24');
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients';
|
|||||||
import timestamp from 'core/utils/timestamp';
|
import timestamp from 'core/utils/timestamp';
|
||||||
import { ACTIVITY_RESPONSE_STUB } from 'vault/tests/helpers/clients/client-count-helpers';
|
import { ACTIVITY_RESPONSE_STUB } from 'vault/tests/helpers/clients/client-count-helpers';
|
||||||
import { formatNumber } from 'core/helpers/format-number';
|
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) {
|
module('Integration | Component | dashboard/client-count-card', function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
@@ -22,18 +23,12 @@ module('Integration | Component | dashboard/client-count-card', function (hooks)
|
|||||||
sinon.stub(timestamp, 'now').callsFake(() => STATIC_NOW);
|
sinon.stub(timestamp, 'now').callsFake(() => STATIC_NOW);
|
||||||
});
|
});
|
||||||
|
|
||||||
hooks.beforeEach(function () {
|
|
||||||
this.license = {
|
|
||||||
startTime: LICENSE_START.toISOString(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
hooks.after(function () {
|
hooks.after(function () {
|
||||||
timestamp.now.restore();
|
timestamp.now.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it should display client count information', async function (assert) {
|
test('it should display client count information', async function (assert) {
|
||||||
assert.expect(9);
|
assert.expect(6);
|
||||||
const { months, total } = ACTIVITY_RESPONSE_STUB;
|
const { months, total } = ACTIVITY_RESPONSE_STUB;
|
||||||
const [latestMonth] = months.slice(-1);
|
const [latestMonth] = months.slice(-1);
|
||||||
this.server.get('sys/internal/counters/activity', () => {
|
this.server.get('sys/internal/counters/activity', () => {
|
||||||
@@ -44,24 +39,62 @@ module('Integration | Component | dashboard/client-count-card', function (hooks)
|
|||||||
data: ACTIVITY_RESPONSE_STUB,
|
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`<Dashboard::ClientCountCard @license={{this.license}} />`);
|
await render(hbs`<Dashboard::ClientCountCard @isEnterprise={{true}} />`);
|
||||||
assert.dom('[data-test-client-count-title]').hasText('Client count');
|
assert.dom('[data-test-client-count-title]').hasText('Client count');
|
||||||
assert.dom('[data-test-stat-text="total-clients"] .stat-label').hasText('Total');
|
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-stat-text="total-clients"] .stat-text')
|
.dom(CLIENT_COUNT.statText('Total'))
|
||||||
.hasText('The number of clients in this billing period (Jul 2023 - Jan 2024).');
|
.hasText(
|
||||||
|
`Total The number of clients in this billing period (Jul 2023 - Jan 2024). ${formatNumber([
|
||||||
|
total.clients,
|
||||||
|
])}`
|
||||||
|
);
|
||||||
|
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-stat-text="total-clients"] .stat-value')
|
.dom(CLIENT_COUNT.statText('New'))
|
||||||
.hasText(`${formatNumber([total.clients])}`);
|
.hasText(
|
||||||
assert.dom('[data-test-stat-text="new-clients"] .stat-label').hasText('New');
|
`New The number of clients new to Vault in the current month. ${formatNumber([
|
||||||
assert
|
latestMonth.new_clients.counts.clients,
|
||||||
.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(`${formatNumber([latestMonth.new_clients.counts.clients])}`);
|
|
||||||
// fires second request to /activity
|
// fires second request to /activity
|
||||||
await click('[data-test-refresh]');
|
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`<Dashboard::ClientCountCard @isEnterprise={{false}} />`);
|
||||||
|
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]');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { render } from '@ember/test-helpers';
|
|||||||
import { hbs } from 'ember-cli-htmlbars';
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||||
import { DASHBOARD } from 'vault/tests/helpers/components/dashboard/dashboard-selectors';
|
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) {
|
module('Integration | Component | dashboard/overview', function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
@@ -60,6 +61,14 @@ module('Integration | Component | dashboard/overview', function (hooks) {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
this.refreshModel = () => {};
|
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) {
|
test('it should show dashboard empty states', async function (assert) {
|
||||||
@@ -129,7 +138,6 @@ module('Integration | Component | dashboard/overview', function (hooks) {
|
|||||||
@replication={{this.replication}}
|
@replication={{this.replication}}
|
||||||
@version={{this.version}}
|
@version={{this.version}}
|
||||||
@isRootNamespace={{true}}
|
@isRootNamespace={{true}}
|
||||||
@license={{this.license}}
|
|
||||||
@refreshModel={{this.refreshModel}} />`
|
@refreshModel={{this.refreshModel}} />`
|
||||||
);
|
);
|
||||||
assert.dom(DASHBOARD.cardHeader('Vault version')).exists();
|
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();
|
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`
|
|
||||||
<Dashboard::Overview
|
|
||||||
@secretsEngines={{this.secretsEngines}}
|
|
||||||
@vaultConfiguration={{this.vaultConfiguration}}
|
|
||||||
@replication={{this.replication}}
|
|
||||||
@version={{this.version}}
|
|
||||||
@isRootNamespace={{this.isRootNamespace}}
|
|
||||||
@refreshModel={{this.refreshModel}}
|
|
||||||
/>`
|
|
||||||
);
|
|
||||||
|
|
||||||
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) {
|
test('it should hide replication on enterprise not on root namespace', async function (assert) {
|
||||||
this.version = this.owner.lookup('service:version');
|
this.version = this.owner.lookup('service:version');
|
||||||
this.version.version = '1.13.1+ent';
|
this.version.version = '1.13.1+ent';
|
||||||
this.version.type = 'enterprise';
|
this.version.type = 'enterprise';
|
||||||
this.isRootNamespace = false;
|
this.isRootNamespace = false;
|
||||||
this.license = {
|
|
||||||
autoloaded: {
|
|
||||||
license_id: '7adbf1f4-56ef-35cd-3a6c-50ef2627865d',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
await render(
|
await render(
|
||||||
hbs`
|
hbs`
|
||||||
@@ -186,7 +162,6 @@ module('Integration | Component | dashboard/overview', function (hooks) {
|
|||||||
@secretsEngines={{this.secretsEngines}}
|
@secretsEngines={{this.secretsEngines}}
|
||||||
@vaultConfiguration={{this.vaultConfiguration}}
|
@vaultConfiguration={{this.vaultConfiguration}}
|
||||||
@replication={{this.replication}}
|
@replication={{this.replication}}
|
||||||
@license={{this.license}}
|
|
||||||
@refreshModel={{this.refreshModel}} />`
|
@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('quick-actions')).exists();
|
||||||
assert.dom(DASHBOARD.cardName('configuration-details')).exists();
|
assert.dom(DASHBOARD.cardName('configuration-details')).exists();
|
||||||
assert.dom(DASHBOARD.cardName('replication')).doesNotExist();
|
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 () {
|
module('learn more card', function () {
|
||||||
@@ -238,7 +213,6 @@ module('Integration | Component | dashboard/overview', function (hooks) {
|
|||||||
<Dashboard::Overview
|
<Dashboard::Overview
|
||||||
@version={{this.version}}
|
@version={{this.version}}
|
||||||
@isRootNamespace={{this.isRootNamespace}}
|
@isRootNamespace={{this.isRootNamespace}}
|
||||||
@license={{this.license}}
|
|
||||||
@secretsEngines={{this.secretsEngines}}
|
@secretsEngines={{this.secretsEngines}}
|
||||||
@vaultConfiguration={{this.vaultConfiguration}}
|
@vaultConfiguration={{this.vaultConfiguration}}
|
||||||
@replication={{this.replication}}
|
@replication={{this.replication}}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
destructureClientCounts,
|
destructureClientCounts,
|
||||||
namespaceArrayToObject,
|
namespaceArrayToObject,
|
||||||
sortMonthsByTimestamp,
|
sortMonthsByTimestamp,
|
||||||
|
setStartTimeQuery,
|
||||||
} from 'core/utils/client-count-utils';
|
} from 'core/utils/client-count-utils';
|
||||||
import { LICENSE_START } from 'vault/mirage/handlers/clients';
|
import { LICENSE_START } from 'vault/mirage/handlers/clients';
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +28,9 @@ used to normalize the sys/counters/activity response in the clients/activity
|
|||||||
serializer. these functions are tested individually here, instead of all at once
|
serializer. these functions are tested individually here, instead of all at once
|
||||||
in a serializer test for easier debugging
|
in a serializer test for easier debugging
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// TODO refactor tests into a module for each util method, then make each assertion its separate tests
|
||||||
|
|
||||||
module('Integration | Util | client count utils', function (hooks) {
|
module('Integration | Util | client count utils', function (hooks) {
|
||||||
setupTest(hooks);
|
setupTest(hooks);
|
||||||
|
|
||||||
@@ -372,4 +376,35 @@ module('Integration | Util | client count utils', function (hooks) {
|
|||||||
'it formats combined data for monthly namespaces_by_key spanning upgrade to 1.10'
|
'it formats combined data for monthly namespaces_by_key spanning upgrade to 1.10'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('setStartTimeQuery: it returns start time query for activity log', async function (assert) {
|
||||||
|
assert.expect(6);
|
||||||
|
const apiPath = 'sys/internal/counters/config';
|
||||||
|
assert.strictEqual(setStartTimeQuery(true, {}), null, `it returns null if no permission to ${apiPath}`);
|
||||||
|
assert.strictEqual(
|
||||||
|
setStartTimeQuery(false, {}),
|
||||||
|
null,
|
||||||
|
`it returns null for community edition and no permission to ${apiPath}`
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
setStartTimeQuery(true, { billingStartTimestamp: new Date('2022-06-08T00:00:00Z') }),
|
||||||
|
1654646400,
|
||||||
|
'it returns unix time if enterprise and billing_start_timestamp exists'
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
setStartTimeQuery(false, { billingStartTimestamp: new Date('0001-01-01T00:00:00Z') }),
|
||||||
|
null,
|
||||||
|
'it returns null time for community edition even if billing_start_timestamp exists'
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
setStartTimeQuery(false, { foo: 'bar' }),
|
||||||
|
null,
|
||||||
|
'it returns null if billing_start_timestamp key does not exist'
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
setStartTimeQuery(false, undefined),
|
||||||
|
null,
|
||||||
|
'fails gracefully if no config model is passed'
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user