From 266ea693ccdec27718c17b7ee354cc6e19d9f305 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com> Date: Wed, 31 Jul 2024 12:35:11 -0500 Subject: [PATCH] UI: remove initial date from client counts (#27816) --- changelog/27816.txt | 3 + ui/app/adapters/clients/activity.js | 41 ++++--- ui/app/components/clients/activity.ts | 14 +-- ui/app/components/clients/date-range.hbs | 4 +- ui/app/components/clients/date-range.ts | 4 +- ui/app/components/clients/page/counts.hbs | 4 +- ui/app/components/clients/page/counts.ts | 30 ++--- ui/app/components/clients/page/overview.hbs | 4 +- ui/app/routes/vault/cluster/clients/counts.ts | 70 +++++++---- ui/lib/core/addon/utils/client-count-utils.ts | 11 -- ui/tests/acceptance/clients/counts-test.js | 86 ++++++++++++- .../clients/counts/overview-test.js | 2 +- .../helpers/clients/client-count-selectors.ts | 1 - .../components/clients/date-range-test.js | 45 ++++++- .../components/clients/page/counts-test.js | 115 ++++++++++++------ .../utils/client-count-utils-test.js | 35 ------ .../unit/adapters/clients-activity-test.js | 30 +++-- 17 files changed, 324 insertions(+), 175 deletions(-) create mode 100644 changelog/27816.txt diff --git a/changelog/27816.txt b/changelog/27816.txt new file mode 100644 index 0000000000..92dd2d7bb9 --- /dev/null +++ b/changelog/27816.txt @@ -0,0 +1,3 @@ +```release-note:improvement +ui: remove initial start/end parameters on the activity call for client counts dashboard. +``` \ No newline at end of file diff --git a/ui/app/adapters/clients/activity.js b/ui/app/adapters/clients/activity.js index 3e9455228e..419d951fab 100644 --- a/ui/app/adapters/clients/activity.js +++ b/ui/app/adapters/clients/activity.js @@ -8,31 +8,42 @@ import { formatDateObject } from 'core/utils/client-count-utils'; import { debug } from '@ember/debug'; export default class ActivityAdapter extends ApplicationAdapter { + formatTimeParam(dateObj, isEnd = false) { + let formatted; + if (dateObj) { + try { + const iso = dateObj.timestamp || formatDateObject(dateObj, isEnd); + formatted = iso; + } catch (e) { + // carry on + } + } + return formatted; + } // javascript localizes new Date() objects but all activity log data is stored in UTC // 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; + const query = {}; + const start = this.formatTimeParam(queryParams?.start_time); + const end = this.formatTimeParam(queryParams?.end_time, true); + if (start) { + query.start_time = start; } - let { start_time, end_time } = queryParams; - start_time = start_time.timestamp || formatDateObject(start_time); - end_time = end_time.timestamp || formatDateObject(end_time, true); - return { start_time, end_time }; + if (end) { + query.end_time = end; + } + return query; } queryRecord(store, type, query) { const url = `${this.buildURL()}/internal/counters/activity`; const queryParams = this.formatQueryParams(query); - if (queryParams) { - return this.ajax(url, 'GET', { data: queryParams }).then((resp) => { - const response = resp || {}; - response.id = response.request_id || 'no-data'; - return response; - }); - } + return this.ajax(url, 'GET', { data: queryParams }).then((resp) => { + const response = resp || {}; + response.id = response.request_id || 'no-data'; + return response; + }); } urlForFindRecord(id) { diff --git a/ui/app/components/clients/activity.ts b/ui/app/components/clients/activity.ts index f22134b94a..10a419b2e9 100644 --- a/ui/app/components/clients/activity.ts +++ b/ui/app/components/clients/activity.ts @@ -7,7 +7,7 @@ // contains getters that filter and extract data from activity model for use in charts import Component from '@glimmer/component'; -import { isSameMonth, fromUnixTime } from 'date-fns'; +import { isSameMonth } from 'date-fns'; import { parseAPITimestamp } from 'core/utils/date-formatters'; import { calculateAverage } from 'vault/utils/chart-helpers'; import { filterVersionHistory, hasMountsKey, hasNamespacesKey } from 'core/utils/client-count-utils'; @@ -24,8 +24,8 @@ import type { interface Args { activity: ClientsActivityModel; versionHistory: ClientsVersionHistoryModel[]; - startTimestamp: number; - endTimestamp: number; + startTimestamp: string; + endTimestamp: string; namespace: string; mountPath: string; } @@ -40,14 +40,6 @@ export default class ClientsActivityComponent extends Component { return calculateAverage(data, key); }; - get startTimeISO() { - return fromUnixTime(this.args.startTimestamp).toISOString(); - } - - get endTimeISO() { - return fromUnixTime(this.args.endTimestamp).toISOString(); - } - get byMonthActivityData() { const { activity, namespace } = this.args; return namespace ? this.filteredActivityByMonth : activity.byMonth; diff --git a/ui/app/components/clients/date-range.hbs b/ui/app/components/clients/date-range.hbs index 0d5ed63634..c652c9f912 100644 --- a/ui/app/components/clients/date-range.hbs +++ b/ui/app/components/clients/date-range.hbs @@ -27,7 +27,7 @@ @text="Set date range" @icon="edit" {{on "click" (fn (mut this.showEditModal) true)}} - data-test-set-date-range + data-test-date-range-edit /> {{/if}} @@ -87,7 +87,7 @@ data-test-date-range-validation >{{this.validationError}} {{/if}} - {{#if this.useDefaultDates}} + {{#if (and this.version.isEnterprise this.useDefaultDates)}} Dashboard will use the default date range from the API. diff --git a/ui/app/components/clients/date-range.ts b/ui/app/components/clients/date-range.ts index 4704a7c745..9e87e413cf 100644 --- a/ui/app/components/clients/date-range.ts +++ b/ui/app/components/clients/date-range.ts @@ -67,8 +67,8 @@ export default class ClientsDateRangeComponent extends Component { } get validationError() { - if (this.useDefaultDates) { - // this means we want to reset, which is fine + if (this.useDefaultDates && this.version.isEnterprise) { + // this means we want to reset, which is fine for ent only return null; } if (!this.startDate || !this.endDate) { diff --git a/ui/app/components/clients/page/counts.hbs b/ui/app/components/clients/page/counts.hbs index bb369ced1f..38b8fd840c 100644 --- a/ui/app/components/clients/page/counts.hbs +++ b/ui/app/components/clients/page/counts.hbs @@ -19,8 +19,8 @@

diff --git a/ui/app/components/clients/page/counts.ts b/ui/app/components/clients/page/counts.ts index b2cf003ddd..bd53145a54 100644 --- a/ui/app/components/clients/page/counts.ts +++ b/ui/app/components/clients/page/counts.ts @@ -6,7 +6,7 @@ import Component from '@glimmer/component'; import { service } from '@ember/service'; import { action } from '@ember/object'; -import { fromUnixTime, isSameMonth, isAfter } from 'date-fns'; +import { isSameMonth, isAfter } from 'date-fns'; import { parseAPITimestamp } from 'core/utils/date-formatters'; import { filterVersionHistory } from 'core/utils/client-count-utils'; @@ -21,11 +21,11 @@ interface Args { activity: ClientsActivityModel; activityError?: AdapterError; config: ClientsConfigModel; - endTimestamp: number; + endTimestamp: string; // ISO format mountPath: string; namespace: string; onFilterChange: CallableFunction; - startTimestamp: number; + startTimestamp: string; // ISO format versionHistory: ClientsVersionHistoryModel[]; } @@ -34,29 +34,21 @@ export default class ClientsCountsPageComponent extends Component { @service declare readonly version: VersionService; @service declare readonly store: StoreService; - get startTimestampISO() { - return this.args.startTimestamp ? fromUnixTime(this.args.startTimestamp).toISOString() : null; - } - - get endTimestampISO() { - return this.args.endTimestamp ? fromUnixTime(this.args.endTimestamp).toISOString() : null; - } - get formattedStartDate() { - return this.startTimestampISO ? parseAPITimestamp(this.startTimestampISO, 'MMMM yyyy') : null; + return this.args.startTimestamp ? parseAPITimestamp(this.args.startTimestamp, 'MMMM yyyy') : null; } // returns text for empty state message if noActivityData get dateRangeMessage() { - if (this.startTimestampISO && this.endTimestampISO) { + if (this.args.startTimestamp && this.args.endTimestamp) { const endMonth = isSameMonth( - parseAPITimestamp(this.startTimestampISO) as Date, - parseAPITimestamp(this.endTimestampISO) as Date + parseAPITimestamp(this.args.startTimestamp) as Date, + parseAPITimestamp(this.args.endTimestamp) as Date ) ? '' - : `to ${parseAPITimestamp(this.endTimestampISO, 'MMMM yyyy')}`; + : `to ${parseAPITimestamp(this.args.endTimestamp, 'MMMM yyyy')}`; // completes the message 'No data received from { dateRangeMessage }' - return `from ${parseAPITimestamp(this.startTimestampISO, 'MMMM yyyy')} ${endMonth}`; + return `from ${parseAPITimestamp(this.args.startTimestamp, 'MMMM yyyy')} ${endMonth}`; } return null; } @@ -127,9 +119,9 @@ export default class ClientsCountsPageComponent extends Component { // show banner if startTime returned from activity log (response) is after the queried startTime const { activity, config } = this.args; const activityStartDateObject = parseAPITimestamp(activity.startTime) as Date; - const queryStartDateObject = parseAPITimestamp(this.startTimestampISO) as Date; + const queryStartDateObject = parseAPITimestamp(this.args.startTimestamp) as Date; const isEnterprise = - this.startTimestampISO === config.billingStartTimestamp?.toISOString() && this.version.isEnterprise; + this.args.startTimestamp === config.billingStartTimestamp?.toISOString() && this.version.isEnterprise; const message = isEnterprise ? 'Your license start date is' : 'You requested data from'; if ( diff --git a/ui/app/components/clients/page/overview.hbs b/ui/app/components/clients/page/overview.hbs index f069423df2..e8abfc6abf 100644 --- a/ui/app/components/clients/page/overview.hbs +++ b/ui/app/components/clients/page/overview.hbs @@ -21,8 +21,8 @@ @totalClientAttribution={{this.totalClientAttribution}} @newClientAttribution={{this.newClientAttribution}} @selectedNamespace={{@namespace}} - @startTimestamp={{this.startTimeISO}} - @endTimestamp={{this.endTimeISO}} + @startTimestamp={{@startTimestamp}} + @endTimestamp={{@endTimestamp}} @responseTimestamp={{@activity.responseTimestamp}} @isHistoricalMonth={{and (not this.isCurrentMonth) (not this.isDateRange)}} @upgradesDuringActivity={{this.upgradesDuringActivity}} diff --git a/ui/app/routes/vault/cluster/clients/counts.ts b/ui/app/routes/vault/cluster/clients/counts.ts index b2322970db..4170d02c4f 100644 --- a/ui/app/routes/vault/cluster/clients/counts.ts +++ b/ui/app/routes/vault/cluster/clients/counts.ts @@ -5,8 +5,7 @@ import Route from '@ember/routing/route'; import { service } from '@ember/service'; -import timestamp from 'core/utils/timestamp'; -import { getUnixTime } from 'date-fns'; +import { fromUnixTime } from 'date-fns'; import type FlagsService from 'vault/services/flags'; import type StoreService from 'vault/services/store'; @@ -14,7 +13,6 @@ import type VersionService from 'vault/services/version'; import type { ModelFrom } from 'vault/vault/route'; import type ClientsRoute from '../clients'; import type ClientsCountsController from 'vault/controllers/vault/cluster/clients/counts'; -import { setStartTimeQuery } from 'core/utils/client-count-utils'; export interface ClientsCountsRouteParams { start_time?: string | number | undefined; @@ -39,38 +37,68 @@ export default class ClientsCountsRoute extends Route { return this.flags.fetchActivatedFlags(); } - async getActivity(start_time: number | null, end_time: number) { + /** + * This method returns the query param timestamp if it exists. If not, it returns the activity timestamp value instead. + */ + paramOrResponseTimestamp( + qpMillisString: string | number | undefined, + activityTimeStamp: string | undefined + ) { + let timestamp: string | undefined; + const millis = Number(qpMillisString); + if (!isNaN(millis)) { + timestamp = fromUnixTime(millis).toISOString(); + } + // fallback to activity timestamp only if there was no query param + if (!timestamp && activityTimeStamp) { + timestamp = activityTimeStamp; + } + return timestamp; + } + + async getActivity(params: ClientsCountsRouteParams) { let activity, activityError; - // if there is no start_time we want the user to manually choose a date - // in that case bypass the query so that the user isn't stuck viewing the activity error - if (start_time) { + // if CE without start time we want to skip the activity call + // so that the user is forced to choose a date range + if (this.version.isEnterprise || params.start_time) { + const query = { + // start and end params are optional -- if not provided, will fallback to API default + start_time: this.formatTimeQuery(params?.start_time), + end_time: this.formatTimeQuery(params?.end_time), + }; try { - activity = await this.store.queryRecord('clients/activity', { - start_time: { timestamp: start_time }, - end_time: { timestamp: end_time }, - }); + activity = await this.store.queryRecord('clients/activity', query); } catch (error) { activityError = error; } } - return { activity, activityError }; + return { + activity, + activityError, + }; + } + + // Takes the string URL param and formats it as the adapter expects it, + // if it exists and is valid + formatTimeQuery(param: string | number | undefined) { + let timeParam: { timestamp: number } | undefined; + const millis = Number(param); + if (!isNaN(millis)) { + timeParam = { timestamp: millis }; + } + return timeParam; } async model(params: ClientsCountsRouteParams) { const { config, versionHistory } = this.modelFor('vault.cluster.clients') as ModelFrom; - // only enterprise versions will have a relevant billing start date, if null users must select initial start time - const startTime = setStartTimeQuery(this.version.isEnterprise, config); - - const startTimestamp = Number(params.start_time) || startTime; - const endTimestamp = Number(params.end_time) || getUnixTime(timestamp.now()); - const { activity, activityError } = await this.getActivity(startTimestamp, endTimestamp); - + const { activity, activityError } = await this.getActivity(params); return { activity, activityError, config, - endTimestamp, - startTimestamp, + // activity.startTime corresponds to first month with data, but we want first month returned or requested + startTimestamp: this.paramOrResponseTimestamp(params?.start_time, activity?.byMonth[0]?.timestamp), + endTimestamp: this.paramOrResponseTimestamp(params?.end_time, activity?.endTime), versionHistory, }; } diff --git a/ui/lib/core/addon/utils/client-count-utils.ts b/ui/lib/core/addon/utils/client-count-utils.ts index 3d20f540e6..50a4b5a9e7 100644 --- a/ui/lib/core/addon/utils/client-count-utils.ts +++ b/ui/lib/core/addon/utils/client-count-utils.ts @@ -66,17 +66,6 @@ export const filterVersionHistory = ( return []; }; -export const setStartTimeQuery = ( - isEnterprise: boolean, - config: ClientsConfigModel | Record -) => { - // CE versions have no license and so the start time defaults to "0001-01-01T00:00:00Z" - if (isEnterprise && _hasConfig(config)) { - return getUnixTime(config.billingStartTimestamp); - } - return null; -}; - // METHODS FOR SERIALIZING ACTIVITY RESPONSE export const formatDateObject = (dateObj: { monthIdx: number; year: number }, isEnd: boolean) => { const { year, monthIdx } = dateObj; diff --git a/ui/tests/acceptance/clients/counts-test.js b/ui/tests/acceptance/clients/counts-test.js index 4c283f4b83..6db0217e10 100644 --- a/ui/tests/acceptance/clients/counts-test.js +++ b/ui/tests/acceptance/clients/counts-test.js @@ -43,19 +43,19 @@ module('Acceptance | clients | counts', function (hooks) { test('it should persist filter query params between child routes', async function (assert) { await visit('/vault/clients/counts/overview'); await click(CLIENT_COUNT.dateRange.edit); - await fillIn(CLIENT_COUNT.dateRange.editDate('start'), '2020-03'); - await fillIn(CLIENT_COUNT.dateRange.editDate('start'), '2022-02'); + await fillIn(CLIENT_COUNT.dateRange.editDate('start'), '2023-03'); + await fillIn(CLIENT_COUNT.dateRange.editDate('end'), '2023-10'); await click(GENERAL.saveButton); assert.strictEqual( currentURL(), - '/vault/clients/counts/overview?end_time=1706659200&start_time=1643673600', + '/vault/clients/counts/overview?end_time=1698710400&start_time=1677628800', 'Start and end times added as query params' ); await click(GENERAL.tab('token')); assert.strictEqual( currentURL(), - '/vault/clients/counts/token?end_time=1706659200&start_time=1643673600', + '/vault/clients/counts/token?end_time=1698710400&start_time=1677628800', 'Start and end times persist through child route change' ); @@ -81,4 +81,82 @@ module('Acceptance | clients | counts', function (hooks) { 'You must be granted permissions to view this page. Ask your administrator if you think you should have access to the /v1/sys/internal/counters/activity endpoint.' ); }); + + test('it should use the first month timestamp from default response rather than response start_time', async function (assert) { + const getCounts = () => { + return { + acme_clients: 0, + clients: 0, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 1, + }; + }; + // set to enterprise because when community the initial activity call is skipped + this.owner.lookup('service:version').type = 'enterprise'; + this.server.get('/sys/internal/counters/activity', function () { + return { + request_id: 'some-activity-id', + data: { + start_time: '2023-04-01T00:00:00Z', // reflects the first month with data + end_time: '2023-04-30T00:00:00Z', + by_namespace: [], + months: [ + { + timestamp: '2023-02-01T00:00:00Z', + counts: null, + namespaces: null, + new_clients: null, + }, + { + timestamp: '2023-03-01T00:00:00Z', + counts: null, + namespaces: null, + new_clients: null, + }, + { + timestamp: '2023-04-01T00:00:00Z', + counts: getCounts(), + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', + counts: getCounts(), + mounts: [ + { + mount_path: 'auth/authid/0', + counts: getCounts(), + }, + ], + }, + ], + new_clients: { + counts: getCounts(), + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', + counts: getCounts(), + mounts: [ + { + mount_path: 'auth/authid/0', + counts: getCounts(), + }, + ], + }, + ], + }, + }, + ], + total: getCounts(), + }, + }; + }); + await visit('/vault/clients/counts/overview'); + assert.dom(CLIENT_COUNT.dateRange.dateDisplay('start')).hasText('February 2023'); + assert.dom(CLIENT_COUNT.dateRange.dateDisplay('end')).hasText('April 2023'); + assert.dom(CLIENT_COUNT.counts.startDiscrepancy).exists(); + }); }); diff --git a/ui/tests/acceptance/clients/counts/overview-test.js b/ui/tests/acceptance/clients/counts/overview-test.js index 6e5ba2dedc..56d8ce5000 100644 --- a/ui/tests/acceptance/clients/counts/overview-test.js +++ b/ui/tests/acceptance/clients/counts/overview-test.js @@ -72,7 +72,7 @@ module('Acceptance | clients | overview', function (hooks) { .exists('new client attribution has empty state'); assert .dom(GENERAL.emptyStateSubtitle) - .hasText('There are no new clients for this namespace during this time period. '); + .hasText('There are no new clients for this namespace during this time period.'); assert.dom(CHARTS.container('total-clients')).exists('total client attribution chart shows'); // reset to billing period diff --git a/ui/tests/helpers/clients/client-count-selectors.ts b/ui/tests/helpers/clients/client-count-selectors.ts index 53691cfabf..ef820a6860 100644 --- a/ui/tests/helpers/clients/client-count-selectors.ts +++ b/ui/tests/helpers/clients/client-count-selectors.ts @@ -14,7 +14,6 @@ export const CLIENT_COUNT = { }, dateRange: { dateDisplay: (name: string) => (name ? `[data-test-date-range="${name}"]` : '[data-test-date-range]'), - set: '[data-test-set-date-range]', edit: '[data-test-date-range-edit]', editModal: '[data-test-date-range-edit-modal]', editDate: (name: string) => `[data-test-date-edit="${name}"]`, diff --git a/ui/tests/integration/components/clients/date-range-test.js b/ui/tests/integration/components/clients/date-range-test.js index beec2fe491..2e3765d6a2 100644 --- a/ui/tests/integration/components/clients/date-range-test.js +++ b/ui/tests/integration/components/clients/date-range-test.js @@ -33,9 +33,11 @@ module('Integration | Component | clients/date-range', function (hooks) { this.startTime = undefined; await this.renderComponent(); - assert.dom(DATE_RANGE.set).exists(); + assert.dom(DATE_RANGE.dateDisplay('start')).doesNotExist(); + assert.dom(DATE_RANGE.dateDisplay('end')).doesNotExist(); + assert.dom(DATE_RANGE.edit).hasText('Set date range'); - await click(DATE_RANGE.set); + await click(DATE_RANGE.edit); assert.dom(DATE_RANGE.editModal).exists(); assert.dom(DATE_RANGE.editDate('start')).hasValue(''); await fillIn(DATE_RANGE.editDate('start'), '2018-01'); @@ -50,11 +52,13 @@ module('Integration | Component | clients/date-range', function (hooks) { assert.dom(DATE_RANGE.editModal).doesNotExist('closes modal'); }); - test('it renders the date range passed and can reset it', async function (assert) { + test('it renders the date range passed and can reset it (ent)', async function (assert) { + this.owner.lookup('service:version').type = 'enterprise'; await this.renderComponent(); assert.dom(DATE_RANGE.dateDisplay('start')).hasText('January 2018'); assert.dom(DATE_RANGE.dateDisplay('end')).hasText('January 2019'); + assert.dom(DATE_RANGE.edit).hasText('Edit'); await click(DATE_RANGE.edit); assert.dom(DATE_RANGE.editModal).exists(); @@ -70,7 +74,30 @@ module('Integration | Component | clients/date-range', function (hooks) { assert.deepEqual(this.onChange.args[0], [{ start_time: undefined, end_time: undefined }]); }); + test('it renders the date range passed and cannot reset it when community', async function (assert) { + this.owner.lookup('service:version').type = 'community'; + await this.renderComponent(); + + assert.dom(DATE_RANGE.dateDisplay('start')).hasText('January 2018'); + assert.dom(DATE_RANGE.dateDisplay('end')).hasText('January 2019'); + assert.dom(DATE_RANGE.edit).hasText('Edit'); + + await click(DATE_RANGE.edit); + assert.dom(DATE_RANGE.editModal).exists(); + assert.dom(DATE_RANGE.editDate('start')).hasValue('2018-01'); + assert.dom(DATE_RANGE.editDate('end')).hasValue('2019-01'); + assert.dom(DATE_RANGE.defaultRangeAlert).doesNotExist(); + + await click(DATE_RANGE.editDate('reset')); + assert.dom(DATE_RANGE.editDate('start')).hasValue(''); + assert.dom(DATE_RANGE.editDate('end')).hasValue(''); + assert.dom(DATE_RANGE.validation).hasText('You must supply both start and end dates.'); + await click(GENERAL.saveButton); + assert.false(this.onChange.called); + }); + test('it does not trigger onChange if date range invalid', async function (assert) { + this.owner.lookup('service:version').type = 'enterprise'; await this.renderComponent(); await click(DATE_RANGE.edit); @@ -90,6 +117,18 @@ module('Integration | Component | clients/date-range', function (hooks) { assert.dom(DATE_RANGE.editModal).doesNotExist(); }); + test('it does not trigger onChange when reset and CE', async function (assert) { + this.owner.lookup('service:version').type = 'community'; + await this.renderComponent(); + + await click(DATE_RANGE.edit); + + await click(DATE_RANGE.reset); + assert.dom(DATE_RANGE.validation).hasText('You must supply both start and end dates.'); + await click(GENERAL.saveButton); + assert.false(this.onChange.called); + }); + test('it resets the tracked values on close', async function (assert) { await this.renderComponent(); diff --git a/ui/tests/integration/components/clients/page/counts-test.js b/ui/tests/integration/components/clients/page/counts-test.js index 4577d695c6..b2567cb826 100644 --- a/ui/tests/integration/components/clients/page/counts-test.js +++ b/ui/tests/integration/components/clients/page/counts-test.js @@ -9,7 +9,7 @@ import { setupMirage } from 'ember-cli-mirage/test-support'; import { render, click, findAll, fillIn } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; import clientsHandler, { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients'; -import { getUnixTime } from 'date-fns'; +import { fromUnixTime, getUnixTime } from 'date-fns'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import { CLIENT_COUNT } from 'vault/tests/helpers/clients/client-count-selectors'; import { selectChoose } from 'ember-power-select/test-support'; @@ -19,6 +19,8 @@ import { allowAllCapabilitiesStub } from 'vault/tests/helpers/stubs'; const START_TIME = getUnixTime(LICENSE_START); const END_TIME = getUnixTime(STATIC_NOW); +const START_ISO = LICENSE_START.toISOString(); +const END_ISO = STATIC_NOW.toISOString(); module('Integration | Component | clients | Page::Counts', function (hooks) { setupRenderingTest(hooks); @@ -35,8 +37,8 @@ module('Integration | Component | clients | Page::Counts', function (hooks) { }; this.activity = await this.store.queryRecord('clients/activity', activityQuery); this.config = await this.store.queryRecord('clients/config', {}); - this.startTimestamp = START_TIME; - this.endTimestamp = END_TIME; + this.startTimestamp = START_ISO; + this.endTimestamp = END_ISO; this.versionHistory = []; this.renderComponent = () => render(hbs` @@ -94,42 +96,77 @@ module('Integration | Component | clients | Page::Counts', function (hooks) { .hasText('Tracking is disabled', 'Config disabled alert renders'); }); - test('it should send correct values on start and end date change', async function (assert) { - assert.expect(3); - const jan23start = getUnixTime(new Date('2023-01-01T00:00:00Z')); - const dec23end = getUnixTime(new Date('2023-12-31T00:00:00Z')); - const jan24end = getUnixTime(new Date('2024-01-31T00:00:00Z')); + const jan23start = getUnixTime(new Date('2023-01-01T00:00:00Z')); + // license start is July 2, 2024 on date change it recalculates start to beginning of the month + const july23start = getUnixTime(new Date('2023-07-01T00:00:00Z')); + const dec23end = getUnixTime(new Date('2023-12-31T00:00:00Z')); + const jan24end = getUnixTime(new Date('2024-01-31T00:00:00Z')); + [ + { + scenario: 'changing start only', + expected: { start_time: jan23start, end_time: jan24end }, + editStart: '2023-01', + expectedStart: 'January 2023', + expectedEnd: 'January 2024', + }, + { + scenario: 'changing end only', + expected: { start_time: july23start, end_time: dec23end }, + editEnd: '2023-12', + expectedStart: 'July 2023', + expectedEnd: 'December 2023', + }, + { + scenario: 'changing both', + expected: { start_time: jan23start, end_time: dec23end }, + editStart: '2023-01', + editEnd: '2023-12', + expectedStart: 'January 2023', + expectedEnd: 'December 2023', + }, + { + scenario: 'reset', + expected: { start_time: undefined, end_time: undefined }, + reset: true, + expectedStart: 'July 2023', + expectedEnd: 'January 2024', + }, + ].forEach((testCase) => { + test(`it should send correct millis value on filter change when ${testCase.scenario}`, async function (assert) { + assert.expect(5); + // set to enterprise so reset will save correctly + this.owner.lookup('service:version').type = 'enterprise'; + this.onFilterChange = (params) => { + assert.deepEqual(params, testCase.expected, 'Correct values sent on filter change'); + // in the app, the timestamp choices trigger a qp refresh as millis from epoch, + // but in the model they are translated from millis to ISO timestamps before being + // passed to this component. Mock that behavior here. + this.set( + 'startTimestamp', + params?.start_time ? fromUnixTime(params.start_time).toISOString() : START_ISO + ); + this.set('endTimestamp', params?.end_time ? fromUnixTime(params.end_time).toISOString() : END_ISO); + }; + await this.renderComponent(); + await click(CLIENT_COUNT.dateRange.edit); - const expected = { start_time: START_TIME, end_time: END_TIME }; - this.onFilterChange = (params) => { - assert.deepEqual(params, expected, 'Correct values sent on filter change'); - this.set('startTimestamp', params.start_time || START_TIME); - this.set('endTimestamp', params.end_time || END_TIME); - }; - // page starts with default billing dates, which are july 23 - jan 24 - await this.renderComponent(); + // page starts with default billing dates, which are july 23 - jan 24 + assert.dom(CLIENT_COUNT.dateRange.editDate('start')).hasValue('2023-07'); + assert.dom(CLIENT_COUNT.dateRange.editDate('end')).hasValue('2024-01'); - // First, change only the start date - expected.start_time = jan23start; - // the end date which is first set to STATIC_NOW gets recalculated - // to the end of given month/year on date range change - expected.end_time = jan24end; - await click(CLIENT_COUNT.dateRange.edit); - await fillIn(CLIENT_COUNT.dateRange.editDate('start'), '2023-01'); - await click(GENERAL.saveButton); - - // Then change only the end date - expected.end_time = dec23end; - await click(CLIENT_COUNT.dateRange.edit); - await fillIn(CLIENT_COUNT.dateRange.editDate('end'), '2023-12'); - await click(GENERAL.saveButton); - - // Then reset to billing which should reset the params - expected.start_time = undefined; - expected.end_time = undefined; - await click(CLIENT_COUNT.dateRange.edit); - await click(CLIENT_COUNT.dateRange.reset); - await click(GENERAL.saveButton); + if (testCase.editStart) { + await fillIn(CLIENT_COUNT.dateRange.editDate('start'), testCase.editStart); + } + if (testCase.editEnd) { + await fillIn(CLIENT_COUNT.dateRange.editDate('end'), testCase.editEnd); + } + if (testCase.reset) { + await click(CLIENT_COUNT.dateRange.reset); + } + await click(GENERAL.saveButton); + assert.dom(CLIENT_COUNT.dateRange.dateDisplay('start')).hasText(testCase.expectedStart); + assert.dom(CLIENT_COUNT.dateRange.dateDisplay('end')).hasText(testCase.expectedEnd); + }); }); test('it should render namespace and auth mount filters', async function (assert) { @@ -168,7 +205,7 @@ module('Integration | Component | clients | Page::Counts', function (hooks) { }); test('it should render start time discrepancy alert', async function (assert) { - this.startTimestamp = getUnixTime(new Date('2022-06-01T00:00:00Z')); + this.startTimestamp = new Date('2022-06-01T00:00:00Z').toISOString(); await this.renderComponent(); @@ -236,7 +273,7 @@ module('Integration | Component | clients | Page::Counts', function (hooks) { await this.renderComponent(); assert.dom(GENERAL.emptyStateTitle).hasText('No start date found', 'Empty state renders'); - assert.dom(CLIENT_COUNT.dateRange.set).exists(); + assert.dom(CLIENT_COUNT.dateRange.edit).hasText('Set date range'); }); test('it should render catch all empty state', async function (assert) { diff --git a/ui/tests/integration/utils/client-count-utils-test.js b/ui/tests/integration/utils/client-count-utils-test.js index 7465297726..b783d7a345 100644 --- a/ui/tests/integration/utils/client-count-utils-test.js +++ b/ui/tests/integration/utils/client-count-utils-test.js @@ -13,7 +13,6 @@ import { destructureClientCounts, namespaceArrayToObject, sortMonthsByTimestamp, - setStartTimeQuery, } from 'core/utils/client-count-utils'; import clientsHandler from 'vault/mirage/handlers/clients'; import { @@ -28,9 +27,6 @@ used to normalize the sys/counters/activity response in the clients/activity serializer. these functions are tested individually here, instead of all at once in a serializer test for easier debugging */ - -// TODO refactor tests into a module for each util method, then make each assertion its separate tests - module('Integration | Util | client count utils', function (hooks) { setupTest(hooks); @@ -439,35 +435,4 @@ module('Integration | Util | client count utils', function (hooks) { 'it formats combined data for monthly namespaces_by_key spanning upgrade to 1.10' ); }); - - test('setStartTimeQuery: it returns start time query for activity log', async function (assert) { - assert.expect(6); - const apiPath = 'sys/internal/counters/config'; - assert.strictEqual(setStartTimeQuery(true, {}), null, `it returns null if no permission to ${apiPath}`); - assert.strictEqual( - setStartTimeQuery(false, {}), - null, - `it returns null for community edition and no permission to ${apiPath}` - ); - assert.strictEqual( - setStartTimeQuery(true, { billingStartTimestamp: new Date('2022-06-08T00:00:00Z') }), - 1654646400, - 'it returns unix time if enterprise and billing_start_timestamp exists' - ); - assert.strictEqual( - setStartTimeQuery(false, { billingStartTimestamp: new Date('0001-01-01T00:00:00Z') }), - null, - 'it returns null time for community edition even if billing_start_timestamp exists' - ); - assert.strictEqual( - setStartTimeQuery(false, { foo: 'bar' }), - null, - 'it returns null if billing_start_timestamp key does not exist' - ); - assert.strictEqual( - setStartTimeQuery(false, undefined), - null, - 'fails gracefully if no config model is passed' - ); - }); }); diff --git a/ui/tests/unit/adapters/clients-activity-test.js b/ui/tests/unit/adapters/clients-activity-test.js index 7d23d1e975..0e732c7933 100644 --- a/ui/tests/unit/adapters/clients-activity-test.js +++ b/ui/tests/unit/adapters/clients-activity-test.js @@ -137,17 +137,33 @@ module('Unit | Adapter | clients activity', function (hooks) { this.store.queryRecord(this.modelName, queryParams); }); - test('it sends current billing period boolean if provided', async function (assert) { + test('it sends without query if no dates provided', async function (assert) { assert.expect(1); this.server.get('sys/internal/counters/activity', (schema, req) => { - assert.propEqual( - req.queryParams, - { current_billing_period: 'true' }, - 'it passes current_billing_period to query record' - ); + assert.propEqual(req.queryParams, {}); }); - this.store.queryRecord(this.modelName, { current_billing_period: true }); + this.store.queryRecord(this.modelName, { foo: 'bar' }); + }); + + test('it sends without query if no valid dates provided', async function (assert) { + assert.expect(1); + + this.server.get('sys/internal/counters/activity', (schema, req) => { + assert.propEqual(req.queryParams, {}); + }); + + this.store.queryRecord(this.modelName, { start_time: 'bar' }); + }); + + test('it handles empty query gracefully', async function (assert) { + assert.expect(1); + + this.server.get('sys/internal/counters/activity', (schema, req) => { + assert.propEqual(req.queryParams, {}); + }); + + this.store.queryRecord(this.modelName, {}); }); });