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');
+ });
+});