Skip to content

Commit

Permalink
UI: [VAULT-17041] Client count card (#22323)
Browse files Browse the repository at this point in the history
Co-authored-by: Angel Garbarino <[email protected]>
  • Loading branch information
kiannaquach and Monkeychip authored Aug 15, 2023
1 parent 49ecc8b commit 95d415a
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 19 deletions.
62 changes: 62 additions & 0 deletions ui/app/components/dashboard/client-count-card.js
Original file line number Diff line number Diff line change
@@ -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
* <Dashboard::ClientCountCard @license={{@model.license}} />
* ```
* @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;
}
}
}
12 changes: 12 additions & 0 deletions ui/app/routes/vault/cluster/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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(),
});
}
}
4 changes: 4 additions & 0 deletions ui/app/styles/helper-classes/spacing.scss
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
padding-right: $spacing-s;
}

.has-padding-xxs {
padding: $spacing-xxs;
}

.has-padding-m {
padding: $spacing-m;
}
Expand Down
20 changes: 1 addition & 19 deletions ui/app/templates/components/clients/dashboard.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -27,25 +27,7 @@
{{this.versionText.description}}
</p>
{{#if this.noActivityData}}
{{#if (eq @model.config.enabled "On")}}
<EmptyState
@title="No data received {{if this.dateRangeMessage this.dateRangeMessage}}"
@message="Tracking is turned on and Vault is gathering data. It should appear here within 30 minutes."
/>
{{else}}
<EmptyState
@title="Data tracking is disabled"
@message="Tracking is disabled, and no data is being collected. To turn it on, edit the configuration."
>
{{#if @model.config.canEdit}}
<p>
<LinkTo @route="vault.cluster.clients.config">
Go to configuration
</LinkTo>
</p>
{{/if}}
</EmptyState>
{{/if}}
<Clients::NoData @config={{@model.config}} @dateRangeMessage={{this.dateRangeMessage}} />
{{else if this.errorObject}}
<Clients::Error @error={{this.errorObject}} />
{{else}}
Expand Down
19 changes: 19 additions & 0 deletions ui/app/templates/components/clients/no-data.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{{#if (eq @config.enabled "On")}}
<EmptyState
@title="No data received {{if @dateRangeMessage @dateRangeMessage}}"
@message="Tracking is turned on and Vault is gathering data. It should appear here within 30 minutes."
/>
{{else}}
<EmptyState
@title="Data tracking is disabled"
@message="Tracking is disabled, and no data is being collected. To turn it on, edit the configuration."
>
{{#if @config.canEdit}}
<p>
<LinkTo @route="vault.cluster.clients.config">
Go to configuration
</LinkTo>
</p>
{{/if}}
</EmptyState>
{{/if}}
60 changes: 60 additions & 0 deletions ui/app/templates/components/dashboard/client-count-card.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<div class="is-flex-between">
<h3 class="title is-4 has-bottom-margin-xxs" data-test-client-count-title>
Client count
</h3>

<div>
<Hds::Link::Inline @route="vault.cluster.clients.dashboard" class="is-no-underline">Details</Hds::Link::Inline>
</div>
</div>

<hr class="has-background-gray-100" />

{{#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 }}
<Clients::NoData @config={{this.clientConfig}} />
{{else}}
<div class="is-grid grid-2-columns grid-gap-2 has-top-margin-m grid-align-items-start is-flex-v-centered">
{{#if this.fetchClientActivity.isRunning}}
<VaultLogoSpinner />
{{else}}
<StatText
@label="Total"
@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"
/>
{{/if}}
</div>

{{#unless this.fetchClientActivity.isRunning}}
<div class="has-top-margin-l is-flex-center">
<Hds::Button
@text="Refresh"
@isIconOnly={{true}}
@color="tertiary"
@icon="sync"
disabled={{this.fetchClientActivity.isRunning}}
class="has-padding-xxs"
{{on "click" (perform this.fetchClientActivity)}}
data-test-refresh
/>
<small class="has-left-margin-xs has-text-grey">
Updated
{{date-format this.updatedAt "MMM dd, yyyy HH:mm:SS"}}
</small>
</div>
{{/unless}}
{{/if}}
5 changes: 5 additions & 0 deletions ui/app/templates/vault/cluster/dashboard.hbs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<Dashboard::VaultVersionTitle />

<div class="is-grid grid-2-columns grid-gap-2 has-bottom-margin-m grid-align-items-start">
{{#if (and @model.version.isEnterprise @model.license)}}
<Hds::Card::Container @hasBorder={{true}} class="has-padding-l has-bottom-padding-m is-flex-column is-flex-half">
<Dashboard::ClientCountCard @license={{@model.license}} />
</Hds::Card::Container>
{{/if}}
<Hds::Card::Container @hasBorder={{true}} class="has-padding-l is-flex-column is-flex-half">
<Dashboard::SecretsEnginesCard @secretsEngines={{@model.secretsEngines}} />
</Hds::Card::Container>
Expand Down
142 changes: 142 additions & 0 deletions ui/tests/integration/components/dashboard/client-count-card-test.js
Original file line number Diff line number Diff line change
@@ -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`<Dashboard::ClientCountCard @license={{this.license}} />`);
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');
});
});

0 comments on commit 95d415a

Please sign in to comment.