Skip to content

Commit

Permalink
UI: wrap client count card in permission conditional (#26848)
Browse files Browse the repository at this point in the history
* consistent timestamp format

* wrap client count card in permissions

* add test

* add changelog

* move tests into module, add more!

* final test cleanup, stub permissions manually without helper

* use current_billing_period for dashboard, add tests

* update mirage to handle new client param

* Update ui/app/components/dashboard/client-count-card.js
  • Loading branch information
hellobontempo authored May 7, 2024
1 parent 57e6795 commit 1e8eefa
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 185 deletions.
3 changes: 3 additions & 0 deletions changelog/26848.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
ui: Hide dashboard client count card if user does not have permission to view clients.
```
5 changes: 5 additions & 0 deletions ui/app/adapters/clients/activity.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export default class ActivityAdapter extends ApplicationAdapter {
// create date object from user's input using Date.UTC() then send to backend as unix
// time params from the backend are formatted as a zulu timestamp
formatQueryParams(queryParams) {
if (queryParams?.current_billing_period) {
// { current_billing_period: true } automatically queries the activity log
// from the builtin license start timestamp to the current month
return queryParams;
}
let { start_time, end_time } = queryParams;
start_time = start_time.timestamp || formatDateObject(start_time);
end_time = end_time.timestamp || formatDateObject(end_time, true);
Expand Down
10 changes: 5 additions & 5 deletions ui/app/components/dashboard/client-count-card.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@

<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}}
{{#if this.hasActivity}}
{{#if this.fetchClientActivity.isRunning}}
<VaultLogoSpinner />
{{else}}
Expand All @@ -44,5 +40,9 @@
</small>
</div>
{{/if}}
{{else}}
{{! This will likely never show since the clients activity api has 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={{hash enabled="On"}} />
{{/if}}
</Hds::Card::Container>
35 changes: 10 additions & 25 deletions ui/app/components/dashboard/client-count-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,22 @@ 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';
import { parseAPITimestamp } from 'core/utils/date-formatters';

/**
* @module DashboardClientCountCard
* DashboardClientCountCard component are used to display total and new client count information
*
* @example
*
* <Dashboard::ClientCountCard @isEnterprise={{@version.isEnterprise}} />
*
* @param {boolean} isEnterprise - used for setting the start time for the activity log query
* <Dashboard::ClientCountCard />
*/

export default class DashboardClientCountCard extends Component {
@service store;

clientConfig = null;
licenseStartTime = null;
@tracked activityData = null;
@tracked updatedAt = timestamp.now().toISOString();
@tracked hasActivity = false;
@tracked updatedAt = null;

constructor() {
super(...arguments);
Expand All @@ -41,12 +36,11 @@ export default class DashboardClientCountCard extends Component {
}

get statSubText() {
const format = (date) => dateFormat([date, 'MMM yyyy'], {});
return this.licenseStartTime
const format = (date) => parseAPITimestamp(date, 'MMM yyyy');
const { startTime, endTime } = this.activityData;
return startTime && endTime
? {
total: `The number of clients in this billing period (${format(this.licenseStartTime)} - ${format(
this.updatedAt
)}).`,
total: `The number of clients in this billing period (${format(startTime)} - ${format(endTime)}).`,
new: 'The number of clients new to Vault in the current month.',
}
: { total: 'No total client data available.', new: 'No new client data available.' };
Expand All @@ -58,20 +52,11 @@ export default class DashboardClientCountCard extends Component {
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 {
this.activityData = yield this.store.queryRecord('clients/activity', {
start_time: { timestamp: this.licenseStartTime },
end_time: { timestamp: this.updatedAt },
current_billing_period: true,
});
this.noActivityData = this.activityData.activity.id === 'no-data' ? true : false;
this.hasActivity = this.activityData.id === 'no-data' ? false : true;
} catch (error) {
this.error = error;
}
Expand Down
8 changes: 4 additions & 4 deletions ui/app/components/dashboard/overview.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
<div class="is-flex-row gap-24">
{{#if (and @version.isEnterprise @isRootNamespace)}}
<div class="is-flex-column is-flex-1 gap-24">
<Dashboard::ClientCountCard @isEnterprise={{@version.isEnterprise}} />
{{#if
(and @isRootNamespace (has-permission "status" routeParams="replication") (not (is-empty-value @replication)))
}}
{{#if (has-permission "clients" routeParams="activity")}}
<Dashboard::ClientCountCard />
{{/if}}
{{#if (and (has-permission "status" routeParams="replication") (not (is-empty-value @replication)))}}
<Dashboard::ReplicationCard
@replication={{@replication}}
@version={{@version}}
Expand Down
2 changes: 1 addition & 1 deletion ui/app/components/dashboard/replication-card.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
/>
<small class="has-left-margin-xs has-text-grey">
Updated
{{date-format @updatedAt "MMM dd, yyyy hh:mm:ss"}}
{{date-format @updatedAt "MMM d yyyy, h:mm:ss aaa" withTimeZone=true}}
</small>
</div>
{{else}}
Expand Down
6 changes: 6 additions & 0 deletions ui/mirage/handlers/clients.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ export default function (server) {

server.get('/sys/internal/counters/activity', (schema, req) => {
let { start_time, end_time } = req.queryParams;
if (req.queryParams.current_billing_period) {
// { current_billing_period: true } automatically queries the activity log
// from the builtin license start timestamp to the current month
start_time = LICENSE_START.toISOString();
end_time = STATIC_NOW.toISOString();
}
// backend returns a timestamp if given unix time, so first convert to timestamp string here
if (!start_time.includes('T')) start_time = fromUnixTime(start_time).toISOString();
if (!end_time.includes('T')) end_time = fromUnixTime(end_time).toISOString();
Expand Down
68 changes: 34 additions & 34 deletions ui/tests/integration/components/dashboard/client-count-card-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,49 +9,44 @@ import { render, click } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupMirage } from 'ember-cli-mirage/test-support';
import sinon from 'sinon';
import { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients';
import { 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';
import { GENERAL } from 'vault/tests/helpers/general-selectors';

module('Integration | Component | dashboard/client-count-card', function (hooks) {
setupRenderingTest(hooks);
setupMirage(hooks);

test('it should display client count information', async function (assert) {
sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW));
assert.expect(6);
assert.expect(5);
const { months, total } = ACTIVITY_RESPONSE_STUB;
const [latestMonth] = months.slice(-1);
this.server.get('sys/internal/counters/activity', () => {
this.server.get('sys/internal/counters/activity', (schema, req) => {
// this assertion should be hit twice, once initially and then again clicking 'refresh'
assert.true(true, 'makes request to sys/internal/counters/activity');
assert.propEqual(
req.queryParams,
{ current_billing_period: 'true' },
'it makes request to sys/internal/counters/activity with builtin license start time'
);
return {
request_id: 'some-activity-id',
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 @isEnterprise={{true}} />`);
await render(hbs`<Dashboard::ClientCountCard />`);
assert.dom('[data-test-client-count-title]').hasText('Client count');
assert
.dom(CLIENT_COUNT.statText('Total'))
.hasText(
`Total The number of clients in this billing period (Jul 2023 - Jan 2024). ${formatNumber([
`Total The number of clients in this billing period (Aug 2023 - Sep 2023). ${formatNumber([
total.clients,
])}`
);

assert
.dom(CLIENT_COUNT.statText('New'))
.hasText(
Expand All @@ -64,30 +59,35 @@ module('Integration | Component | dashboard/client-count-card', function (hooks)
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');
test('it shows no data subtext if no start or end timestamp', async function (assert) {
assert.expect(2);
// as far as I know, responses will always have a start/end time
// stubbing this unrealistic response just to test component subtext logic
this.server.get('sys/internal/counters/activity', () => {
return {
request_id: 'some-config-id',
data: {
billing_start_timestamp: '0001-01-01T00:00:00Z',
},
request_id: 'some-activity-id',
data: { by_namespace: [], months: [], total: {} },
};
});

await render(hbs`<Dashboard::ClientCountCard @isEnterprise={{false}} />`);
await render(hbs`<Dashboard::ClientCountCard />`);
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]');
test('it shows empty state if no activity data', async function (assert) {
// the activity response has changed and now should ALWAYS return something
// but adding this test until we update the adapter to reflect that
assert.expect(3);
this.server.get('sys/internal/counters/activity', () => {
assert.true(true, 'makes request to sys/internal/counters/activity');
return { data: {} };
});

await render(hbs`<Dashboard::ClientCountCard />`);
assert.dom(GENERAL.emptyStateTitle).hasText('No data received');
assert
.dom(GENERAL.emptyStateMessage)
.hasText('Tracking is turned on and Vault is gathering data. It should appear here within 30 minutes.');
});
});
Loading

0 comments on commit 1e8eefa

Please sign in to comment.