Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: Fix client counts bug when no new clients #27352

Merged
merged 15 commits into from
Jun 6, 2024
Merged
3 changes: 3 additions & 0 deletions changelog/27352.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
ui: fix issue where a month without new clients breaks the client count dashboard
```
122 changes: 81 additions & 41 deletions ui/lib/core/addon/utils/client-count-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ export const formatDateObject = (dateObj: { monthIdx: number; year: number }, is
return getUnixTime(utc);
};

export const formatByMonths = (monthsArray: ActivityMonthBlock[] | EmptyActivityMonthBlock[]) => {
export const formatByMonths = (
monthsArray: (ActivityMonthBlock | EmptyActivityMonthBlock | NoNewClientsActivityMonthBlock)[]
) => {
const sortedPayload = sortMonthsByTimestamp(monthsArray);
return sortedPayload?.map((m) => {
const month = parseAPITimestamp(m.timestamp, 'M/yy') as string;
Expand All @@ -95,23 +97,28 @@ export const formatByMonths = (monthsArray: ActivityMonthBlock[] | EmptyActivity
if (m.counts) {
const totalClientsByNamespace = formatByNamespace(m.namespaces);
const newClientsByNamespace = formatByNamespace(m.new_clients?.namespaces);

let newClients: ByMonthNewClients = { month, timestamp, namespaces: [] };
if (m.new_clients?.counts) {
newClients = {
month,
timestamp,
...destructureClientCounts(m?.new_clients?.counts),
namespaces: formatByNamespace(m.new_clients?.namespaces),
};
}
return {
month,
timestamp,
...destructureClientCounts(m.counts),
namespaces: formatByNamespace(m.namespaces) || [],
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) || [],
},
new_clients: newClients,
};
}
// empty month
Expand All @@ -125,7 +132,8 @@ export const formatByMonths = (monthsArray: ActivityMonthBlock[] | EmptyActivity
});
};

export const formatByNamespace = (namespaceArray: NamespaceObject[]) => {
export const formatByNamespace = (namespaceArray: NamespaceObject[] | null): ByNamespaceClients[] => {
if (!Array.isArray(namespaceArray)) return [];
return namespaceArray.map((ns) => {
// i.e. 'namespace_path' is an empty string for 'root', so use namespace_id
const label = ns.namespace_path === '' ? ns.namespace_id : ns.namespace_path;
Expand Down Expand Up @@ -158,7 +166,9 @@ export const destructureClientCounts = (verboseObject: Counts | ByNamespaceClien
);
};

export const sortMonthsByTimestamp = (monthsArray: ActivityMonthBlock[] | EmptyActivityMonthBlock[]) => {
export const sortMonthsByTimestamp = (
monthsArray: (ActivityMonthBlock | EmptyActivityMonthBlock | NoNewClientsActivityMonthBlock)[]
) => {
const sortedPayload = [...monthsArray];
return sortedPayload.sort((a, b) =>
compareAsc(parseAPITimestamp(a.timestamp) as Date, parseAPITimestamp(b.timestamp) as Date)
Expand All @@ -168,44 +178,53 @@ export const sortMonthsByTimestamp = (monthsArray: ActivityMonthBlock[] | EmptyA
export const namespaceArrayToObject = (
monthTotals: ByNamespaceClients[],
// technically this arg (monthNew) is the same type as above, just nested inside monthly new clients
monthNew: ByMonthClients['new_clients']['namespaces'],
monthNew: ByMonthClients['new_clients']['namespaces'] | null,
month: string,
timestamp: string
) => {
// 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 keyedNs: NamespaceByKey = {
...destructureClientCounts(ns),
timestamp,
month,
mounts_by_key: {},
new_clients: {
month,
timestamp,
label: ns.label,
mounts: [],
},
};
const newNsClients = monthNew?.find((n) => n.label === ns.label);
// mounts_by_key is is used to filter further in a namespace and get monthly activity by mount
// it's an object inside the namespace block where the keys are mount paths
// and the values include new and total client counts for that mount in that month
keyedNs.mounts_by_key = ns.mounts.reduce(
(mountObj: { [key: string]: MountByKey }, mount) => {
const mountNewClients = newNsClients ? newNsClients.mounts.find((m) => m.label === mount.label) : {};
mountObj[mount.label] = {
...mount,
timestamp,
month,
new_clients: {
timestamp,
month,
label: mount.label,
...mountNewClients,
},
};

return mountObj;
},
{} as { [key: string]: MountByKey }
);
if (newNsClients) {
// mounts_by_key is is used to filter further in a namespace and get monthly activity by mount
// it's an object inside the namespace block where the keys are mount paths
// and the values include new and total client counts for that mount in that month
const mounts_by_key = ns.mounts.reduce(
(mountObj: { [key: string]: MountByKey }, mount) => {
const newMountClients = newNsClients.mounts.find((m) => m.label === mount.label);

if (newMountClients) {
mountObj[mount.label] = {
...mount,
timestamp,
month,
new_clients: { month, timestamp, ...newMountClients },
};
}
return mountObj;
},
{} as { [key: string]: MountByKey }
);

nsObject[ns.label] = {
...destructureClientCounts(ns),
timestamp,
month,
new_clients: { month, timestamp, ...newNsClients },
mounts_by_key,
};
keyedNs.new_clients = { month, timestamp, ...newNsClients };
}
nsObject[ns.label] = keyedNs;
return nsObject;
}, {});

Expand Down Expand Up @@ -239,6 +258,15 @@ export interface TotalClients {
acme_clients: number;
}

// extend this type when the counts are optional (eg for new clients)
interface TotalClientsSometimes {
clients?: number;
entity_clients?: number;
non_entity_clients?: number;
secret_syncs?: number;
acme_clients?: number;
}

export interface ByNamespaceClients extends TotalClients {
label: string;
mounts: MountClients[];
Expand All @@ -255,7 +283,9 @@ export interface ByMonthClients extends TotalClients {
namespaces_by_key: { [key: string]: NamespaceByKey };
new_clients: ByMonthNewClients;
}
export interface ByMonthNewClients extends TotalClients {

// clients numbers are only returned if month is of type ActivityMonthBlock
export interface ByMonthNewClients extends TotalClientsSometimes {
month: string;
timestamp: string;
namespaces: ByNamespaceClients[];
Expand All @@ -268,7 +298,7 @@ export interface NamespaceByKey extends TotalClients {
new_clients: NamespaceNewClients;
}

export interface NamespaceNewClients extends TotalClients {
export interface NamespaceNewClients extends TotalClientsSometimes {
month: string;
timestamp: string;
label: string;
Expand All @@ -282,7 +312,7 @@ export interface MountByKey extends TotalClients {
new_clients: MountNewClients;
}

export interface MountNewClients extends TotalClients {
export interface MountNewClients extends TotalClientsSometimes {
month: string;
timestamp: string;
label: string;
Expand All @@ -308,6 +338,16 @@ export interface ActivityMonthBlock {
};
}

export interface NoNewClientsActivityMonthBlock {
timestamp: string; // YYYY-MM-01T00:00:00Z (always the first day of the month)
counts: Counts;
namespaces: NamespaceObject[];
new_clients: {
counts: null;
namespaces: null;
};
}

export interface EmptyActivityMonthBlock {
timestamp: string; // YYYY-MM-01T00:00:00Z (always the first day of the month)
counts: null;
Expand Down
Loading
Loading