mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 02:02:43 +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 { isSameMonth, fromUnixTime } from 'date-fns';
|
||||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
||||||
import { calculateAverage } from 'vault/utils/chart-helpers';
|
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 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 ClientsVersionHistoryModel from 'vault/models/clients/version-history';
|
||||||
|
import type {
|
||||||
|
ByMonthNewClients,
|
||||||
|
MountNewClients,
|
||||||
|
NamespaceByKey,
|
||||||
|
NamespaceNewClients,
|
||||||
|
} from 'core/utils/client-count-utils';
|
||||||
|
|
||||||
interface Args {
|
interface Args {
|
||||||
isSecretsSyncActivated?: boolean;
|
isSecretsSyncActivated?: boolean;
|
||||||
@@ -33,10 +34,8 @@ interface Args {
|
|||||||
export default class ClientsActivityComponent extends Component<Args> {
|
export default class ClientsActivityComponent extends Component<Args> {
|
||||||
average = (
|
average = (
|
||||||
data:
|
data:
|
||||||
| ClientActivityMonthly[]
|
| (ByMonthNewClients | NamespaceNewClients | MountNewClients | undefined)[]
|
||||||
| (ClientActivityResourceByKey | undefined)[]
|
| (NamespaceByKey | undefined)[],
|
||||||
| (ClientActivityNewClients | undefined)[]
|
|
||||||
| undefined,
|
|
||||||
key: string
|
key: string
|
||||||
) => {
|
) => {
|
||||||
return calculateAverage(data, key);
|
return calculateAverage(data, key);
|
||||||
@@ -65,18 +64,18 @@ export default class ClientsActivityComponent extends Component<Args> {
|
|||||||
return activity.byMonth;
|
return activity.byMonth;
|
||||||
}
|
}
|
||||||
const namespaceData = 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);
|
.filter((d) => d !== undefined);
|
||||||
|
|
||||||
if (!mountPath) {
|
if (!mountPath) {
|
||||||
return namespaceData.length === 0 ? undefined : namespaceData;
|
return namespaceData || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const mountData = mountPath
|
const mountData = namespaceData
|
||||||
? namespaceData.map((namespace) => namespace?.mounts_by_key[mountPath]).filter((d) => d !== undefined)
|
?.map((namespace) => namespace?.mounts_by_key[mountPath])
|
||||||
: namespaceData;
|
.filter((d) => d !== undefined);
|
||||||
|
|
||||||
return mountData.length === 0 ? undefined : mountData;
|
return mountData || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
get filteredActivityByNamespace() {
|
get filteredActivityByNamespace() {
|
||||||
@@ -119,11 +118,13 @@ export default class ClientsActivityComponent extends Component<Args> {
|
|||||||
return filterVersionHistory(versionHistory, activity.startTime, activity.endTime);
|
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() {
|
get newClientCounts() {
|
||||||
if (this.isDateRange || !this.byMonthActivityData) {
|
if (this.isDateRange || this.byMonthActivityData.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.byMonthActivityData[0]?.new_clients;
|
return this.byMonthActivityData[0]?.new_clients;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,13 +141,14 @@ export default class ClientsActivityComponent extends Component<Args> {
|
|||||||
// new client data for horizontal bar chart
|
// new client data for horizontal bar chart
|
||||||
get newClientAttribution() {
|
get newClientAttribution() {
|
||||||
// new client attribution only available in a single, historical month (not a date range or current month)
|
// 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) {
|
const newCounts = this.newClientCounts;
|
||||||
return this.newClientCounts?.mounts || null;
|
if (this.args.namespace && hasMountsKey(newCounts)) return newCounts?.mounts;
|
||||||
} else {
|
|
||||||
return this.newClientCounts?.namespaces || null;
|
if (hasNamespacesKey(newCounts)) return newCounts?.namespaces;
|
||||||
}
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasAttributionData() {
|
get hasAttributionData() {
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import { parseAPITimestamp } from 'core/utils/date-formatters';
|
|||||||
import { format, isValid } from 'date-fns';
|
import { format, isValid } from 'date-fns';
|
||||||
import { debug } from '@ember/debug';
|
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 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 {
|
interface Args {
|
||||||
dataset: MonthlyChartData[];
|
dataset: MonthlyChartData[];
|
||||||
@@ -67,7 +68,7 @@ export default class LineChart extends Component<Args> {
|
|||||||
const upgradeMessage = this.getUpgradeMessage(datum);
|
const upgradeMessage = this.getUpgradeMessage(datum);
|
||||||
return {
|
return {
|
||||||
x: timestamp,
|
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),
|
new: this.getNewClients(datum),
|
||||||
tooltipUpgrade: upgradeMessage,
|
tooltipUpgrade: upgradeMessage,
|
||||||
month: datum.month,
|
month: datum.month,
|
||||||
@@ -123,7 +124,7 @@ export default class LineChart extends Component<Args> {
|
|||||||
}
|
}
|
||||||
getNewClients(datum: MonthlyChartData) {
|
getNewClients(datum: MonthlyChartData) {
|
||||||
if (!datum?.new_clients) return 0;
|
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) => {
|
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 { formatNumber } from 'core/helpers/format-number';
|
||||||
import { parseAPITimestamp } from 'core/utils/date-formatters';
|
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 {
|
interface Args {
|
||||||
data: MonthlyChartData[];
|
data: MonthlyChartData[];
|
||||||
@@ -51,7 +52,7 @@ export default class VerticalBarBasic extends Component<Args> {
|
|||||||
get chartData() {
|
get chartData() {
|
||||||
return this.args.data.map((d): ChartData => {
|
return this.args.data.map((d): ChartData => {
|
||||||
const xValue = d.timestamp as string;
|
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 {
|
return {
|
||||||
x: parseAPITimestamp(xValue, 'M/yy') as string,
|
x: parseAPITimestamp(xValue, 'M/yy') as string,
|
||||||
y: yValue,
|
y: yValue,
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export default class ClientsCountsPageComponent extends Component<Args> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
onDateChange(dateObject: { dateType: string; monthIdx: string; year: string }) {
|
onDateChange(dateObject: { dateType: string; monthIdx: number; year: number }) {
|
||||||
const { dateType, monthIdx, year } = dateObject;
|
const { dateType, monthIdx, year } = dateObject;
|
||||||
const { config } = this.args;
|
const { config } = this.args;
|
||||||
const currentTimestamp = getUnixTime(timestamp.now());
|
const currentTimestamp = getUnixTime(timestamp.now());
|
||||||
|
|||||||
@@ -6,10 +6,11 @@
|
|||||||
import ActivityComponent from '../activity';
|
import ActivityComponent from '../activity';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ClientActivityNewClients,
|
ByMonthNewClients,
|
||||||
ClientActivityMonthly,
|
MountNewClients,
|
||||||
ClientActivityResourceByKey,
|
NamespaceByKey,
|
||||||
} from 'vault/vault/models/clients/activity';
|
NamespaceNewClients,
|
||||||
|
} from 'core/utils/client-count-utils';
|
||||||
|
|
||||||
export default class ClientsTokenPageComponent extends ActivityComponent {
|
export default class ClientsTokenPageComponent extends ActivityComponent {
|
||||||
legend = [
|
legend = [
|
||||||
@@ -19,10 +20,8 @@ export default class ClientsTokenPageComponent extends ActivityComponent {
|
|||||||
|
|
||||||
calculateClientAverages(
|
calculateClientAverages(
|
||||||
dataset:
|
dataset:
|
||||||
| ClientActivityMonthly[]
|
| (NamespaceByKey | undefined)[]
|
||||||
| (ClientActivityResourceByKey | undefined)[]
|
| (ByMonthNewClients | NamespaceNewClients | MountNewClients | undefined)[]
|
||||||
| (ClientActivityNewClients | undefined)[]
|
|
||||||
| undefined
|
|
||||||
) {
|
) {
|
||||||
return ['entity_clients', 'non_entity_clients'].reduce((count, key) => {
|
return ['entity_clients', 'non_entity_clients'].reduce((count, key) => {
|
||||||
const average = this.average(dataset, 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 numberOfMonths = differenceInCalendarMonths(endDateObject, startDateObject) + 1;
|
||||||
const months = [];
|
const months = [];
|
||||||
|
|
||||||
// only generate monthly block if queried dates span an upgrade
|
// only generate monthly block if queried dates span or follow upgrade to 1.10
|
||||||
if (isWithinInterval(UPGRADE_DATE, { start: startDateObject, end: endDateObject })) {
|
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++) {
|
for (let i = 0; i < numberOfMonths; i++) {
|
||||||
const month = addMonths(startOfMonth(startDateObject), i);
|
const month = addMonths(startOfMonth(startDateObject), i);
|
||||||
const hasNoData = isBefore(month, UPGRADE_DATE) && !isSameMonth(month, UPGRADE_DATE);
|
const hasNoData = isBefore(month, UPGRADE_DATE) && !isSameMonth(month, UPGRADE_DATE);
|
||||||
|
|||||||
@@ -193,7 +193,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
namespace_id: '81ry61',
|
namespace_id: '81ry61',
|
||||||
namespace_path: 'ns/1',
|
namespace_path: 'ns1',
|
||||||
counts: {
|
counts: {
|
||||||
distinct_entities: 783,
|
distinct_entities: 783,
|
||||||
entity_clients: 783,
|
entity_clients: 783,
|
||||||
@@ -315,7 +315,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
namespace_id: '81ry61',
|
namespace_id: '81ry61',
|
||||||
namespace_path: 'ns/1',
|
namespace_path: 'ns1',
|
||||||
counts: {
|
counts: {
|
||||||
distinct_entities: 50,
|
distinct_entities: 50,
|
||||||
entity_clients: 50,
|
entity_clients: 50,
|
||||||
@@ -378,7 +378,7 @@ export const ACTIVITY_RESPONSE_STUB = {
|
|||||||
namespaces: [
|
namespaces: [
|
||||||
{
|
{
|
||||||
namespace_id: '81ry61',
|
namespace_id: '81ry61',
|
||||||
namespace_path: 'ns/1',
|
namespace_path: 'ns1',
|
||||||
counts: {
|
counts: {
|
||||||
distinct_entities: 30,
|
distinct_entities: 30,
|
||||||
entity_clients: 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
|
// format returned by model hook in routes/vault/cluster/clients.ts
|
||||||
export const VERSION_HISTORY = [
|
export const VERSION_HISTORY = [
|
||||||
{
|
{
|
||||||
@@ -560,7 +728,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'ns/1',
|
label: 'ns1',
|
||||||
clients: 2376,
|
clients: 2376,
|
||||||
entity_clients: 783,
|
entity_clients: 783,
|
||||||
non_entity_clients: 1193,
|
non_entity_clients: 1193,
|
||||||
@@ -649,7 +817,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'ns/1',
|
label: 'ns1',
|
||||||
clients: 3085,
|
clients: 3085,
|
||||||
entity_clients: 50,
|
entity_clients: 50,
|
||||||
non_entity_clients: 140,
|
non_entity_clients: 140,
|
||||||
@@ -787,7 +955,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'ns/1': {
|
ns1: {
|
||||||
month: '9/23',
|
month: '9/23',
|
||||||
timestamp: '2023-09-01T00:00:00Z',
|
timestamp: '2023-09-01T00:00:00Z',
|
||||||
clients: 3085,
|
clients: 3085,
|
||||||
@@ -797,7 +965,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||||||
acme_clients: 125,
|
acme_clients: 125,
|
||||||
new_clients: {
|
new_clients: {
|
||||||
month: '9/23',
|
month: '9/23',
|
||||||
label: 'ns/1',
|
label: 'ns1',
|
||||||
clients: 222,
|
clients: 222,
|
||||||
entity_clients: 30,
|
entity_clients: 30,
|
||||||
non_entity_clients: 62,
|
non_entity_clients: 62,
|
||||||
@@ -901,7 +1069,7 @@ export const SERIALIZED_ACTIVITY_RESPONSE = {
|
|||||||
acme_clients: 50,
|
acme_clients: 50,
|
||||||
namespaces: [
|
namespaces: [
|
||||||
{
|
{
|
||||||
label: 'ns/1',
|
label: 'ns1',
|
||||||
clients: 222,
|
clients: 222,
|
||||||
entity_clients: 30,
|
entity_clients: 30,
|
||||||
non_entity_clients: 62,
|
non_entity_clients: 62,
|
||||||
|
|||||||
@@ -8,59 +8,37 @@ import { setupRenderingTest } from 'vault/tests/helpers';
|
|||||||
import { render, click } from '@ember/test-helpers';
|
import { render, click } from '@ember/test-helpers';
|
||||||
import { hbs } from 'ember-cli-htmlbars';
|
import { hbs } from 'ember-cli-htmlbars';
|
||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
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 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) {
|
module('Integration | Component | dashboard/client-count-card', function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
setupMirage(hooks);
|
setupMirage(hooks);
|
||||||
|
|
||||||
|
hooks.before(function () {
|
||||||
|
sinon.stub(timestamp, 'now').callsFake(() => STATIC_NOW);
|
||||||
|
});
|
||||||
|
|
||||||
hooks.beforeEach(function () {
|
hooks.beforeEach(function () {
|
||||||
this.license = {
|
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) {
|
test('it should display client count information', async function (assert) {
|
||||||
|
assert.expect(9);
|
||||||
this.server.get('sys/internal/counters/activity', () => {
|
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 {
|
return {
|
||||||
request_id: 'some-activity-id',
|
request_id: 'some-activity-id',
|
||||||
data: {
|
data: ACTIVITY_RESPONSE_STUB,
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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-label').hasText('Total');
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-stat-text="total-clients"] .stat-text')
|
.dom('[data-test-stat-text="total-clients"] .stat-text')
|
||||||
.hasText(
|
.hasText('The number of clients in this billing period (Jul 2023 - Jan 2024).');
|
||||||
`The number of clients in this billing period (Apr 2018 - ${parseAPITimestamp(
|
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('7,805');
|
||||||
timestamp.now().toISOString(),
|
|
||||||
'MMM yyyy'
|
|
||||||
)}).`
|
|
||||||
);
|
|
||||||
assert.dom('[data-test-stat-text="total-clients"] .stat-value').hasText('300,417');
|
|
||||||
assert.dom('[data-test-stat-text="new-clients"] .stat-label').hasText('New');
|
assert.dom('[data-test-stat-text="new-clients"] .stat-label').hasText('New');
|
||||||
assert
|
assert
|
||||||
.dom('[data-test-stat-text="new-clients"] .stat-text')
|
.dom('[data-test-stat-text="new-clients"] .stat-text')
|
||||||
.hasText('The number of clients new to Vault in the current month.');
|
.hasText('The number of clients new to Vault in the current month.');
|
||||||
assert.dom('[data-test-stat-text="new-clients"] .stat-value').hasText('12');
|
assert.dom('[data-test-stat-text="new-clients"] .stat-value').hasText('336');
|
||||||
this.server.get('sys/internal/counters/activity', () => {
|
|
||||||
return {
|
// fires second request to /activity
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
await click('[data-test-refresh]');
|
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 { LICENSE_START } from 'vault/mirage/handlers/clients';
|
||||||
import {
|
import {
|
||||||
ACTIVITY_RESPONSE_STUB as RESPONSE,
|
ACTIVITY_RESPONSE_STUB as RESPONSE,
|
||||||
|
MIXED_ACTIVITY_RESPONSE_STUB as MIXED_RESPONSE,
|
||||||
VERSION_HISTORY,
|
VERSION_HISTORY,
|
||||||
SERIALIZED_ACTIVITY_RESPONSE,
|
SERIALIZED_ACTIVITY_RESPONSE,
|
||||||
} from 'vault/tests/helpers/clients';
|
} from 'vault/tests/helpers/clients';
|
||||||
@@ -29,7 +30,7 @@ in a serializer test for easier debugging
|
|||||||
module('Integration | Util | client count utils', function (hooks) {
|
module('Integration | Util | client count utils', function (hooks) {
|
||||||
setupTest(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);
|
assert.expect(2);
|
||||||
// LICENSE_START is '2023-07-02T00:00:00Z'
|
// LICENSE_START is '2023-07-02T00:00:00Z'
|
||||||
const original = [...VERSION_HISTORY];
|
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');
|
assert.propEqual(VERSION_HISTORY, original, 'it does not modify original array');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('formatByMonths: formats the months array', async function (assert) {
|
test('formatByMonths: it formats the months array', async function (assert) {
|
||||||
assert.expect(4);
|
assert.expect(5);
|
||||||
const original = [...RESPONSE.months];
|
const original = [...RESPONSE.months];
|
||||||
|
|
||||||
const [formattedNoData, formattedWithActivity] = formatByMonths(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'
|
'it formats new_clients block for months with data'
|
||||||
);
|
);
|
||||||
assert.propEqual(RESPONSE.months, original, 'it does not modify original months array');
|
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);
|
assert.expect(3);
|
||||||
const original = [...RESPONSE.by_namespace];
|
const original = [...RESPONSE.by_namespace];
|
||||||
const [formattedRoot, formattedNs1] = formatByNamespace(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');
|
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) {
|
test('destructureClientCounts: it returns relevant key names when both old and new keys exist', 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) {
|
|
||||||
assert.expect(2);
|
assert.expect(2);
|
||||||
const original = { ...RESPONSE.total };
|
const original = { ...RESPONSE.total };
|
||||||
const expected = {
|
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');
|
assert.propEqual(RESPONSE.months, original, 'it does not modify original array');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('namespaceArrayToObject: it generates namespaces_by_key without modifying original', async function (assert) {
|
test('namespaceArrayToObject: it returns namespaces_by_key and mounts_by_key', async function (assert) {
|
||||||
assert.expect(3);
|
assert.expect(5);
|
||||||
|
|
||||||
// month at 0-index has no data so use second month in array
|
// month at 0-index has no data so use second month in array, empty month format covered by formatByMonths test above
|
||||||
const { namespaces, new_clients } = RESPONSE.months[1];
|
|
||||||
const original = { ...RESPONSE.months[1] };
|
const original = { ...RESPONSE.months[1] };
|
||||||
const byNamespaceKeyObject = namespaceArrayToObject(
|
const expectedObject = SERIALIZED_ACTIVITY_RESPONSE.by_month[1].namespaces_by_key;
|
||||||
formatByNamespace(namespaces),
|
const formattedTotal = formatByNamespace(RESPONSE.months[1].namespaces);
|
||||||
formatByNamespace(new_clients.namespaces),
|
|
||||||
|
const testObject = namespaceArrayToObject(
|
||||||
|
formattedTotal,
|
||||||
|
formatByNamespace(RESPONSE.months[1].new_clients.namespaces),
|
||||||
'9/23',
|
'9/23',
|
||||||
'2023-09-01T00:00:00Z'
|
'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(
|
assert.propEqual(
|
||||||
byNamespaceKeyObject,
|
namespaceArrayToObject(formattedTotal, formatByNamespace([]), '9/23', '2023-09-01T00:00:00Z'),
|
||||||
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'),
|
|
||||||
{},
|
{},
|
||||||
'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');
|
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
|
* 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
|
// but that's hard to represent in an interface
|
||||||
// so for now we just have both
|
// so for now we just have both
|
||||||
interface Count {
|
|
||||||
clients?: number;
|
|
||||||
entity_clients?: number;
|
|
||||||
non_entity_clients?: number;
|
|
||||||
secret_syncs?: number;
|
|
||||||
}
|
|
||||||
interface EmptyCount {
|
interface EmptyCount {
|
||||||
count?: null;
|
count?: null;
|
||||||
}
|
}
|
||||||
@@ -20,6 +16,6 @@ interface Timestamp {
|
|||||||
timestamp: string; // ISO 8601
|
timestamp: string; // ISO 8601
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MonthlyChartData extends Count, EmptyCount, Timestamp {
|
export interface MonthlyChartData extends TotalClients, EmptyCount, Timestamp {
|
||||||
new_clients?: Count;
|
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';
|
import type { Model } from 'vault/app-types';
|
||||||
|
|
||||||
interface ClientActivityTotals {
|
import type { ByMonthClients, ByNamespaceClients, TotalClients } from 'core/utils/client-count-utils';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default interface ClientsActivityModel extends Model {
|
export default interface ClientsActivityModel extends Model {
|
||||||
byMonth: ClientActivityMonthly[];
|
byMonth: ByMonthClients[];
|
||||||
byNamespace: ClientActivityNamespace[];
|
byNamespace: ByNamespaceClients[];
|
||||||
total: ClientActivityTotals;
|
total: TotalClients;
|
||||||
startTime: string;
|
startTime: string;
|
||||||
endTime: string;
|
endTime: string;
|
||||||
responseTimestamp: string;
|
responseTimestamp: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user