mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +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:
		
							
								
								
									
										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; | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 claire bontempo
					claire bontempo