diff --git a/ui/app/components/dashboard/client-count-card.js b/ui/app/components/dashboard/client-count-card.js new file mode 100644 index 000000000000..7b459cdb417a --- /dev/null +++ b/ui/app/components/dashboard/client-count-card.js @@ -0,0 +1,62 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +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 { inject as service } from '@ember/service'; + +/** + * @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 + */ + +export default class DashboardClientCountCard extends Component { + @service store; + + @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; + } + + @task + @waitFor + *fetchClientActivity() { + this.updatedAt = timestamp.now().toISOString(); + // only make the network request if we have a start_time + if (!this.licenseStartTime) return {}; + try { + this.activityData = yield this.store.queryRecord('clients/activity', { + start_time: { timestamp: this.licenseStartTime }, + end_time: { timestamp: this.updatedAt }, + }); + this.noActivityData = this.activityData.activity.id === 'no-data' ? true : false; + } catch (error) { + this.error = error; + } + } +} diff --git a/ui/app/routes/vault/cluster/dashboard.js b/ui/app/routes/vault/cluster/dashboard.js index 4a2ce2cf8449..52d487fe5fe3 100644 --- a/ui/app/routes/vault/cluster/dashboard.js +++ b/ui/app/routes/vault/cluster/dashboard.js @@ -8,8 +8,10 @@ import { inject as service } from '@ember/service'; import { hash } from 'rsvp'; // eslint-disable-next-line ember/no-mixins import ClusterRoute from 'vault/mixins/cluster-route'; + export default class VaultClusterDashboardRoute extends Route.extend(ClusterRoute) { @service store; + @service version; async getVaultConfiguration() { try { @@ -21,12 +23,22 @@ export default class VaultClusterDashboardRoute extends Route.extend(ClusterRout } } + async getLicense() { + try { + return await this.store.queryRecord('license', {}); + } catch (e) { + return null; + } + } + model() { const vaultConfiguration = this.getVaultConfiguration(); return hash({ vaultConfiguration, secretsEngines: this.store.query('secret-engine', {}), + version: this.version, + license: this.getLicense(), }); } } diff --git a/ui/app/styles/helper-classes/spacing.scss b/ui/app/styles/helper-classes/spacing.scss index 8704ab3ff0a3..22aa94b3f268 100644 --- a/ui/app/styles/helper-classes/spacing.scss +++ b/ui/app/styles/helper-classes/spacing.scss @@ -26,6 +26,10 @@ padding-right: $spacing-s; } +.has-padding-xxs { + padding: $spacing-xxs; +} + .has-padding-m { padding: $spacing-m; } diff --git a/ui/app/templates/components/clients/dashboard.hbs b/ui/app/templates/components/clients/dashboard.hbs index 214914cfd99e..eb236d70cbb1 100644 --- a/ui/app/templates/components/clients/dashboard.hbs +++ b/ui/app/templates/components/clients/dashboard.hbs @@ -27,25 +27,7 @@ {{this.versionText.description}}

{{#if this.noActivityData}} - {{#if (eq @model.config.enabled "On")}} - - {{else}} - - {{#if @model.config.canEdit}} -

- - Go to configuration - -

- {{/if}} -
- {{/if}} + {{else if this.errorObject}} {{else}} diff --git a/ui/app/templates/components/clients/no-data.hbs b/ui/app/templates/components/clients/no-data.hbs new file mode 100644 index 000000000000..074677a74c0d --- /dev/null +++ b/ui/app/templates/components/clients/no-data.hbs @@ -0,0 +1,19 @@ +{{#if (eq @config.enabled "On")}} + +{{else}} + + {{#if @config.canEdit}} +

+ + Go to configuration + +

+ {{/if}} +
+{{/if}} \ No newline at end of file diff --git a/ui/app/templates/components/dashboard/client-count-card.hbs b/ui/app/templates/components/dashboard/client-count-card.hbs new file mode 100644 index 000000000000..2291c4f8930d --- /dev/null +++ b/ui/app/templates/components/dashboard/client-count-card.hbs @@ -0,0 +1,60 @@ +
+

+ Client count +

+ +
+ Details +
+
+ +
+ +{{#if this.noActivityData}} + {{! This will likely not be show since the client activity api was changed to always return data. In the past it + would return no activity data. Adding this empty state here to match the current client count behavior }} + +{{else}} +
+ {{#if this.fetchClientActivity.isRunning}} + + {{else}} + + + {{/if}} +
+ + {{#unless this.fetchClientActivity.isRunning}} +
+ + + Updated + {{date-format this.updatedAt "MMM dd, yyyy HH:mm:SS"}} + +
+ {{/unless}} +{{/if}} \ No newline at end of file diff --git a/ui/app/templates/vault/cluster/dashboard.hbs b/ui/app/templates/vault/cluster/dashboard.hbs index 04cbd586e602..6526ad9f2064 100644 --- a/ui/app/templates/vault/cluster/dashboard.hbs +++ b/ui/app/templates/vault/cluster/dashboard.hbs @@ -1,6 +1,11 @@
+ {{#if (and @model.version.isEnterprise @model.license)}} + + + + {{/if}} diff --git a/ui/tests/integration/components/dashboard/client-count-card-test.js b/ui/tests/integration/components/dashboard/client-count-card-test.js new file mode 100644 index 000000000000..f749c6bcfd36 --- /dev/null +++ b/ui/tests/integration/components/dashboard/client-count-card-test.js @@ -0,0 +1,142 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; +import { render, click } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import timestamp from 'core/utils/timestamp'; +import { parseAPITimestamp } from 'core/utils/date-formatters'; + +module('Integration | Component | dashboard/client-count-card', function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(function () { + this.license = { + startTime: '2018-04-03T14:15:30', + }; + }); + + test('it should display client count information', async function (assert) { + this.server.get('sys/internal/counters/activity', () => { + return { + request_id: 'some-activity-id', + data: { + months: [ + { + timestamp: '2023-08-01T00:00:00-07:00', + counts: {}, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', + counts: {}, + mounts: [{ mount_path: 'auth/up2/', counts: {} }], + }, + ], + new_clients: { + counts: { + clients: 12, + }, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', + counts: { + clients: 12, + }, + mounts: [{ mount_path: 'auth/up2/', counts: {} }], + }, + ], + }, + }, + ], + total: { + clients: 300417, + entity_clients: 73150, + non_entity_clients: 227267, + }, + }, + }; + }); + + 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 (Apr 2018 - ${parseAPITimestamp( + timestamp.now().toISOString(), + 'MMM yyyy' + )}).` + ); + assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('300,417'); + 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('12'); + this.server.get('sys/internal/counters/activity', () => { + return { + request_id: 'some-activity-id', + data: { + months: [ + { + timestamp: '2023-09-01T00:00:00-07:00', + counts: {}, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', + counts: {}, + mounts: [{ mount_path: 'auth/up2/', counts: {} }], + }, + ], + new_clients: { + counts: { + clients: 5, + }, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', + counts: { + clients: 12, + }, + mounts: [{ mount_path: 'auth/up2/', counts: {} }], + }, + ], + }, + }, + ], + total: { + clients: 120, + entity_clients: 100, + non_entity_clients: 100, + }, + }, + }; + }); + await click('[data-test-refresh]'); + 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 (Apr 2018 - ${parseAPITimestamp( + timestamp.now().toISOString(), + 'MMM yyyy' + )}).` + ); + assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('120'); + 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('5'); + }); +});