mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
UI: Convert client count utils to typescript (#26262)
* cleanup namespaceArrayToObject method * WIP typescript conversion * WIP typescripted destructured block * slowly making progress.... * WIP move all types to util type file, separate returns in formatByMonths * namespaceArrayToObject is working?!? * fix mirage handler not generating months when queries are after upgrade * wow, the types are actually working omg * add comments and update client-count-utils test * delete old js file * remove types from activity model * remove comment * reuse totalclients type to minimize places we add types * commit file with type changes for git diff * delete util file again * address PR feedback and move type declarations to util file * remove unused types * update tests, use client helper in dashboard clients test * remove typo * make modifications with updated combined activity response from the backend
This commit is contained in:
@@ -10,15 +10,16 @@ import Component from '@glimmer/component';
|
||||
import { isSameMonth, fromUnixTime } from 'date-fns';
|
||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
import { calculateAverage } from 'vault/utils/chart-helpers';
|
||||
import { filterVersionHistory } from 'core/utils/client-count-utils';
|
||||
import { filterVersionHistory, hasMountsKey, hasNamespacesKey } from 'core/utils/client-count-utils';
|
||||
|
||||
import type ClientsActivityModel from 'vault/models/clients/activity';
|
||||
import type {
|
||||
ClientActivityNewClients,
|
||||
ClientActivityMonthly,
|
||||
ClientActivityResourceByKey,
|
||||
} from 'vault/models/clients/activity';
|
||||
import type ClientsVersionHistoryModel from 'vault/models/clients/version-history';
|
||||
import type {
|
||||
ByMonthNewClients,
|
||||
MountNewClients,
|
||||
NamespaceByKey,
|
||||
NamespaceNewClients,
|
||||
} from 'core/utils/client-count-utils';
|
||||
|
||||
interface Args {
|
||||
isSecretsSyncActivated?: boolean;
|
||||
@@ -33,10 +34,8 @@ interface Args {
|
||||
export default class ClientsActivityComponent extends Component<Args> {
|
||||
average = (
|
||||
data:
|
||||
| ClientActivityMonthly[]
|
||||
| (ClientActivityResourceByKey | undefined)[]
|
||||
| (ClientActivityNewClients | undefined)[]
|
||||
| undefined,
|
||||
| (ByMonthNewClients | NamespaceNewClients | MountNewClients | undefined)[]
|
||||
| (NamespaceByKey | undefined)[],
|
||||
key: string
|
||||
) => {
|
||||
return calculateAverage(data, key);
|
||||
@@ -65,18 +64,18 @@ export default class ClientsActivityComponent extends Component<Args> {
|
||||
return activity.byMonth;
|
||||
}
|
||||
const namespaceData = activity.byMonth
|
||||
.map((m) => m.namespaces_by_key[namespace as keyof typeof m.namespaces_by_key])
|
||||
?.map((m) => m.namespaces_by_key[namespace])
|
||||
.filter((d) => d !== undefined);
|
||||
|
||||
if (!mountPath) {
|
||||
return namespaceData.length === 0 ? undefined : namespaceData;
|
||||
return namespaceData || [];
|
||||
}
|
||||
|
||||
const mountData = mountPath
|
||||
? namespaceData.map((namespace) => namespace?.mounts_by_key[mountPath]).filter((d) => d !== undefined)
|
||||
: namespaceData;
|
||||
const mountData = namespaceData
|
||||
?.map((namespace) => namespace?.mounts_by_key[mountPath])
|
||||
.filter((d) => d !== undefined);
|
||||
|
||||
return mountData.length === 0 ? undefined : mountData;
|
||||
return mountData || [];
|
||||
}
|
||||
|
||||
get filteredActivityByNamespace() {
|
||||
@@ -119,11 +118,13 @@ export default class ClientsActivityComponent extends Component<Args> {
|
||||
return filterVersionHistory(versionHistory, activity.startTime, activity.endTime);
|
||||
}
|
||||
|
||||
// (object) single month new client data with total counts + array of namespace breakdown
|
||||
// (object) single month new client data with total counts and array of
|
||||
// either namespaces or mounts
|
||||
get newClientCounts() {
|
||||
if (this.isDateRange || !this.byMonthActivityData) {
|
||||
if (this.isDateRange || this.byMonthActivityData.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.byMonthActivityData[0]?.new_clients;
|
||||
}
|
||||
|
||||
@@ -140,13 +141,14 @@ export default class ClientsActivityComponent extends Component<Args> {
|
||||
// new client data for horizontal bar chart
|
||||
get newClientAttribution() {
|
||||
// new client attribution only available in a single, historical month (not a date range or current month)
|
||||
if (this.isDateRange || this.isCurrentMonth) return null;
|
||||
if (this.isDateRange || this.isCurrentMonth || !this.newClientCounts) return null;
|
||||
|
||||
if (this.args.namespace) {
|
||||
return this.newClientCounts?.mounts || null;
|
||||
} else {
|
||||
return this.newClientCounts?.namespaces || null;
|
||||
}
|
||||
const newCounts = this.newClientCounts;
|
||||
if (this.args.namespace && hasMountsKey(newCounts)) return newCounts?.mounts;
|
||||
|
||||
if (hasNamespacesKey(newCounts)) return newCounts?.namespaces;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
get hasAttributionData() {
|
||||
|
||||
@@ -9,8 +9,9 @@ import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
import { format, isValid } from 'date-fns';
|
||||
import { debug } from '@ember/debug';
|
||||
|
||||
import type { Count, MonthlyChartData, Timestamp } from 'vault/vault/charts/client-counts';
|
||||
import type ClientsVersionHistoryModel from 'vault/models/clients/version-history';
|
||||
import type { MonthlyChartData, Timestamp } from 'vault/vault/charts/client-counts';
|
||||
import type { TotalClients } from 'core/utils/client-count-utils';
|
||||
|
||||
interface Args {
|
||||
dataset: MonthlyChartData[];
|
||||
@@ -67,7 +68,7 @@ export default class LineChart extends Component<Args> {
|
||||
const upgradeMessage = this.getUpgradeMessage(datum);
|
||||
return {
|
||||
x: timestamp,
|
||||
y: (datum[this.yKey as keyof Count] as number) ?? null,
|
||||
y: (datum[this.yKey as keyof TotalClients] as number) ?? null,
|
||||
new: this.getNewClients(datum),
|
||||
tooltipUpgrade: upgradeMessage,
|
||||
month: datum.month,
|
||||
@@ -123,7 +124,7 @@ export default class LineChart extends Component<Args> {
|
||||
}
|
||||
getNewClients(datum: MonthlyChartData) {
|
||||
if (!datum?.new_clients) return 0;
|
||||
return (datum?.new_clients[this.yKey as keyof Count] as number) || 0;
|
||||
return (datum?.new_clients[this.yKey as keyof TotalClients] as number) || 0;
|
||||
}
|
||||
|
||||
hasValue = (count: number | null) => {
|
||||
|
||||
@@ -9,7 +9,8 @@ import { BAR_WIDTH, formatNumbers } from 'vault/utils/chart-helpers';
|
||||
import { formatNumber } from 'core/helpers/format-number';
|
||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
|
||||
import type { Count, MonthlyChartData } from 'vault/vault/charts/client-counts';
|
||||
import type { MonthlyChartData } from 'vault/vault/charts/client-counts';
|
||||
import type { TotalClients } from 'core/utils/client-count-utils';
|
||||
|
||||
interface Args {
|
||||
data: MonthlyChartData[];
|
||||
@@ -51,7 +52,7 @@ export default class VerticalBarBasic extends Component<Args> {
|
||||
get chartData() {
|
||||
return this.args.data.map((d): ChartData => {
|
||||
const xValue = d.timestamp as string;
|
||||
const yValue = (d[this.args.dataKey as keyof Count] as number) ?? null;
|
||||
const yValue = (d[this.args.dataKey as keyof TotalClients] as number) ?? null;
|
||||
return {
|
||||
x: parseAPITimestamp(xValue, 'M/yy') as string,
|
||||
y: yValue,
|
||||
|
||||
@@ -162,7 +162,7 @@ export default class ClientsCountsPageComponent extends Component<Args> {
|
||||
}
|
||||
|
||||
@action
|
||||
onDateChange(dateObject: { dateType: string; monthIdx: string; year: string }) {
|
||||
onDateChange(dateObject: { dateType: string; monthIdx: number; year: number }) {
|
||||
const { dateType, monthIdx, year } = dateObject;
|
||||
const { config } = this.args;
|
||||
const currentTimestamp = getUnixTime(timestamp.now());
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
import ActivityComponent from '../activity';
|
||||
|
||||
import type {
|
||||
ClientActivityNewClients,
|
||||
ClientActivityMonthly,
|
||||
ClientActivityResourceByKey,
|
||||
} from 'vault/vault/models/clients/activity';
|
||||
ByMonthNewClients,
|
||||
MountNewClients,
|
||||
NamespaceByKey,
|
||||
NamespaceNewClients,
|
||||
} from 'core/utils/client-count-utils';
|
||||
|
||||
export default class ClientsTokenPageComponent extends ActivityComponent {
|
||||
legend = [
|
||||
@@ -19,10 +20,8 @@ export default class ClientsTokenPageComponent extends ActivityComponent {
|
||||
|
||||
calculateClientAverages(
|
||||
dataset:
|
||||
| ClientActivityMonthly[]
|
||||
| (ClientActivityResourceByKey | undefined)[]
|
||||
| (ClientActivityNewClients | undefined)[]
|
||||
| undefined
|
||||
| (NamespaceByKey | undefined)[]
|
||||
| (ByMonthNewClients | NamespaceNewClients | MountNewClients | undefined)[]
|
||||
) {
|
||||
return ['entity_clients', 'non_entity_clients'].reduce((count, key) => {
|
||||
const average = this.average(dataset, key);
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
import { compareAsc, getUnixTime, isWithinInterval } from 'date-fns';
|
||||
|
||||
// add new types here
|
||||
export const CLIENT_TYPES = [
|
||||
'acme_clients',
|
||||
'clients', // summation of total clients
|
||||
'entity_clients',
|
||||
'non_entity_clients',
|
||||
'secret_syncs',
|
||||
];
|
||||
|
||||
// returns array of VersionHistoryModels for noteworthy upgrades: 1.9, 1.10
|
||||
// that occurred between timestamps (i.e. queried activity data)
|
||||
export const filterVersionHistory = (versionHistory, start, end) => {
|
||||
if (versionHistory) {
|
||||
const upgrades = versionHistory.reduce((array, upgradeData) => {
|
||||
const includesVersion = (v) =>
|
||||
// only add first match, disregard subsequent patch releases of the same version
|
||||
upgradeData.version.match(v) && !array.some((d) => d.version.match(v));
|
||||
|
||||
['1.9', '1.10'].forEach((v) => {
|
||||
if (includesVersion(v)) array.push(upgradeData);
|
||||
});
|
||||
|
||||
return array;
|
||||
}, []);
|
||||
|
||||
// if there are noteworthy upgrades, only return those during queried date range
|
||||
if (upgrades.length) {
|
||||
const startDate = parseAPITimestamp(start);
|
||||
const endDate = parseAPITimestamp(end);
|
||||
return upgrades.filter(({ timestampInstalled }) => {
|
||||
const upgradeDate = parseAPITimestamp(timestampInstalled);
|
||||
return isWithinInterval(upgradeDate, { start: startDate, end: endDate });
|
||||
});
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const formatDateObject = (dateObj, isEnd) => {
|
||||
if (dateObj) {
|
||||
const { year, monthIdx } = dateObj;
|
||||
// day=0 for Date.UTC() returns the last day of the month before
|
||||
// increase monthIdx by one to get last day of queried month
|
||||
const utc = isEnd ? Date.UTC(year, monthIdx + 1, 0) : Date.UTC(year, monthIdx, 1);
|
||||
return getUnixTime(utc);
|
||||
}
|
||||
};
|
||||
|
||||
export const formatByMonths = (monthsArray) => {
|
||||
// the monthsArray will always include a timestamp of the month and either new/total client data or counts = null
|
||||
if (!Array.isArray(monthsArray)) return monthsArray;
|
||||
|
||||
const sortedPayload = sortMonthsByTimestamp(monthsArray);
|
||||
return sortedPayload?.map((m) => {
|
||||
const month = parseAPITimestamp(m.timestamp, 'M/yy');
|
||||
const totalClientsByNamespace = formatByNamespace(m.namespaces);
|
||||
const newClientsByNamespace = formatByNamespace(m.new_clients?.namespaces);
|
||||
return {
|
||||
month,
|
||||
timestamp: m.timestamp,
|
||||
...destructureClientCounts(m?.counts),
|
||||
namespaces: formatByNamespace(m.namespaces) || [],
|
||||
namespaces_by_key: namespaceArrayToObject(
|
||||
totalClientsByNamespace,
|
||||
newClientsByNamespace,
|
||||
month,
|
||||
m.timestamp
|
||||
),
|
||||
new_clients: {
|
||||
month,
|
||||
timestamp: m.timestamp,
|
||||
...destructureClientCounts(m?.new_clients?.counts),
|
||||
namespaces: formatByNamespace(m.new_clients?.namespaces) || [],
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const formatByNamespace = (namespaceArray) => {
|
||||
if (!Array.isArray(namespaceArray)) return namespaceArray;
|
||||
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;
|
||||
// data prior to adding mount granularity will still have a mounts key,
|
||||
// but with the value: "no mount accessor (pre-1.10 upgrade?)" (ref: vault/activity_log_util_common.go)
|
||||
// transform to an empty array for type consistency
|
||||
let mounts = [];
|
||||
if (Array.isArray(ns.mounts)) {
|
||||
mounts = ns.mounts.map((m) => ({ label: m['mount_path'], ...destructureClientCounts(m?.counts) }));
|
||||
}
|
||||
return {
|
||||
label,
|
||||
...destructureClientCounts(ns.counts),
|
||||
mounts,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// In 1.10 'distinct_entities' changed to 'entity_clients' and 'non_entity_tokens' to 'non_entity_clients'
|
||||
// these deprecated keys still exist on the response, so only return relevant keys here
|
||||
// when querying historical data the response will always contain the latest client type keys because the activity log is
|
||||
// constructed based on the version of Vault the user is on (key values will be 0)
|
||||
export const destructureClientCounts = (verboseObject) => {
|
||||
if (!verboseObject) return;
|
||||
return CLIENT_TYPES.reduce((newObj, clientType) => {
|
||||
newObj[clientType] = verboseObject[clientType];
|
||||
return newObj;
|
||||
}, {});
|
||||
};
|
||||
|
||||
export const sortMonthsByTimestamp = (monthsArray) => {
|
||||
const sortedPayload = [...monthsArray];
|
||||
return sortedPayload.sort((a, b) =>
|
||||
compareAsc(parseAPITimestamp(a.timestamp), parseAPITimestamp(b.timestamp))
|
||||
);
|
||||
};
|
||||
|
||||
export const namespaceArrayToObject = (totalClientsByNamespace, newClientsByNamespace, month, timestamp) => {
|
||||
if (!totalClientsByNamespace) return {}; // return if no data for that month
|
||||
// all 'new_client' data resides within a separate key of each month (see data structure below)
|
||||
// FIRST: iterate and nest respective 'new_clients' data within each namespace and mount object
|
||||
// note: this is happening within the month object
|
||||
const nestNewClientsWithinNamespace = totalClientsByNamespace?.map((ns) => {
|
||||
const newNamespaceCounts = newClientsByNamespace?.find((n) => n.label === ns.label);
|
||||
if (newNamespaceCounts) {
|
||||
const newClientsByMount = [...newNamespaceCounts.mounts];
|
||||
const nestNewClientsWithinMounts = ns.mounts?.map((mount) => {
|
||||
const new_clients = newClientsByMount?.find((m) => m.label === mount.label) || {};
|
||||
return {
|
||||
...mount,
|
||||
new_clients,
|
||||
};
|
||||
});
|
||||
return {
|
||||
...ns,
|
||||
new_clients: {
|
||||
label: ns.label,
|
||||
...destructureClientCounts(newNamespaceCounts),
|
||||
mounts: newClientsByMount,
|
||||
},
|
||||
mounts: [...nestNewClientsWithinMounts],
|
||||
};
|
||||
}
|
||||
return {
|
||||
...ns,
|
||||
new_clients: {},
|
||||
};
|
||||
});
|
||||
// SECOND: create a new object (namespace_by_key) in which each namespace label is a key
|
||||
const namespaces_by_key = {};
|
||||
nestNewClientsWithinNamespace?.forEach((namespaceObject) => {
|
||||
// THIRD: make another object within the namespace where each mount label is a key
|
||||
const mounts_by_key = {};
|
||||
namespaceObject.mounts.forEach((mountObject) => {
|
||||
mounts_by_key[mountObject.label] = {
|
||||
month,
|
||||
timestamp,
|
||||
...mountObject,
|
||||
new_clients: { month, ...mountObject.new_clients },
|
||||
};
|
||||
});
|
||||
|
||||
const { label, new_clients } = namespaceObject;
|
||||
namespaces_by_key[label] = {
|
||||
month,
|
||||
timestamp,
|
||||
...destructureClientCounts(namespaceObject),
|
||||
new_clients: { month, ...new_clients },
|
||||
mounts_by_key,
|
||||
};
|
||||
});
|
||||
return namespaces_by_key;
|
||||
/*
|
||||
structure of object returned
|
||||
namespace_by_key: {
|
||||
"namespace_label": {
|
||||
month: "3/22",
|
||||
clients: 32,
|
||||
entity_clients: 16,
|
||||
non_entity_clients: 16,
|
||||
new_clients: {
|
||||
month: "3/22",
|
||||
clients: 5,
|
||||
entity_clients: 2,
|
||||
non_entity_clients: 3,
|
||||
mounts: [...array of this namespace's mounts and their new client counts],
|
||||
},
|
||||
mounts_by_key: {
|
||||
"mount_label": {
|
||||
month: "3/22",
|
||||
clients: 3,
|
||||
entity_clients: 2,
|
||||
non_entity_clients: 1,
|
||||
new_clients: {
|
||||
month: "3/22",
|
||||
clients: 5,
|
||||
entity_clients: 2,
|
||||
non_entity_clients: 3,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
*/
|
||||
};
|
||||
295
ui/lib/core/addon/utils/client-count-utils.ts
Normal file
295
ui/lib/core/addon/utils/client-count-utils.ts
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
import { compareAsc, getUnixTime, isWithinInterval } from 'date-fns';
|
||||
|
||||
import type ClientsVersionHistoryModel from 'vault/vault/models/clients/version-history';
|
||||
|
||||
/*
|
||||
The client count utils are responsible for serializing the sys/internal/counters/activity API response
|
||||
The initial API response shape and serialized types are defined below.
|
||||
|
||||
To help visualize there are sample responses in ui/tests/helpers/clients.js
|
||||
*/
|
||||
|
||||
// add new types here
|
||||
export const CLIENT_TYPES = [
|
||||
'acme_clients',
|
||||
'clients', // summation of total clients
|
||||
'entity_clients',
|
||||
'non_entity_clients',
|
||||
'secret_syncs',
|
||||
] as const;
|
||||
|
||||
type ClientTypes = (typeof CLIENT_TYPES)[number];
|
||||
|
||||
// returns array of VersionHistoryModels for noteworthy upgrades: 1.9, 1.10
|
||||
// that occurred between timestamps (i.e. queried activity data)
|
||||
export const filterVersionHistory = (
|
||||
versionHistory: ClientsVersionHistoryModel[],
|
||||
start: string,
|
||||
end: string
|
||||
) => {
|
||||
if (versionHistory) {
|
||||
const upgrades = versionHistory.reduce((array: ClientsVersionHistoryModel[], upgradeData) => {
|
||||
const includesVersion = (v: string) =>
|
||||
// only add first match, disregard subsequent patch releases of the same version
|
||||
upgradeData.version.match(v) && !array.some((d: ClientsVersionHistoryModel) => d.version.match(v));
|
||||
|
||||
['1.9', '1.10'].forEach((v) => {
|
||||
if (includesVersion(v)) array.push(upgradeData);
|
||||
});
|
||||
|
||||
return array;
|
||||
}, []);
|
||||
|
||||
// if there are noteworthy upgrades, only return those during queried date range
|
||||
if (upgrades.length) {
|
||||
const startDate = parseAPITimestamp(start) as Date;
|
||||
const endDate = parseAPITimestamp(end) as Date;
|
||||
return upgrades.filter(({ timestampInstalled }) => {
|
||||
const upgradeDate = parseAPITimestamp(timestampInstalled) as Date;
|
||||
return isWithinInterval(upgradeDate, { start: startDate, end: endDate });
|
||||
});
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const formatDateObject = (dateObj: { monthIdx: number; year: number }, isEnd: boolean) => {
|
||||
const { year, monthIdx } = dateObj;
|
||||
// day=0 for Date.UTC() returns the last day of the month before
|
||||
// increase monthIdx by one to get last day of queried month
|
||||
const utc = isEnd ? Date.UTC(year, monthIdx + 1, 0) : Date.UTC(year, monthIdx, 1);
|
||||
return getUnixTime(utc);
|
||||
};
|
||||
|
||||
export const formatByMonths = (monthsArray: ActivityMonthBlock[] | EmptyActivityMonthBlock[]) => {
|
||||
const sortedPayload = sortMonthsByTimestamp(monthsArray);
|
||||
return sortedPayload?.map((m) => {
|
||||
const month = parseAPITimestamp(m.timestamp, 'M/yy') as string;
|
||||
const { timestamp } = m;
|
||||
// counts are null if there is no monthly data
|
||||
if (m.counts) {
|
||||
const totalClientsByNamespace = formatByNamespace(m.namespaces);
|
||||
const newClientsByNamespace = formatByNamespace(m.new_clients?.namespaces);
|
||||
return {
|
||||
month,
|
||||
timestamp,
|
||||
...destructureClientCounts(m.counts),
|
||||
namespaces: formatByNamespace(m.namespaces) || [],
|
||||
namespaces_by_key: namespaceArrayToObject(
|
||||
totalClientsByNamespace,
|
||||
newClientsByNamespace,
|
||||
month,
|
||||
m.timestamp
|
||||
),
|
||||
new_clients: {
|
||||
month,
|
||||
timestamp,
|
||||
...destructureClientCounts(m?.new_clients?.counts),
|
||||
namespaces: formatByNamespace(m.new_clients?.namespaces) || [],
|
||||
},
|
||||
};
|
||||
}
|
||||
// empty month
|
||||
return {
|
||||
month,
|
||||
timestamp,
|
||||
namespaces: [],
|
||||
namespaces_by_key: {},
|
||||
new_clients: { month, timestamp, namespaces: [] },
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const formatByNamespace = (namespaceArray: NamespaceObject[]) => {
|
||||
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;
|
||||
// data prior to adding mount granularity will still have a mounts array,
|
||||
// but the mount_path value will be "no mount accessor (pre-1.10 upgrade?)" (ref: vault/activity_log_util_common.go)
|
||||
// transform to an empty array for type consistency
|
||||
let mounts: MountClients[] | [] = [];
|
||||
if (Array.isArray(ns.mounts)) {
|
||||
mounts = ns.mounts.map((m) => ({ label: m.mount_path, ...destructureClientCounts(m.counts) }));
|
||||
}
|
||||
return {
|
||||
label,
|
||||
...destructureClientCounts(ns.counts),
|
||||
mounts,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// In 1.10 'distinct_entities' changed to 'entity_clients' and 'non_entity_tokens' to 'non_entity_clients'
|
||||
// these deprecated keys still exist on the response, so only return relevant keys here
|
||||
// when querying historical data the response will always contain the latest client type keys because the activity log is
|
||||
// constructed based on the version of Vault the user is on (key values will be 0)
|
||||
export const destructureClientCounts = (verboseObject: Counts | ByNamespaceClients) => {
|
||||
return CLIENT_TYPES.reduce((newObj: Record<ClientTypes, Counts[ClientTypes]>, clientType: ClientTypes) => {
|
||||
newObj[clientType] = verboseObject[clientType];
|
||||
return newObj;
|
||||
}, {} as Record<ClientTypes, Counts[ClientTypes]>);
|
||||
};
|
||||
|
||||
export const sortMonthsByTimestamp = (monthsArray: ActivityMonthBlock[] | EmptyActivityMonthBlock[]) => {
|
||||
const sortedPayload = [...monthsArray];
|
||||
return sortedPayload.sort((a, b) =>
|
||||
compareAsc(parseAPITimestamp(a.timestamp) as Date, parseAPITimestamp(b.timestamp) as Date)
|
||||
);
|
||||
};
|
||||
|
||||
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'],
|
||||
month: string,
|
||||
timestamp: string
|
||||
) => {
|
||||
// namespaces_by_key is used to filter monthly activity data by namespace
|
||||
// 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 newNsClients = monthNew?.find((n) => n.label === ns.label);
|
||||
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, ...newMountClients },
|
||||
};
|
||||
}
|
||||
return mountObj;
|
||||
}, {} as { [key: string]: MountByKey });
|
||||
|
||||
nsObject[ns.label] = {
|
||||
...destructureClientCounts(ns),
|
||||
timestamp,
|
||||
month,
|
||||
new_clients: { month, ...newNsClients },
|
||||
mounts_by_key,
|
||||
};
|
||||
}
|
||||
return nsObject;
|
||||
}, {});
|
||||
|
||||
return namespaces_by_key;
|
||||
};
|
||||
|
||||
// type guards for conditionals
|
||||
export function hasMountsKey(
|
||||
obj: ByMonthNewClients | NamespaceNewClients | MountNewClients
|
||||
): obj is NamespaceNewClients {
|
||||
return 'mounts' in obj;
|
||||
}
|
||||
|
||||
export function hasNamespacesKey(
|
||||
obj: ByMonthNewClients | NamespaceNewClients | MountNewClients
|
||||
): obj is ByMonthNewClients {
|
||||
return 'namespaces' in obj;
|
||||
}
|
||||
|
||||
// TYPES RETURNED BY UTILS (serialized)
|
||||
|
||||
export interface TotalClients {
|
||||
clients: number;
|
||||
entity_clients: number;
|
||||
non_entity_clients: number;
|
||||
secret_syncs: number;
|
||||
acme_clients: number;
|
||||
}
|
||||
|
||||
export interface ByNamespaceClients extends TotalClients {
|
||||
label: string;
|
||||
mounts: MountClients[];
|
||||
}
|
||||
|
||||
export interface MountClients extends TotalClients {
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface ByMonthClients extends TotalClients {
|
||||
month: string;
|
||||
timestamp: string;
|
||||
namespaces: ByNamespaceClients[];
|
||||
namespaces_by_key: { [key: string]: NamespaceByKey };
|
||||
new_clients: ByMonthNewClients;
|
||||
}
|
||||
export interface ByMonthNewClients extends TotalClients {
|
||||
month: string;
|
||||
timestamp: string;
|
||||
namespaces: ByNamespaceClients[];
|
||||
}
|
||||
|
||||
export interface NamespaceByKey extends TotalClients {
|
||||
month: string;
|
||||
timestamp: string;
|
||||
mounts_by_key: { [key: string]: MountByKey };
|
||||
new_clients: NamespaceNewClients;
|
||||
}
|
||||
|
||||
export interface NamespaceNewClients extends TotalClients {
|
||||
month: string;
|
||||
label: string;
|
||||
mounts: MountClients[];
|
||||
}
|
||||
|
||||
export interface MountByKey extends TotalClients {
|
||||
month: string;
|
||||
timestamp: string;
|
||||
label: string;
|
||||
new_clients: MountNewClients;
|
||||
}
|
||||
|
||||
export interface MountNewClients extends TotalClients {
|
||||
month: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// API RESPONSE SHAPE (prior to serialization)
|
||||
|
||||
export interface NamespaceObject {
|
||||
namespace_id: string;
|
||||
namespace_path: string;
|
||||
counts: Counts;
|
||||
mounts: { mount_path: string; counts: Counts }[];
|
||||
}
|
||||
|
||||
export interface ActivityMonthBlock {
|
||||
timestamp: string; // YYYY-MM-01T00:00:00Z (always the first day of the month)
|
||||
counts: Counts;
|
||||
namespaces: NamespaceObject[];
|
||||
new_clients: {
|
||||
counts: Counts;
|
||||
namespaces: NamespaceObject[];
|
||||
timestamp: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface EmptyActivityMonthBlock {
|
||||
timestamp: string; // YYYY-MM-01T00:00:00Z (always the first day of the month)
|
||||
counts: null;
|
||||
namespaces: null;
|
||||
new_clients: null;
|
||||
}
|
||||
|
||||
export interface Counts {
|
||||
acme_clients: number;
|
||||
clients: number;
|
||||
distinct_entities: number;
|
||||
entity_clients: number;
|
||||
non_entity_clients: number;
|
||||
non_entity_tokens: number;
|
||||
secret_syncs: number;
|
||||
}
|
||||
@@ -110,8 +110,10 @@ function generateMonths(startDate, endDate, namespaces) {
|
||||
const numberOfMonths = differenceInCalendarMonths(endDateObject, startDateObject) + 1;
|
||||
const months = [];
|
||||
|
||||
// only generate monthly block if queried dates span an upgrade
|
||||
if (isWithinInterval(UPGRADE_DATE, { start: startDateObject, end: endDateObject })) {
|
||||
// only generate monthly block if queried dates span or follow upgrade to 1.10
|
||||
const upgradeWithin = isWithinInterval(UPGRADE_DATE, { start: startDateObject, end: endDateObject });
|
||||
const upgradeAfter = isAfter(startDateObject, UPGRADE_DATE);
|
||||
if (upgradeWithin || upgradeAfter) {
|
||||
for (let i = 0; i < numberOfMonths; i++) {
|
||||
const month = addMonths(startOfMonth(startDateObject), i);
|
||||
const hasNoData = isBefore(month, UPGRADE_DATE) && !isSameMonth(month, UPGRADE_DATE);
|
||||
|
||||
@@ -193,7 +193,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
{
|
||||
namespace_id: '81ry61',
|
||||
namespace_path: 'ns/1',
|
||||
namespace_path: 'ns1',
|
||||
counts: {
|
||||
distinct_entities: 783,
|
||||
entity_clients: 783,
|
||||
@@ -315,7 +315,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
{
|
||||
namespace_id: '81ry61',
|
||||
namespace_path: 'ns/1',
|
||||
namespace_path: 'ns1',
|
||||
counts: {
|
||||
distinct_entities: 50,
|
||||
entity_clients: 50,
|
||||
@@ -378,7 +378,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: '81ry61',
|
||||
namespace_path: 'ns/1',
|
||||
namespace_path: 'ns1',
|
||||
counts: {
|
||||
distinct_entities: 30,
|
||||
entity_clients: 30,
|
||||
@@ -493,6 +493,174 @@ export const ACTIVITY_RESPONSE_STUB = {
|
||||
},
|
||||
};
|
||||
|
||||
// combined activity data before and after 1.10 upgrade when Vault added mount attribution
|
||||
export const MIXED_ACTIVITY_RESPONSE_STUB = {
|
||||
start_time: '2024-03-01T00:00:00Z',
|
||||
end_time: '2024-04-30T23:59:59Z',
|
||||
total: {
|
||||
acme_clients: 0,
|
||||
clients: 3,
|
||||
distinct_entities: 3,
|
||||
entity_clients: 3,
|
||||
non_entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
by_namespace: [
|
||||
{
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 3,
|
||||
distinct_entities: 3,
|
||||
entity_clients: 3,
|
||||
non_entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 2,
|
||||
distinct_entities: 2,
|
||||
entity_clients: 2,
|
||||
non_entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
mount_path: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
},
|
||||
{
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 1,
|
||||
distinct_entities: 1,
|
||||
entity_clients: 1,
|
||||
non_entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
mount_path: 'auth/u/',
|
||||
},
|
||||
],
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
},
|
||||
],
|
||||
months: [
|
||||
{
|
||||
counts: null,
|
||||
namespaces: null,
|
||||
new_clients: null,
|
||||
timestamp: '2024-03-01T00:00:00Z',
|
||||
},
|
||||
{
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 3,
|
||||
distinct_entities: 0,
|
||||
entity_clients: 3,
|
||||
non_entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
namespaces: [
|
||||
{
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 3,
|
||||
distinct_entities: 0,
|
||||
entity_clients: 3,
|
||||
non_entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 2,
|
||||
distinct_entities: 0,
|
||||
entity_clients: 2,
|
||||
non_entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
mount_path: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
},
|
||||
{
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 1,
|
||||
distinct_entities: 0,
|
||||
entity_clients: 1,
|
||||
non_entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
mount_path: 'auth/u/',
|
||||
},
|
||||
],
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
},
|
||||
],
|
||||
new_clients: {
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 3,
|
||||
distinct_entities: 0,
|
||||
entity_clients: 3,
|
||||
non_entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
namespaces: [
|
||||
{
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 3,
|
||||
distinct_entities: 0,
|
||||
entity_clients: 3,
|
||||
non_entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 2,
|
||||
distinct_entities: 0,
|
||||
entity_clients: 2,
|
||||
non_entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
mount_path: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
},
|
||||
{
|
||||
counts: {
|
||||
acme_clients: 0,
|
||||
clients: 1,
|
||||
distinct_entities: 0,
|
||||
entity_clients: 1,
|
||||
non_entity_clients: 0,
|
||||
non_entity_tokens: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
mount_path: 'auth/u/',
|
||||
},
|
||||
],
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
timestamp: '2024-04-01T00:00:00Z',
|
||||
},
|
||||
],
|
||||
};
|
||||
// format returned by model hook in routes/vault/cluster/clients.ts
|
||||
export const VERSION_HISTORY = [
|
||||
{
|
||||
@@ -560,7 +728,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'ns/1',
|
||||
label: 'ns1',
|
||||
clients: 2376,
|
||||
entity_clients: 783,
|
||||
non_entity_clients: 1193,
|
||||
@@ -649,7 +817,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'ns/1',
|
||||
label: 'ns1',
|
||||
clients: 3085,
|
||||
entity_clients: 50,
|
||||
non_entity_clients: 140,
|
||||
@@ -787,7 +955,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
},
|
||||
},
|
||||
},
|
||||
'ns/1': {
|
||||
ns1: {
|
||||
month: '9/23',
|
||||
timestamp: '2023-09-01T00:00:00Z',
|
||||
clients: 3085,
|
||||
@@ -797,7 +965,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
acme_clients: 125,
|
||||
new_clients: {
|
||||
month: '9/23',
|
||||
label: 'ns/1',
|
||||
label: 'ns1',
|
||||
clients: 222,
|
||||
entity_clients: 30,
|
||||
non_entity_clients: 62,
|
||||
@@ -901,7 +1069,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
||||
acme_clients: 50,
|
||||
namespaces: [
|
||||
{
|
||||
label: 'ns/1',
|
||||
label: 'ns1',
|
||||
clients: 222,
|
||||
entity_clients: 30,
|
||||
non_entity_clients: 62,
|
||||
|
||||
@@ -8,59 +8,37 @@ 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 sinon from 'sinon';
|
||||
import { LICENSE_START, STATIC_NOW } from 'vault/mirage/handlers/clients';
|
||||
import timestamp from 'core/utils/timestamp';
|
||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||
import { ACTIVITY_RESPONSE_STUB } from 'vault/tests/helpers/clients';
|
||||
|
||||
module('Integration | Component | dashboard/client-count-card', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.before(function () {
|
||||
sinon.stub(timestamp, 'now').callsFake(() => STATIC_NOW);
|
||||
});
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.license = {
|
||||
startTime: '2018-04-03T14:15:30',
|
||||
startTime: LICENSE_START.toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
hooks.after(function () {
|
||||
timestamp.now.restore();
|
||||
});
|
||||
|
||||
test('it should display client count information', async function (assert) {
|
||||
assert.expect(9);
|
||||
this.server.get('sys/internal/counters/activity', () => {
|
||||
// this assertion should be hit twice, once initially and then again clicking 'refresh'
|
||||
assert.true(true, 'makes request to sys/internal/counters/activity');
|
||||
return {
|
||||
request_id: 'some-activity-id',
|
||||
data: {
|
||||
months: [
|
||||
{
|
||||
timestamp: '2023-08-01T00:00:00-07:00',
|
||||
counts: {},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {},
|
||||
mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
},
|
||||
],
|
||||
new_clients: {
|
||||
counts: {
|
||||
clients: 12,
|
||||
},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
clients: 12,
|
||||
},
|
||||
mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
total: {
|
||||
clients: 300417,
|
||||
entity_clients: 73150,
|
||||
non_entity_clients: 227267,
|
||||
},
|
||||
},
|
||||
data: ACTIVITY_RESPONSE_STUB,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -69,74 +47,15 @@ module('Integration | Component | dashboard/client-count-card', function (hooks)
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-label').hasText('Total');
|
||||
assert
|
||||
.dom('[data-test-stat-text="total-clients"] .stat-text')
|
||||
.hasText(
|
||||
`The number of clients in this billing period (Apr 2018 - ${parseAPITimestamp(
|
||||
timestamp.now().toISOString(),
|
||||
'MMM yyyy'
|
||||
)}).`
|
||||
);
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('300,417');
|
||||
.hasText('The number of clients in this billing period (Jul 2023 - Jan 2024).');
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('7,805');
|
||||
assert.dom('[data-test-stat-text="new-clients"] .stat-label').hasText('New');
|
||||
assert
|
||||
.dom('[data-test-stat-text="new-clients"] .stat-text')
|
||||
.hasText('The number of clients new to Vault in the current month.');
|
||||
assert.dom('[data-test-stat-text="new-clients"] .stat-value').hasText('12');
|
||||
this.server.get('sys/internal/counters/activity', () => {
|
||||
return {
|
||||
request_id: 'some-activity-id',
|
||||
data: {
|
||||
months: [
|
||||
{
|
||||
timestamp: '2023-09-01T00:00:00-07:00',
|
||||
counts: {},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {},
|
||||
mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
},
|
||||
],
|
||||
new_clients: {
|
||||
counts: {
|
||||
clients: 5,
|
||||
},
|
||||
namespaces: [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
clients: 12,
|
||||
},
|
||||
mounts: [{ mount_path: 'auth/up2/', counts: {} }],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
total: {
|
||||
clients: 120,
|
||||
entity_clients: 100,
|
||||
non_entity_clients: 100,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
assert.dom('[data-test-stat-text="new-clients"] .stat-value').hasText('336');
|
||||
|
||||
// fires second request to /activity
|
||||
await click('[data-test-refresh]');
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-label').hasText('Total');
|
||||
assert
|
||||
.dom('[data-test-stat-text="total-clients"] .stat-text')
|
||||
.hasText(
|
||||
`The number of clients in this billing period (Apr 2018 - ${parseAPITimestamp(
|
||||
timestamp.now().toISOString(),
|
||||
'MMM yyyy'
|
||||
)}).`
|
||||
);
|
||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('120');
|
||||
assert.dom('[data-test-stat-text="new-clients"] .stat-label').hasText('New');
|
||||
assert
|
||||
.dom('[data-test-stat-text="new-clients"] .stat-text')
|
||||
.hasText('The number of clients new to Vault in the current month.');
|
||||
assert.dom('[data-test-stat-text="new-clients"] .stat-value').hasText('5');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { LICENSE_START } from 'vault/mirage/handlers/clients';
|
||||
import {
|
||||
ACTIVITY_RESPONSE_STUB as RESPONSE,
|
||||
MIXED_ACTIVITY_RESPONSE_STUB as MIXED_RESPONSE,
|
||||
VERSION_HISTORY,
|
||||
SERIALIZED_ACTIVITY_RESPONSE,
|
||||
} from 'vault/tests/helpers/clients';
|
||||
@@ -29,7 +30,7 @@ in a serializer test for easier debugging
|
||||
module('Integration | Util | client count utils', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('filterVersionHistory: returns version data for relevant upgrades that occurred during date range', async function (assert) {
|
||||
test('filterVersionHistory: it returns version data for relevant upgrades that occurred during date range', async function (assert) {
|
||||
assert.expect(2);
|
||||
// LICENSE_START is '2023-07-02T00:00:00Z'
|
||||
const original = [...VERSION_HISTORY];
|
||||
@@ -56,8 +57,8 @@ module('Integration | Util | client count utils', function (hooks) {
|
||||
assert.propEqual(VERSION_HISTORY, original, 'it does not modify original array');
|
||||
});
|
||||
|
||||
test('formatByMonths: formats the months array', async function (assert) {
|
||||
assert.expect(4);
|
||||
test('formatByMonths: it formats the months array', async function (assert) {
|
||||
assert.expect(5);
|
||||
const original = [...RESPONSE.months];
|
||||
|
||||
const [formattedNoData, formattedWithActivity] = formatByMonths(RESPONSE.months);
|
||||
@@ -79,9 +80,10 @@ module('Integration | Util | client count utils', function (hooks) {
|
||||
'it formats new_clients block for months with data'
|
||||
);
|
||||
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');
|
||||
});
|
||||
|
||||
test('formatByNamespace: formats namespace array with mounts', async function (assert) {
|
||||
test('formatByNamespace: it formats namespace array with mounts', async function (assert) {
|
||||
assert.expect(3);
|
||||
const original = [...RESPONSE.by_namespace];
|
||||
const [formattedRoot, formattedNs1] = formatByNamespace(RESPONSE.by_namespace);
|
||||
@@ -92,39 +94,7 @@ module('Integration | Util | client count utils', function (hooks) {
|
||||
assert.propEqual(RESPONSE.by_namespace, original, 'it does not modify original by_namespace array');
|
||||
});
|
||||
|
||||
test('formatByNamespace: formats namespace array with no mounts (activity log data < 1.10)', async function (assert) {
|
||||
assert.expect(1);
|
||||
const noMounts = [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 10,
|
||||
entity_clients: 10,
|
||||
non_entity_tokens: 20,
|
||||
non_entity_clients: 20,
|
||||
secret_syncs: 0,
|
||||
acme_clients: 0,
|
||||
clients: 30,
|
||||
},
|
||||
mounts: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
},
|
||||
];
|
||||
const expected = [
|
||||
{
|
||||
acme_clients: 0,
|
||||
clients: 30,
|
||||
entity_clients: 10,
|
||||
label: 'root',
|
||||
mounts: [],
|
||||
non_entity_clients: 20,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
];
|
||||
assert.propEqual(formatByNamespace(noMounts), expected, 'it formats namespace without mounts');
|
||||
});
|
||||
|
||||
test('destructureClientCounts: homogenizes key names when both old and new keys exist, or just old key names', async function (assert) {
|
||||
test('destructureClientCounts: it returns relevant key names when both old and new keys exist', async function (assert) {
|
||||
assert.expect(2);
|
||||
const original = { ...RESPONSE.total };
|
||||
const expected = {
|
||||
@@ -148,29 +118,250 @@ module('Integration | Util | client count utils', function (hooks) {
|
||||
assert.propEqual(RESPONSE.months, original, 'it does not modify original array');
|
||||
});
|
||||
|
||||
test('namespaceArrayToObject: it generates namespaces_by_key without modifying original', async function (assert) {
|
||||
assert.expect(3);
|
||||
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
|
||||
const { namespaces, new_clients } = RESPONSE.months[1];
|
||||
// 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 byNamespaceKeyObject = namespaceArrayToObject(
|
||||
formatByNamespace(namespaces),
|
||||
formatByNamespace(new_clients.namespaces),
|
||||
const expectedObject = SERIALIZED_ACTIVITY_RESPONSE.by_month[1].namespaces_by_key;
|
||||
const formattedTotal = formatByNamespace(RESPONSE.months[1].namespaces);
|
||||
|
||||
const testObject = namespaceArrayToObject(
|
||||
formattedTotal,
|
||||
formatByNamespace(RESPONSE.months[1].new_clients.namespaces),
|
||||
'9/23',
|
||||
'2023-09-01T00:00:00Z'
|
||||
);
|
||||
|
||||
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(
|
||||
byNamespaceKeyObject,
|
||||
SERIALIZED_ACTIVITY_RESPONSE.by_month[1].namespaces_by_key,
|
||||
'it returns object with namespaces by key and includes mounts_by_key'
|
||||
);
|
||||
assert.propEqual(
|
||||
namespaceArrayToObject(null, null, '10/21', 'timestamp-here'),
|
||||
namespaceArrayToObject(formattedTotal, formatByNamespace([]), '9/23', '2023-09-01T00:00:00Z'),
|
||||
{},
|
||||
'returns an empty object when monthByNamespace = null'
|
||||
'returns an empty object when there are no new clients '
|
||||
);
|
||||
assert.propEqual(RESPONSE.months[1], original, 'it does not modify original month data');
|
||||
});
|
||||
|
||||
// TESTS FOR COMBINED ACTIVITY DATA - no mount attribution < 1.10
|
||||
test('it formats the namespaces array with no mount attribution (activity log data < 1.10)', async function (assert) {
|
||||
assert.expect(2);
|
||||
const noMounts = [
|
||||
{
|
||||
namespace_id: 'root',
|
||||
namespace_path: '',
|
||||
counts: {
|
||||
distinct_entities: 10,
|
||||
entity_clients: 10,
|
||||
non_entity_tokens: 20,
|
||||
non_entity_clients: 20,
|
||||
secret_syncs: 0,
|
||||
acme_clients: 0,
|
||||
clients: 30,
|
||||
},
|
||||
mounts: [
|
||||
{
|
||||
counts: {
|
||||
distinct_entities: 10,
|
||||
entity_clients: 10,
|
||||
non_entity_tokens: 20,
|
||||
non_entity_clients: 20,
|
||||
secret_syncs: 0,
|
||||
acme_clients: 0,
|
||||
clients: 30,
|
||||
},
|
||||
mount_path: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const expected = [
|
||||
{
|
||||
acme_clients: 0,
|
||||
clients: 30,
|
||||
entity_clients: 10,
|
||||
label: 'root',
|
||||
mounts: [
|
||||
{
|
||||
acme_clients: 0,
|
||||
clients: 30,
|
||||
entity_clients: 10,
|
||||
label: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
non_entity_clients: 20,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
],
|
||||
non_entity_clients: 20,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
];
|
||||
assert.propEqual(formatByNamespace(noMounts), expected, 'it formats namespace without mounts');
|
||||
assert.propEqual(formatByNamespace([]), [], 'it returns an empty array if the by_namespace key is empty');
|
||||
});
|
||||
|
||||
test('it formats the months array with mixed activity data', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
const [, formattedWithActivity] = formatByMonths(MIXED_RESPONSE.months);
|
||||
// mirage isn't set up to generate mixed data, so hardcoding the expected responses here
|
||||
assert.propEqual(
|
||||
formattedWithActivity.namespaces,
|
||||
[
|
||||
{
|
||||
acme_clients: 0,
|
||||
clients: 3,
|
||||
entity_clients: 3,
|
||||
label: 'root',
|
||||
mounts: [
|
||||
{
|
||||
acme_clients: 0,
|
||||
clients: 2,
|
||||
entity_clients: 2,
|
||||
label: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
acme_clients: 0,
|
||||
clients: 1,
|
||||
entity_clients: 1,
|
||||
label: 'auth/u/',
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
],
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
],
|
||||
'it formats combined data for monthly namespaces spanning upgrade to 1.10'
|
||||
);
|
||||
assert.propEqual(
|
||||
formattedWithActivity.new_clients,
|
||||
{
|
||||
acme_clients: 0,
|
||||
clients: 3,
|
||||
entity_clients: 3,
|
||||
month: '4/24',
|
||||
namespaces: [
|
||||
{
|
||||
acme_clients: 0,
|
||||
clients: 3,
|
||||
entity_clients: 3,
|
||||
label: 'root',
|
||||
mounts: [
|
||||
{
|
||||
acme_clients: 0,
|
||||
clients: 2,
|
||||
entity_clients: 2,
|
||||
label: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
acme_clients: 0,
|
||||
clients: 1,
|
||||
entity_clients: 1,
|
||||
label: 'auth/u/',
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
],
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
],
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
timestamp: '2024-04-01T00:00:00Z',
|
||||
},
|
||||
'it formats combined data for monthly new_clients spanning upgrade to 1.10'
|
||||
);
|
||||
assert.propEqual(
|
||||
formattedWithActivity.namespaces_by_key,
|
||||
{
|
||||
root: {
|
||||
acme_clients: 0,
|
||||
clients: 3,
|
||||
entity_clients: 3,
|
||||
month: '4/24',
|
||||
mounts_by_key: {
|
||||
'auth/u/': {
|
||||
acme_clients: 0,
|
||||
clients: 1,
|
||||
entity_clients: 1,
|
||||
label: 'auth/u/',
|
||||
month: '4/24',
|
||||
new_clients: {
|
||||
acme_clients: 0,
|
||||
clients: 1,
|
||||
entity_clients: 1,
|
||||
label: 'auth/u/',
|
||||
month: '4/24',
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
timestamp: '2024-04-01T00:00:00Z',
|
||||
},
|
||||
'no mount accessor (pre-1.10 upgrade?)': {
|
||||
acme_clients: 0,
|
||||
clients: 2,
|
||||
entity_clients: 2,
|
||||
label: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
month: '4/24',
|
||||
new_clients: {
|
||||
acme_clients: 0,
|
||||
clients: 2,
|
||||
entity_clients: 2,
|
||||
label: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
month: '4/24',
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
timestamp: '2024-04-01T00:00:00Z',
|
||||
},
|
||||
},
|
||||
new_clients: {
|
||||
acme_clients: 0,
|
||||
clients: 3,
|
||||
entity_clients: 3,
|
||||
label: 'root',
|
||||
month: '4/24',
|
||||
mounts: [
|
||||
{
|
||||
acme_clients: 0,
|
||||
clients: 2,
|
||||
entity_clients: 2,
|
||||
label: 'no mount accessor (pre-1.10 upgrade?)',
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
{
|
||||
acme_clients: 0,
|
||||
clients: 1,
|
||||
entity_clients: 1,
|
||||
label: 'auth/u/',
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
],
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
},
|
||||
non_entity_clients: 0,
|
||||
secret_syncs: 0,
|
||||
timestamp: '2024-04-01T00:00:00Z',
|
||||
},
|
||||
},
|
||||
'it formats combined data for monthly namespaces_by_key spanning upgrade to 1.10'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
14
ui/types/vault/charts/client-counts.d.ts
vendored
14
ui/types/vault/charts/client-counts.d.ts
vendored
@@ -3,15 +3,11 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
// Count and EmptyCount are mutually exclusive
|
||||
import type { TotalClients } from 'core/utils/client-count-utils';
|
||||
|
||||
// TotalClients and EmptyCount are mutually exclusive
|
||||
// but that's hard to represent in an interface
|
||||
// so for now we just have both
|
||||
interface Count {
|
||||
clients?: number;
|
||||
entity_clients?: number;
|
||||
non_entity_clients?: number;
|
||||
secret_syncs?: number;
|
||||
}
|
||||
interface EmptyCount {
|
||||
count?: null;
|
||||
}
|
||||
@@ -20,6 +16,6 @@ interface Timestamp {
|
||||
timestamp: string; // ISO 8601
|
||||
}
|
||||
|
||||
export interface MonthlyChartData extends Count, EmptyCount, Timestamp {
|
||||
new_clients?: Count;
|
||||
export interface MonthlyChartData extends TotalClients, EmptyCount, Timestamp {
|
||||
new_clients?: TotalClients;
|
||||
}
|
||||
|
||||
41
ui/types/vault/models/clients/activity.d.ts
vendored
41
ui/types/vault/models/clients/activity.d.ts
vendored
@@ -5,45 +5,12 @@
|
||||
|
||||
import type { Model } from 'vault/app-types';
|
||||
|
||||
interface ClientActivityTotals {
|
||||
clients: number;
|
||||
entity_clients: number;
|
||||
non_entity_clients: number;
|
||||
secret_syncs: number;
|
||||
}
|
||||
|
||||
interface ClientActivityNestedCount extends ClientActivityTotals {
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ClientActivityNewClients extends ClientActivityTotals {
|
||||
month: string;
|
||||
mounts?: ClientActivityNestedCount[];
|
||||
namespaces?: ClientActivityNestedCount[];
|
||||
}
|
||||
|
||||
interface ClientActivityNamespace extends ClientActivityNestedCount {
|
||||
mounts: ClientActivityNestedCount[];
|
||||
}
|
||||
|
||||
interface ClientActivityResourceByKey extends ClientActivityTotals {
|
||||
month: 'string';
|
||||
mounts_by_key: { [key: string]: ClientActivityResourceByKey };
|
||||
new_clients: ClientActivityNewClients;
|
||||
}
|
||||
|
||||
interface ClientActivityMonthly extends ClientActivityTotals {
|
||||
month: string;
|
||||
timestamp: string;
|
||||
namespaces: ClientActivityNamespace[];
|
||||
namespaces_by_key: { [key: string]: ClientActivityResourceByKey };
|
||||
new_clients: ClientActivityNewClients;
|
||||
}
|
||||
import type { ByMonthClients, ByNamespaceClients, TotalClients } from 'core/utils/client-count-utils';
|
||||
|
||||
export default interface ClientsActivityModel extends Model {
|
||||
byMonth: ClientActivityMonthly[];
|
||||
byNamespace: ClientActivityNamespace[];
|
||||
total: ClientActivityTotals;
|
||||
byMonth: ByMonthClients[];
|
||||
byNamespace: ByNamespaceClients[];
|
||||
total: TotalClients;
|
||||
startTime: string;
|
||||
endTime: string;
|
||||
responseTimestamp: string;
|
||||
|
||||
Reference in New Issue
Block a user