From 473eeaa806bfa268ee8392d8af44ac1784514bca Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Tue, 4 Jun 2024 12:17:19 -0500 Subject: [PATCH 01/13] Handle month data with no new client counts --- ui/lib/core/addon/utils/client-count-utils.ts | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/ui/lib/core/addon/utils/client-count-utils.ts b/ui/lib/core/addon/utils/client-count-utils.ts index a41b1d263102..0b0a572f3f9f 100644 --- a/ui/lib/core/addon/utils/client-count-utils.ts +++ b/ui/lib/core/addon/utils/client-count-utils.ts @@ -86,7 +86,9 @@ export const formatDateObject = (dateObj: { monthIdx: number; year: number }, is return getUnixTime(utc); }; -export const formatByMonths = (monthsArray: ActivityMonthBlock[] | EmptyActivityMonthBlock[]) => { +export const formatByMonths = ( + monthsArray: (ActivityMonthBlock | EmptyActivityMonthBlock | NoNewClientsActivityMonthBlock)[] +) => { const sortedPayload = sortMonthsByTimestamp(monthsArray); return sortedPayload?.map((m) => { const month = parseAPITimestamp(m.timestamp, 'M/yy') as string; @@ -95,6 +97,16 @@ export const formatByMonths = (monthsArray: ActivityMonthBlock[] | EmptyActivity if (m.counts) { const totalClientsByNamespace = formatByNamespace(m.namespaces); const newClientsByNamespace = formatByNamespace(m.new_clients?.namespaces); + + let newClients: ByMonthNewClients = { month, timestamp, namespaces: [] }; + if (m.new_clients?.counts) { + newClients = { + month, + timestamp, + ...destructureClientCounts(m?.new_clients?.counts), + namespaces: formatByNamespace(m.new_clients?.namespaces) || [], + }; + } return { month, timestamp, @@ -106,12 +118,7 @@ export const formatByMonths = (monthsArray: ActivityMonthBlock[] | EmptyActivity month, m.timestamp ), - new_clients: { - month, - timestamp, - ...destructureClientCounts(m?.new_clients?.counts), - namespaces: formatByNamespace(m.new_clients?.namespaces) || [], - }, + new_clients: newClients, }; } // empty month @@ -125,7 +132,10 @@ export const formatByMonths = (monthsArray: ActivityMonthBlock[] | EmptyActivity }); }; -export const formatByNamespace = (namespaceArray: NamespaceObject[]) => { +export const formatByNamespace = (namespaceArray: NamespaceObject[] | null): ByNamespaceClients[] => { + if (!namespaceArray) { + return []; + } return namespaceArray.map((ns) => { // i.e. 'namespace_path' is an empty string for 'root', so use namespace_id const label = ns.namespace_path === '' ? ns.namespace_id : ns.namespace_path; @@ -158,7 +168,9 @@ export const destructureClientCounts = (verboseObject: Counts | ByNamespaceClien ); }; -export const sortMonthsByTimestamp = (monthsArray: ActivityMonthBlock[] | EmptyActivityMonthBlock[]) => { +export const sortMonthsByTimestamp = ( + monthsArray: (ActivityMonthBlock | EmptyActivityMonthBlock | NoNewClientsActivityMonthBlock)[] +) => { const sortedPayload = [...monthsArray]; return sortedPayload.sort((a, b) => compareAsc(parseAPITimestamp(a.timestamp) as Date, parseAPITimestamp(b.timestamp) as Date) @@ -168,7 +180,7 @@ export const sortMonthsByTimestamp = (monthsArray: ActivityMonthBlock[] | EmptyA export const namespaceArrayToObject = ( monthTotals: ByNamespaceClients[], // technically this arg (monthNew) is the same type as above, just nested inside monthly new clients - monthNew: ByMonthClients['new_clients']['namespaces'], + monthNew: ByMonthClients['new_clients']['namespaces'] | null, month: string, timestamp: string ) => { @@ -255,7 +267,14 @@ export interface ByMonthClients extends TotalClients { namespaces_by_key: { [key: string]: NamespaceByKey }; new_clients: ByMonthNewClients; } -export interface ByMonthNewClients extends TotalClients { + +// clients numbers are only returned if month is of type ActivityMonthBlock +export interface ByMonthNewClients { + clients?: number; + entity_clients?: number; + non_entity_clients?: number; + secret_syncs?: number; + acme_clients?: number; month: string; timestamp: string; namespaces: ByNamespaceClients[]; @@ -308,6 +327,16 @@ export interface ActivityMonthBlock { }; } +export interface NoNewClientsActivityMonthBlock { + timestamp: string; // YYYY-MM-01T00:00:00Z (always the first day of the month) + counts: Counts; + namespaces: NamespaceObject[]; + new_clients: { + counts: null; + namespaces: null; + }; +} + export interface EmptyActivityMonthBlock { timestamp: string; // YYYY-MM-01T00:00:00Z (always the first day of the month) counts: null; From fa06ac4f19c836ca3b51c128c7afcac106e6d597 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Tue, 4 Jun 2024 12:28:58 -0500 Subject: [PATCH 02/13] Update ACTIVITY_RESPONSE_STUB with no new clients block --- .../helpers/clients/client-count-helpers.js | 202 +++++++++++++++++- 1 file changed, 200 insertions(+), 2 deletions(-) diff --git a/ui/tests/helpers/clients/client-count-helpers.js b/ui/tests/helpers/clients/client-count-helpers.js index 374b3669fece..6987d33c8835 100644 --- a/ui/tests/helpers/clients/client-count-helpers.js +++ b/ui/tests/helpers/clients/client-count-helpers.js @@ -40,7 +40,7 @@ export function assertBarChart(assert, chartName, byMonthData, isStacked = false } export const ACTIVITY_RESPONSE_STUB = { - start_time: '2023-08-01T00:00:00Z', + start_time: '2023-06-01T00:00:00Z', end_time: '2023-09-30T23:59:59Z', // is always the last day and hour of the month queried by_namespace: [ { @@ -148,11 +148,209 @@ export const ACTIVITY_RESPONSE_STUB = { ], months: [ { - timestamp: '2023-08-01T00:00:00Z', + timestamp: '2023-06-01T00:00:00Z', counts: null, namespaces: null, new_clients: null, }, + { + timestamp: '2023-07-01T00:00:00Z', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + distinct_entities: 100, + non_entity_tokens: 100, + }, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + distinct_entities: 100, + non_entity_tokens: 100, + }, + mounts: [ + { + mount_path: 'pki-engine-0', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'auth/authid/0', + counts: { + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'kvv2-engine-0', + counts: { + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + ], + }, + ], + new_clients: { + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + distinct_entities: 100, + non_entity_tokens: 100, + }, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + distinct_entities: 100, + non_entity_tokens: 100, + }, + mounts: [ + { + mount_path: 'pki-engine-0', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'auth/authid/0', + counts: { + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'kvv2-engine-0', + counts: { + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + ], + }, + ], + }, + }, + { + timestamp: '2023-08-01T00:00:00Z', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + distinct_entities: 100, + non_entity_tokens: 100, + }, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + distinct_entities: 100, + non_entity_tokens: 100, + }, + mounts: [ + { + mount_path: 'pki-engine-0', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'auth/authid/0', + counts: { + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'kvv2-engine-0', + counts: { + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + ], + }, + ], + new_clients: { + counts: null, + namespaces: null, + }, + }, { timestamp: '2023-09-01T00:00:00Z', counts: { From ea91a911cfda5f2da8632d1a8b1fab75d0d2609d Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Tue, 4 Jun 2024 15:03:10 -0500 Subject: [PATCH 03/13] cleanup --- ui/lib/core/addon/utils/client-count-utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/lib/core/addon/utils/client-count-utils.ts b/ui/lib/core/addon/utils/client-count-utils.ts index 0b0a572f3f9f..42dcf497cf77 100644 --- a/ui/lib/core/addon/utils/client-count-utils.ts +++ b/ui/lib/core/addon/utils/client-count-utils.ts @@ -104,14 +104,14 @@ export const formatByMonths = ( month, timestamp, ...destructureClientCounts(m?.new_clients?.counts), - namespaces: formatByNamespace(m.new_clients?.namespaces) || [], + namespaces: formatByNamespace(m.new_clients?.namespaces), }; } return { month, timestamp, ...destructureClientCounts(m.counts), - namespaces: formatByNamespace(m.namespaces) || [], + namespaces: formatByNamespace(m.namespaces), namespaces_by_key: namespaceArrayToObject( totalClientsByNamespace, newClientsByNamespace, From 27d258fdc1cbae22cf21aca752515a111322841a Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Tue, 4 Jun 2024 15:04:40 -0500 Subject: [PATCH 04/13] Update SERIALIZED_ACTIVITY_RESPONSE to match new months in ACTIVITY_RESPONSE --- .../helpers/clients/client-count-helpers.js | 363 +++++++++++++++++- 1 file changed, 361 insertions(+), 2 deletions(-) diff --git a/ui/tests/helpers/clients/client-count-helpers.js b/ui/tests/helpers/clients/client-count-helpers.js index 6987d33c8835..db611cc397f1 100644 --- a/ui/tests/helpers/clients/client-count-helpers.js +++ b/ui/tests/helpers/clients/client-count-helpers.js @@ -844,10 +844,369 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { ], by_month: [ { - month: '8/23', - timestamp: '2023-08-01T00:00:00Z', + month: '6/23', + timestamp: '2023-06-01T00:00:00Z', namespaces: [], namespaces_by_key: {}, + new_clients: { + month: '6/23', + timestamp: '2023-06-01T00:00:00Z', + namespaces: [], + }, + }, + { + month: '7/23', + timestamp: '2023-07-01T00:00:00Z', + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + namespaces: [ + { + label: 'root', + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + mounts: [ + { + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + }, + { + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + }, + { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + }, + ], + }, + ], + namespaces_by_key: { + root: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + timestamp: '2023-07-01T00:00:00Z', + month: '7/23', + new_clients: { + month: '7/23', + timestamp: '2023-07-01T00:00:00Z', + label: 'root', + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + mounts: [ + { + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + }, + { + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + }, + { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + }, + ], + }, + mounts_by_key: { + 'pki-engine-0': { + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + timestamp: '2023-07-01T00:00:00Z', + month: '7/23', + new_clients: { + month: '7/23', + timestamp: '2023-07-01T00:00:00Z', + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + }, + }, + 'auth/authid/0': { + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + timestamp: '2023-07-01T00:00:00Z', + month: '7/23', + new_clients: { + month: '7/23', + timestamp: '2023-07-01T00:00:00Z', + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + }, + }, + 'kvv2-engine-0': { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + timestamp: '2023-07-01T00:00:00Z', + month: '7/23', + new_clients: { + month: '7/23', + timestamp: '2023-07-01T00:00:00Z', + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + }, + }, + }, + }, + }, + new_clients: { + month: '7/23', + timestamp: '2023-07-01T00:00:00Z', + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + namespaces: [ + { + label: 'root', + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + mounts: [ + { + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + }, + { + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + }, + { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + }, + ], + }, + ], + }, + }, + { + month: '8/23', + timestamp: '2023-08-01T00:00:00Z', + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + namespaces: [ + { + label: 'root', + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + mounts: [ + { + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + }, + { + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 748, + secret_syncs: 0, + }, + { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + }, + ], + }, + ], + namespaces_by_key: { + root: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + timestamp: '2023-08-01T00:00:00Z', + month: '8/23', + // TODO: this isn't right + new_clients: { + month: '8/23', + timestamp: '2023-08-01T00:00:00Z', + label: 'root', + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + mounts: [ + { + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + }, + { + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + }, + { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + }, + ], + }, + mounts_by_key: { + 'pki-engine-0': { + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + timestamp: '2023-08-01T00:00:00Z', + month: '8/23', + new_clients: { + month: '8/23', + timestamp: '2023-08-01T00:00:00Z', + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + }, + }, + 'auth/authid/0': { + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + timestamp: '2023-08-01T00:00:00Z', + month: '8/23', + new_clients: { + month: '8/23', + timestamp: '2023-08-01T00:00:00Z', + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + }, + }, + 'kvv2-engine-0': { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + timestamp: '2023-08-01T00:00:00Z', + month: '8/23', + new_clients: { + month: '8/23', + timestamp: '2023-08-01T00:00:00Z', + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + }, + }, + }, + }, + }, new_clients: { month: '8/23', timestamp: '2023-08-01T00:00:00Z', From 07ce298cb9d6ea4acdba4c57041e134f5f354158 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Tue, 4 Jun 2024 15:05:12 -0500 Subject: [PATCH 05/13] Update tests to account for new Activity Response --- .../dashboard/client-count-card-test.js | 2 +- .../utils/client-count-utils-test.js | 22 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) 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 de4a81f07d08..388de5097e20 100644 --- a/ui/tests/integration/components/dashboard/client-count-card-test.js +++ b/ui/tests/integration/components/dashboard/client-count-card-test.js @@ -43,7 +43,7 @@ module('Integration | Component | dashboard/client-count-card', function (hooks) assert .dom(CLIENT_COUNT.statText('Total')) .hasText( - `Total The number of clients in this billing period (Aug 2023 - Sep 2023). ${formatNumber([ + `Total The number of clients in this billing period (Jun 2023 - Sep 2023). ${formatNumber([ total.clients, ])}` ); diff --git a/ui/tests/integration/utils/client-count-utils-test.js b/ui/tests/integration/utils/client-count-utils-test.js index 24c9f85d339f..62595d91df85 100644 --- a/ui/tests/integration/utils/client-count-utils-test.js +++ b/ui/tests/integration/utils/client-count-utils-test.js @@ -195,18 +195,16 @@ module('Integration | Util | client count utils', function (hooks) { }); test('namespaceArrayToObject: it returns namespaces_by_key and mounts_by_key', async function (assert) { - assert.expect(5); - - // month at 0-index has no data so use second month in array, empty month format covered by formatByMonths test above - const original = { ...RESPONSE.months[1] }; - const expectedObject = SERIALIZED_ACTIVITY_RESPONSE.by_month[1].namespaces_by_key; - const formattedTotal = formatByNamespace(RESPONSE.months[1].namespaces); - + // namespaceToArrayObject is only called when new clients are present, so only check against month with new clients + const i = RESPONSE.months.length - 1; + const original = { ...RESPONSE.months[i] }; + const expectedObject = SERIALIZED_ACTIVITY_RESPONSE.by_month[i].namespaces_by_key; + const formattedTotal = formatByNamespace(RESPONSE.months[i].namespaces); const testObject = namespaceArrayToObject( formattedTotal, - formatByNamespace(RESPONSE.months[1].new_clients.namespaces), - '9/23', - '2023-09-01T00:00:00Z' + formatByNamespace(RESPONSE.months[i].new_clients.namespaces), + `9/23`, + original.timestamp ); const { root } = testObject; @@ -216,11 +214,11 @@ module('Integration | Util | client count utils', function (hooks) { assert.propContains(root, expectedRoot, 'namespace has correct keys'); assert.propEqual( - namespaceArrayToObject(formattedTotal, formatByNamespace([]), '9/23', '2023-09-01T00:00:00Z'), + namespaceArrayToObject(formattedTotal, [], '9/23', '2023-09-01T00:00:00Z'), {}, 'returns an empty object when there are no new clients ' ); - assert.propEqual(RESPONSE.months[1], original, 'it does not modify original month data'); + assert.propEqual(RESPONSE.months[i], original, 'it does not modify original month data'); }); // TESTS FOR COMBINED ACTIVITY DATA - no mount attribution < 1.10 From e4d9099499c56b0eaf46634931b0f5b63c6bbdc3 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Tue, 4 Jun 2024 15:13:19 -0500 Subject: [PATCH 06/13] parity --- ui/lib/core/addon/utils/client-count-utils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ui/lib/core/addon/utils/client-count-utils.ts b/ui/lib/core/addon/utils/client-count-utils.ts index 42dcf497cf77..36aeef8ff9a6 100644 --- a/ui/lib/core/addon/utils/client-count-utils.ts +++ b/ui/lib/core/addon/utils/client-count-utils.ts @@ -133,9 +133,7 @@ export const formatByMonths = ( }; export const formatByNamespace = (namespaceArray: NamespaceObject[] | null): ByNamespaceClients[] => { - if (!namespaceArray) { - return []; - } + if (!Array.isArray(namespaceArray)) return []; return namespaceArray.map((ns) => { // i.e. 'namespace_path' is an empty string for 'root', so use namespace_id const label = ns.namespace_path === '' ? ns.namespace_id : ns.namespace_path; From fade988e2a793e53aaa2eb2aab2c475332128b1d Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Tue, 4 Jun 2024 15:35:55 -0500 Subject: [PATCH 07/13] fix last failing cc test --- ui/tests/integration/utils/client-count-utils-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/tests/integration/utils/client-count-utils-test.js b/ui/tests/integration/utils/client-count-utils-test.js index 62595d91df85..18c28665bc8b 100644 --- a/ui/tests/integration/utils/client-count-utils-test.js +++ b/ui/tests/integration/utils/client-count-utils-test.js @@ -187,7 +187,7 @@ module('Integration | Util | client count utils', function (hooks) { test('sortMonthsByTimestamp: sorts timestamps chronologically, oldest to most recent', async function (assert) { assert.expect(2); // API returns them in order so this test is extra extra - const unOrdered = [RESPONSE.months[1], RESPONSE.months[0]]; // mixup order + const unOrdered = [RESPONSE.months[1], RESPONSE.months[0], RESPONSE.months[3], RESPONSE.months[2]]; // mixup order const original = [...RESPONSE.months]; const expected = RESPONSE.months; assert.propEqual(sortMonthsByTimestamp(unOrdered), expected); From a248105f96544c8c1444e4827abe3b4ed5592e52 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Tue, 4 Jun 2024 15:38:56 -0500 Subject: [PATCH 08/13] add changelog --- changelog/27352.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog/27352.txt diff --git a/changelog/27352.txt b/changelog/27352.txt new file mode 100644 index 000000000000..70a7fa366126 --- /dev/null +++ b/changelog/27352.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: fix issue where a month without new clients breaks the client count dashboard +``` From 0ac07e36205e97629b62a6af69792a756b2e57c9 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Wed, 5 Jun 2024 13:43:03 -0500 Subject: [PATCH 09/13] iterate on months --- .../utils/client-count-utils-test.js | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/ui/tests/integration/utils/client-count-utils-test.js b/ui/tests/integration/utils/client-count-utils-test.js index 18c28665bc8b..805ba65da4f7 100644 --- a/ui/tests/integration/utils/client-count-utils-test.js +++ b/ui/tests/integration/utils/client-count-utils-test.js @@ -195,30 +195,31 @@ module('Integration | Util | client count utils', function (hooks) { }); test('namespaceArrayToObject: it returns namespaces_by_key and mounts_by_key', async function (assert) { - // namespaceToArrayObject is only called when new clients are present, so only check against month with new clients - const i = RESPONSE.months.length - 1; - const original = { ...RESPONSE.months[i] }; - const expectedObject = SERIALIZED_ACTIVITY_RESPONSE.by_month[i].namespaces_by_key; - const formattedTotal = formatByNamespace(RESPONSE.months[i].namespaces); - const testObject = namespaceArrayToObject( - formattedTotal, - formatByNamespace(RESPONSE.months[i].new_clients.namespaces), - `9/23`, - original.timestamp - ); + // namespaceArrayToObject only called when there are counts, so skip month 0 which has no counts + for (let i = 1; i < RESPONSE.months.length; i++) { + const original = { ...RESPONSE.months[i] }; + const expectedObject = SERIALIZED_ACTIVITY_RESPONSE.by_month[i].namespaces_by_key; + const formattedTotal = formatByNamespace(RESPONSE.months[i].namespaces); + const testObject = namespaceArrayToObject( + formattedTotal, + formatByNamespace(RESPONSE.months[i].new_clients.namespaces), + `9/23`, + original.timestamp + ); - const { root } = testObject; - const { root: expectedRoot } = expectedObject; - assert.propEqual(root.new_clients, expectedRoot.new_clients, 'it formats namespaces new_clients'); - assert.propEqual(root.mounts_by_key, expectedRoot.mounts_by_key, 'it formats namespaces mounts_by_key'); - assert.propContains(root, expectedRoot, 'namespace has correct keys'); + const { root } = testObject; + const { root: expectedRoot } = expectedObject; + assert.propEqual(root.new_clients, expectedRoot.new_clients, 'it formats namespaces new_clients'); + assert.propEqual(root.mounts_by_key, expectedRoot.mounts_by_key, 'it formats namespaces mounts_by_key'); + assert.propContains(root, expectedRoot, 'namespace has correct keys'); - assert.propEqual( - namespaceArrayToObject(formattedTotal, [], '9/23', '2023-09-01T00:00:00Z'), - {}, - 'returns an empty object when there are no new clients ' - ); - assert.propEqual(RESPONSE.months[i], original, 'it does not modify original month data'); + assert.propEqual( + namespaceArrayToObject(formattedTotal, [], '9/23', '2023-09-01T00:00:00Z'), + {}, + 'returns an empty object when there are no new clients ' + ); + assert.propEqual(RESPONSE.months[i], original, 'it does not modify original month data'); + } }); // TESTS FOR COMBINED ACTIVITY DATA - no mount attribution < 1.10 From 41fc8fd0c98f7e27693eead695ee94d7789aa714 Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Wed, 5 Jun 2024 14:02:35 -0500 Subject: [PATCH 10/13] update serialized blob to correct for no new clients --- .../helpers/clients/client-count-helpers.js | 34 +------------------ 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/ui/tests/helpers/clients/client-count-helpers.js b/ui/tests/helpers/clients/client-count-helpers.js index db611cc397f1..952cb4898b0b 100644 --- a/ui/tests/helpers/clients/client-count-helpers.js +++ b/ui/tests/helpers/clients/client-count-helpers.js @@ -1106,42 +1106,10 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { secret_syncs: 100, timestamp: '2023-08-01T00:00:00Z', month: '8/23', - // TODO: this isn't right new_clients: { month: '8/23', timestamp: '2023-08-01T00:00:00Z', - label: 'root', - acme_clients: 100, - clients: 100, - entity_clients: 100, - non_entity_clients: 100, - secret_syncs: 100, - mounts: [ - { - label: 'pki-engine-0', - acme_clients: 100, - clients: 100, - entity_clients: 0, - non_entity_clients: 0, - secret_syncs: 0, - }, - { - label: 'auth/authid/0', - acme_clients: 0, - clients: 100, - entity_clients: 100, - non_entity_clients: 100, - secret_syncs: 0, - }, - { - label: 'kvv2-engine-0', - acme_clients: 0, - clients: 100, - entity_clients: 0, - non_entity_clients: 0, - secret_syncs: 100, - }, - ], + namespaces: [], }, mounts_by_key: { 'pki-engine-0': { From 48830baa052f625c8bd84ec1d744b19e0ee9f9bc Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Wed, 5 Jun 2024 14:12:47 -0500 Subject: [PATCH 11/13] update in mounts_by_key --- .../helpers/clients/client-count-helpers.js | 21 +++---------------- 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/ui/tests/helpers/clients/client-count-helpers.js b/ui/tests/helpers/clients/client-count-helpers.js index 952cb4898b0b..8ad6f080caa8 100644 --- a/ui/tests/helpers/clients/client-count-helpers.js +++ b/ui/tests/helpers/clients/client-count-helpers.js @@ -1124,12 +1124,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { new_clients: { month: '8/23', timestamp: '2023-08-01T00:00:00Z', - label: 'pki-engine-0', - acme_clients: 100, - clients: 100, - entity_clients: 0, - non_entity_clients: 0, - secret_syncs: 0, + namespaces: [], }, }, 'auth/authid/0': { @@ -1144,12 +1139,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { new_clients: { month: '8/23', timestamp: '2023-08-01T00:00:00Z', - label: 'auth/authid/0', - acme_clients: 0, - clients: 100, - entity_clients: 100, - non_entity_clients: 100, - secret_syncs: 0, + namespaces: [], }, }, 'kvv2-engine-0': { @@ -1164,12 +1154,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { new_clients: { month: '8/23', timestamp: '2023-08-01T00:00:00Z', - label: 'kvv2-engine-0', - acme_clients: 0, - clients: 100, - entity_clients: 0, - non_entity_clients: 0, - secret_syncs: 100, + namespaces: [], }, }, }, From 1a9eb674ea06abe916285fa550003dd0279b439c Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Wed, 5 Jun 2024 16:14:29 -0500 Subject: [PATCH 12/13] muck through the response --- ui/lib/core/addon/utils/client-count-utils.ts | 83 +++++++++++-------- .../helpers/clients/client-count-helpers.js | 9 +- .../utils/client-count-utils-test.js | 15 ++-- 3 files changed, 60 insertions(+), 47 deletions(-) diff --git a/ui/lib/core/addon/utils/client-count-utils.ts b/ui/lib/core/addon/utils/client-count-utils.ts index 36aeef8ff9a6..69091f888ea1 100644 --- a/ui/lib/core/addon/utils/client-count-utils.ts +++ b/ui/lib/core/addon/utils/client-count-utils.ts @@ -186,36 +186,45 @@ export const namespaceArrayToObject = ( // it's an object in each month data block where the keys are namespace paths // and values include new and total client counts for that namespace in that month const namespaces_by_key = monthTotals.reduce((nsObject: { [key: string]: NamespaceByKey }, ns) => { + const keyedNs: NamespaceByKey = { + ...destructureClientCounts(ns), + timestamp, + month, + mounts_by_key: {}, + new_clients: { + month, + timestamp, + label: ns.label, + mounts: [], + }, + }; const newNsClients = monthNew?.find((n) => n.label === ns.label); + // mounts_by_key is is used to filter further in a namespace and get monthly activity by mount + // it's an object inside the namespace block where the keys are mount paths + // and the values include new and total client counts for that mount in that month + keyedNs.mounts_by_key = ns.mounts.reduce( + (mountObj: { [key: string]: MountByKey }, mount) => { + const mountNewClients = newNsClients ? newNsClients.mounts.find((m) => m.label === mount.label) : {}; + mountObj[mount.label] = { + ...mount, + timestamp, + month, + new_clients: { + timestamp, + month, + label: mount.label, + ...mountNewClients, + }, + }; + + return mountObj; + }, + {} as { [key: string]: MountByKey } + ); if (newNsClients) { - // mounts_by_key is is used to filter further in a namespace and get monthly activity by mount - // it's an object inside the namespace block where the keys are mount paths - // and the values include new and total client counts for that mount in that month - const mounts_by_key = ns.mounts.reduce( - (mountObj: { [key: string]: MountByKey }, mount) => { - const newMountClients = newNsClients.mounts.find((m) => m.label === mount.label); - - if (newMountClients) { - mountObj[mount.label] = { - ...mount, - timestamp, - month, - new_clients: { month, timestamp, ...newMountClients }, - }; - } - return mountObj; - }, - {} as { [key: string]: MountByKey } - ); - - nsObject[ns.label] = { - ...destructureClientCounts(ns), - timestamp, - month, - new_clients: { month, timestamp, ...newNsClients }, - mounts_by_key, - }; + keyedNs.new_clients = { month, timestamp, ...newNsClients }; } + nsObject[ns.label] = keyedNs; return nsObject; }, {}); @@ -249,6 +258,15 @@ export interface TotalClients { acme_clients: number; } +// extend this type when the counts are optional (eg for new clients) +interface TotalClientsSometimes { + clients?: number; + entity_clients?: number; + non_entity_clients?: number; + secret_syncs?: number; + acme_clients?: number; +} + export interface ByNamespaceClients extends TotalClients { label: string; mounts: MountClients[]; @@ -267,12 +285,7 @@ export interface ByMonthClients extends TotalClients { } // clients numbers are only returned if month is of type ActivityMonthBlock -export interface ByMonthNewClients { - clients?: number; - entity_clients?: number; - non_entity_clients?: number; - secret_syncs?: number; - acme_clients?: number; +export interface ByMonthNewClients extends TotalClientsSometimes { month: string; timestamp: string; namespaces: ByNamespaceClients[]; @@ -285,7 +298,7 @@ export interface NamespaceByKey extends TotalClients { new_clients: NamespaceNewClients; } -export interface NamespaceNewClients extends TotalClients { +export interface NamespaceNewClients extends TotalClientsSometimes { month: string; timestamp: string; label: string; @@ -299,7 +312,7 @@ export interface MountByKey extends TotalClients { new_clients: MountNewClients; } -export interface MountNewClients extends TotalClients { +export interface MountNewClients extends TotalClientsSometimes { month: string; timestamp: string; label: string; diff --git a/ui/tests/helpers/clients/client-count-helpers.js b/ui/tests/helpers/clients/client-count-helpers.js index 8ad6f080caa8..58e1f4a5f8d3 100644 --- a/ui/tests/helpers/clients/client-count-helpers.js +++ b/ui/tests/helpers/clients/client-count-helpers.js @@ -1107,9 +1107,10 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { timestamp: '2023-08-01T00:00:00Z', month: '8/23', new_clients: { + label: 'root', month: '8/23', timestamp: '2023-08-01T00:00:00Z', - namespaces: [], + mounts: [], }, mounts_by_key: { 'pki-engine-0': { @@ -1122,9 +1123,9 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { timestamp: '2023-08-01T00:00:00Z', month: '8/23', new_clients: { + label: 'pki-engine-0', month: '8/23', timestamp: '2023-08-01T00:00:00Z', - namespaces: [], }, }, 'auth/authid/0': { @@ -1137,9 +1138,9 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { timestamp: '2023-08-01T00:00:00Z', month: '8/23', new_clients: { + label: 'auth/authid/0', month: '8/23', timestamp: '2023-08-01T00:00:00Z', - namespaces: [], }, }, 'kvv2-engine-0': { @@ -1152,9 +1153,9 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { timestamp: '2023-08-01T00:00:00Z', month: '8/23', new_clients: { + label: 'kvv2-engine-0', month: '8/23', timestamp: '2023-08-01T00:00:00Z', - namespaces: [], }, }, }, diff --git a/ui/tests/integration/utils/client-count-utils-test.js b/ui/tests/integration/utils/client-count-utils-test.js index 805ba65da4f7..9f577ac8126c 100644 --- a/ui/tests/integration/utils/client-count-utils-test.js +++ b/ui/tests/integration/utils/client-count-utils-test.js @@ -203,21 +203,20 @@ module('Integration | Util | client count utils', function (hooks) { const testObject = namespaceArrayToObject( formattedTotal, formatByNamespace(RESPONSE.months[i].new_clients.namespaces), - `9/23`, + `${i + 6}/23`, original.timestamp ); - const { root } = testObject; const { root: expectedRoot } = expectedObject; - assert.propEqual(root.new_clients, expectedRoot.new_clients, 'it formats namespaces new_clients'); - assert.propEqual(root.mounts_by_key, expectedRoot.mounts_by_key, 'it formats namespaces mounts_by_key'); - assert.propContains(root, expectedRoot, 'namespace has correct keys'); assert.propEqual( - namespaceArrayToObject(formattedTotal, [], '9/23', '2023-09-01T00:00:00Z'), - {}, - 'returns an empty object when there are no new clients ' + root?.new_clients, + expectedRoot?.new_clients, + `it formats namespaces new_clients for ${original.timestamp}` ); + assert.propEqual(root.mounts_by_key, expectedRoot.mounts_by_key, 'it formats namespaces mounts_by_key'); + assert.propContains(root, expectedRoot, 'namespace has correct keys'); + assert.propEqual(RESPONSE.months[i], original, 'it does not modify original month data'); } }); From 841cffc5323d155b2725a8b32d8dbcd164fb663e Mon Sep 17 00:00:00 2001 From: Chelsea Shaw Date: Wed, 5 Jun 2024 16:29:34 -0500 Subject: [PATCH 13/13] more tests --- .../helpers/clients/client-count-helpers.js | 2 +- .../utils/client-count-utils-test.js | 30 ++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/ui/tests/helpers/clients/client-count-helpers.js b/ui/tests/helpers/clients/client-count-helpers.js index 58e1f4a5f8d3..d433c9f09597 100644 --- a/ui/tests/helpers/clients/client-count-helpers.js +++ b/ui/tests/helpers/clients/client-count-helpers.js @@ -1083,7 +1083,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { acme_clients: 0, clients: 100, entity_clients: 100, - non_entity_clients: 748, + non_entity_clients: 100, secret_syncs: 0, }, { diff --git a/ui/tests/integration/utils/client-count-utils-test.js b/ui/tests/integration/utils/client-count-utils-test.js index 9f577ac8126c..14444f8d9229 100644 --- a/ui/tests/integration/utils/client-count-utils-test.js +++ b/ui/tests/integration/utils/client-count-utils-test.js @@ -129,27 +129,29 @@ module('Integration | Util | client count utils', function (hooks) { }); test('formatByMonths: it formats the months array', async function (assert) { - assert.expect(5); + assert.expect(9); const original = [...RESPONSE.months]; - const [formattedNoData, formattedWithActivity] = formatByMonths(RESPONSE.months); + const [formattedNoData, formattedWithActivity, formattedNoNew] = formatByMonths(RESPONSE.months); // instead of asserting the whole expected response, broken up so tests are easier to debug // but kept whole above to copy/paste updated response expectations in the future - const [expectedNoData, expectedWithActivity] = SERIALIZED_ACTIVITY_RESPONSE.by_month; - const { namespaces, new_clients } = expectedWithActivity; + const [expectedNoData, expectedWithActivity, expectedNoNew] = SERIALIZED_ACTIVITY_RESPONSE.by_month; assert.propEqual(formattedNoData, expectedNoData, 'it formats months without data'); - assert.propEqual( - formattedWithActivity.namespaces, - namespaces, - 'it formats namespaces array for months with data' - ); - assert.propEqual( - formattedWithActivity.new_clients, - new_clients, - 'it formats new_clients block for months with data' - ); + ['namespaces', 'new_clients', 'namespaces_by_key'].forEach((key) => { + assert.propEqual( + formattedWithActivity[key], + expectedWithActivity[key], + `it formats ${key} array for months with data` + ); + assert.propEqual( + formattedNoNew[key], + expectedNoNew[key], + `it formats the ${key} array for months with no new clients` + ); + }); + assert.propEqual(RESPONSE.months, original, 'it does not modify original months array'); assert.propEqual(formatByMonths([]), [], 'it returns an empty array if the months key is empty'); });