diff --git a/changelog/27559.txt b/changelog/27559.txt new file mode 100644 index 000000000000..a9afccdc98d5 --- /dev/null +++ b/changelog/27559.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: Remove deprecated `current_billing_period` from dashboard activity log request +``` diff --git a/ui/app/adapters/clients/activity.js b/ui/app/adapters/clients/activity.js index 67c4a7478695..3e9455228e94 100644 --- a/ui/app/adapters/clients/activity.js +++ b/ui/app/adapters/clients/activity.js @@ -5,6 +5,7 @@ import ApplicationAdapter from '../application'; import { formatDateObject } from 'core/utils/client-count-utils'; +import { debug } from '@ember/debug'; export default class ActivityAdapter extends ApplicationAdapter { // javascript localizes new Date() objects but all activity log data is stored in UTC @@ -33,4 +34,12 @@ export default class ActivityAdapter extends ApplicationAdapter { }); } } + + urlForFindRecord(id) { + // debug reminder so model is stored in Ember data with the same id for consistency + if (id !== 'clients/activity') { + debug(`findRecord('clients/activity') should pass 'clients/activity' as the id, you passed: '${id}'`); + } + return `${this.buildURL()}/internal/counters/activity`; + } } diff --git a/ui/app/components/clients/no-data.hbs b/ui/app/components/clients/no-data.hbs index b6fa080dcd8d..113c1e33c733 100644 --- a/ui/app/components/clients/no-data.hbs +++ b/ui/app/components/clients/no-data.hbs @@ -3,13 +3,13 @@ SPDX-License-Identifier: BUSL-1.1 ~}} -{{#if (eq @config.enabled "On")}} +{{#if (or @config.reportingEnabled (eq @config.enabled "On"))}} -{{else}} +{{else if @config}} {{/if}} +{{else}} + {{/if}} \ No newline at end of file diff --git a/ui/app/components/dashboard/client-count-card.hbs b/ui/app/components/dashboard/client-count-card.hbs index 2b9b4fa32026..b904566483ef 100644 --- a/ui/app/components/dashboard/client-count-card.hbs +++ b/ui/app/components/dashboard/client-count-card.hbs @@ -14,10 +14,10 @@
- {{#if this.hasActivity}} - {{#if this.fetchClientActivity.isRunning}} - - {{else}} + {{#if this.fetchClientActivity.isRunning}} + + {{else}} + {{#if this.activityData}}
@@ -34,15 +34,13 @@ {{on "click" (perform this.fetchClientActivity)}} data-test-refresh /> - + Updated {{date-format this.updatedAt "MMM d yyyy, h:mm:ss aaa" withTimeZone=true}}
+ {{else}} + {{/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 }} - {{/if}} \ No newline at end of file diff --git a/ui/app/components/dashboard/client-count-card.js b/ui/app/components/dashboard/client-count-card.js index 51c2d174ddc4..6b613bfe53ad 100644 --- a/ui/app/components/dashboard/client-count-card.js +++ b/ui/app/components/dashboard/client-count-card.js @@ -23,7 +23,7 @@ export default class DashboardClientCountCard extends Component { @service store; @tracked activityData = null; - @tracked hasActivity = false; + @tracked activityConfig = null; @tracked updatedAt = null; constructor() { @@ -53,11 +53,10 @@ export default class DashboardClientCountCard extends Component { this.updatedAt = timestamp.now().toISOString(); try { - this.activityData = yield this.store.queryRecord('clients/activity', { - current_billing_period: true, - }); - this.hasActivity = this.activityData.id === 'no-data' ? false : true; + this.activityData = yield this.store.findRecord('clients/activity', 'clients/activity'); } catch (error) { + // used for rendering the "No data" empty state, swallow any errors requesting config data + this.activityConfig = yield this.store.queryRecord('clients/config', {}).catch(() => null); this.error = error; } } diff --git a/ui/app/models/clients/config.js b/ui/app/models/clients/config.js index 69288e23fb76..26c1663a9740 100644 --- a/ui/app/models/clients/config.js +++ b/ui/app/models/clients/config.js @@ -35,8 +35,10 @@ export default class ClientsConfigModel extends Model { @attr('number') minimumRetentionMonths; + // refers specifically to the activitylog and will always be on for enterprise @attr('string') enabled; + // reporting_enabled is for automated reporting and only true of the customer hasn’t opted-out of automated license reporting @attr('boolean') reportingEnabled; @attr('date') billingStartTimestamp; diff --git a/ui/mirage/handlers/clients.js b/ui/mirage/handlers/clients.js index a8e08e81cba2..1ac7659833f1 100644 --- a/ui/mirage/handlers/clients.js +++ b/ui/mirage/handlers/clients.js @@ -215,9 +215,9 @@ 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 + if (!start_time && !end_time) { + // if there are no date query params, the activity log default behavior + // queries from the builtin license start timestamp to the current month start_time = LICENSE_START.toISOString(); end_time = STATIC_NOW.toISOString(); } diff --git a/ui/tests/acceptance/dashboard-test.js b/ui/tests/acceptance/dashboard-test.js index aec0a89ec7f4..d1119d4df25f 100644 --- a/ui/tests/acceptance/dashboard-test.js +++ b/ui/tests/acceptance/dashboard-test.js @@ -402,7 +402,7 @@ module('Acceptance | landing page dashboard', function (hooks) { assert.true(version.isEnterprise, 'version is enterprise'); assert.strictEqual(currentURL(), '/vault/dashboard'); assert.dom(DASHBOARD.cardName('client-count')).exists(); - const response = await this.store.peekRecord('clients/activity', 'some-activity-id'); + const response = await this.store.findRecord('clients/activity', 'clients/activity'); assert.dom('[data-test-client-count-title]').hasText('Client count'); 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])); diff --git a/ui/tests/integration/components/clients/no-data-test.js b/ui/tests/integration/components/clients/no-data-test.js new file mode 100644 index 000000000000..7605d24d6a85 --- /dev/null +++ b/ui/tests/integration/components/clients/no-data-test.js @@ -0,0 +1,93 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'vault/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { GENERAL } from 'vault/tests/helpers/general-selectors'; +import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs'; + +module('Integration | Component | clients/no-data', function (hooks) { + setupRenderingTest(hooks); + setupMirage(hooks); + + hooks.beforeEach(async function () { + this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub()); + this.store = this.owner.lookup('service:store'); + this.setConfig = async (data) => { + // the clients/config model does some funky serializing for the "enabled" param + // so stubbing the request here instead of just the model for additional coverage + this.server.get('sys/internal/counters/config', () => { + return { + request_id: '25a94b99-b49a-c4ac-cb7b-5ba0eb390a25', + data, + }; + }); + return this.store.queryRecord('clients/config', {}); + }; + this.renderComponent = async () => { + return render(hbs``); + }; + }); + + test('it renders empty state when enabled is "on"', async function (assert) { + assert.expect(2); + const data = { + enabled: 'default-enabled', + reporting_enabled: false, + }; + ``; + this.config = await this.setConfig(data); + await this.renderComponent(); + 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.'); + }); + + test('it renders empty state when reporting_enabled is true', async function (assert) { + assert.expect(2); + const data = { + enabled: 'default-disabled', + reporting_enabled: true, + }; + this.config = await this.setConfig(data); + await this.renderComponent(); + 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.'); + }); + + test('it renders empty state when reporting is fully disabled', async function (assert) { + assert.expect(2); + const data = { + enabled: 'default-disabled', + reporting_enabled: false, + }; + this.config = await this.setConfig(data); + await this.renderComponent(); + assert.dom(GENERAL.emptyStateTitle).hasText('Data tracking is disabled'); + assert + .dom(GENERAL.emptyStateMessage) + .hasText( + 'Tracking is disabled, and no data is being collected. To turn it on, edit the configuration.' + ); + }); + + test('it renders empty state when config data is not available', async function (assert) { + assert.expect(2); + this.config = null; + await this.renderComponent(); + assert.dom(GENERAL.emptyStateTitle).hasText('Activity configuration data is unavailable'); + assert + .dom(GENERAL.emptyStateMessage) + .hasText( + 'Reporting status is unknown and could be enabled or disabled. Check the Vault logs for more information.' + ); + }); +}); diff --git a/ui/tests/integration/components/dashboard/client-count-card-test.js b/ui/tests/integration/components/dashboard/client-count-card-test.js index 388de5097e20..1542018a2904 100644 --- a/ui/tests/integration/components/dashboard/client-count-card-test.js +++ b/ui/tests/integration/components/dashboard/client-count-card-test.js @@ -8,6 +8,7 @@ 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 { Response } from 'miragejs'; import sinon from 'sinon'; import { STATIC_NOW } from 'vault/mirage/handlers/clients'; import timestamp from 'core/utils/timestamp'; @@ -21,21 +22,14 @@ module('Integration | Component | dashboard/client-count-card', function (hooks) setupMirage(hooks); test('it should display client count information', async function (assert) { - sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); - assert.expect(5); + assert.expect(6); + sinon.replace(timestamp, 'now', sinon.fake.returns(STATIC_NOW)); // 1/25/24 const { months, total } = ACTIVITY_RESPONSE_STUB; const [latestMonth] = months.slice(-1); - this.server.get('sys/internal/counters/activity', (schema, req) => { + this.server.get('sys/internal/counters/activity', () => { // this assertion should be hit twice, once initially and then again clicking 'refresh' - 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, - }; + assert.true(true, 'makes request to sys/internal/counters/activity'); + return { data: ACTIVITY_RESPONSE_STUB }; }); await render(hbs``); @@ -54,6 +48,7 @@ module('Integration | Component | dashboard/client-count-card', function (hooks) latestMonth.new_clients.counts.clients, ])}` ); + assert.dom('[data-test-updated-timestamp]').hasTextContaining('Updated Jan 25 2024'); // fires second request to /activity await click('[data-test-refresh]'); @@ -65,7 +60,6 @@ module('Integration | Component | dashboard/client-count-card', function (hooks) // stubbing this unrealistic response just to test component subtext logic this.server.get('sys/internal/counters/activity', () => { return { - request_id: 'some-activity-id', data: { by_namespace: [], months: [], total: {} }, }; }); @@ -75,19 +69,48 @@ module('Integration | Component | dashboard/client-count-card', function (hooks) assert.dom(CLIENT_COUNT.statText('New')).hasText('New No new client data available. -'); }); - test('it shows empty state if no activity data', async function (assert) { + test('it shows empty state if no activity data and reporting is enabled', 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); + assert.expect(4); this.server.get('sys/internal/counters/activity', () => { assert.true(true, 'makes request to sys/internal/counters/activity'); return { data: {} }; }); - + this.server.get('sys/internal/counters/config', () => { + assert.true(true, 'makes request to sys/internal/counters/config'); + return { + request_id: '25a94b99-b49a-c4ac-cb7b-5ba0eb390a25', + data: { reporting_enabled: true }, + }; + }); await render(hbs``); 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.'); }); + + test('it shows empty state if no activity data and config data is unavailable', async function (assert) { + assert.expect(4); + this.server.get('sys/internal/counters/activity', () => { + assert.true(true, 'makes request to sys/internal/counters/activity'); + return { data: {} }; + }); + this.server.get('sys/internal/counters/config', () => { + assert.true(true, 'makes request to sys/internal/counters/config'); + return new Response( + 403, + { 'Content-Type': 'application/json' }, + JSON.stringify({ errors: ['permission denied'] }) + ); + }); + await render(hbs``); + assert.dom(GENERAL.emptyStateTitle).hasText('Activity configuration data is unavailable'); + assert + .dom(GENERAL.emptyStateMessage) + .hasText( + 'Reporting status is unknown and could be enabled or disabled. Check the Vault logs for more information.' + ); + }); });