mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 10:12:35 +00:00
* 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
296 lines
9.5 KiB
TypeScript
296 lines
9.5 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|