Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog/7751-aggregate-statistics.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
type: Added # One of: Added, Changed, Developer Experience, Deprecated, Docs, Fixed, Removed, Security
description: Adding aggregate statistics widgets to action center
pr: 7751 # PR number
labels: [] # Optional: ["high-risk", "db-migration"]
28 changes: 14 additions & 14 deletions clients/admin-ui/src/features/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,20 +279,6 @@ export const truncateUrl = (url: string, limit: number): string => {
}
};

/**
* Formats a number with a suffix for large numbers (K, M, etc.)
* @param num - The number to format
* @param digits - The number of digits to round to (default is 0)
* @returns The formatted number as a string
*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
*
* @example
* nFormatter(1111); // returns "1K"
* nFormatter(1111, 0); // returns "1K"
* nFormatter(1111, 1); // returns "1.1K"
* nFormatter(1111, 2); // returns "1.11K"
*/
/**
* Converts a snake_case string into a human-readable title.
* Known acronyms are uppercased; other words are title-cased.
Expand All @@ -310,6 +296,20 @@ export const snakeCaseToTitleCase = (
)
.join(" ");

/**
* Formats a number with a suffix for large numbers (K, M, etc.)
* @param num - The number to format
* @param digits - The number of digits to round to (default is 0)
* @returns The formatted number as a string
*
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
*
* @example
* nFormatter(1111); // returns "1K"
* nFormatter(1111, 0); // returns "1K"
* nFormatter(1111, 1); // returns "1.1K"
* nFormatter(1111, 2); // returns "1.11K"
*/
export const nFormatter = (num: number = 0, digits: number = 0) =>
Intl.NumberFormat("en", {
notation: "compact",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import classNames from "classnames";
import {
Badge,
BadgeProps,
Expand All @@ -14,10 +15,17 @@ import { PropsWithChildren } from "react";
import FixedLayout from "~/features/common/FixedLayout";
import { ACTION_CENTER_ROUTE } from "~/features/common/nav/routes";
import PageHeader from "~/features/common/PageHeader";
import { pluralize } from "~/features/common/utils";
import useActionCenterNavigation, {
ActionCenterRoute,
ActionCenterRouteConfig,
} from "~/features/data-discovery-and-detection/action-center/hooks/useActionCenterNavigation";
import { AggregateStatisticsResponse } from "~/types/api/models/AggregateStatisticsResponse";
import { APIMonitorType } from "~/types/api/models/APIMonitorType";

import { useGetAggretateStatisticsQuery } from "./action-center.slice";
import { MONITOR_UPDATE_LABELS } from "./constants";
import { MonitorStatCard, MonitorStatCardProps } from "./MonitorStatCard";

export interface ActionCenterLayoutProps {
monitorId?: string;
Expand All @@ -28,6 +36,88 @@ export interface ActionCenterLayoutProps {
};
}

const getMonitorUpdateName = (key: string, count: number) => {
const names = Object.entries(MONITOR_UPDATE_LABELS).find(
([k]) => key === k,
)?.[1];

if (!names) {
return key;
}
// names is [singular, plural]
return pluralize(count, ...names);
};

const MONITOR_TYPE_TO_LABEL: Record<APIMonitorType, string> = {
datastore: "Data stores",
infrastructure: "Infrastructure",
website: "Web monitors",
};

const MONITOR_TYPE_TO_PRIMARY_STATISTIC: Record<APIMonitorType, string> = {
datastore: "Resources approved",
infrastructure: "Total systems",
website: "Resources approved",
};

const MONITOR_TYPE_TO_NUMERIC_STATISTIC: Record<
APIMonitorType,
keyof AggregateStatisticsResponse
> = {
datastore: "status_counts",
infrastructure: "vendor_counts",
website: "status_counts",
};

const MONITOR_TYPE_TO_PERCENT_STATISTIC_KEY: Record<
APIMonitorType,
keyof NonNullable<AggregateStatisticsResponse["top_classifications"]>
> = {
datastore: "data_categories",
infrastructure: "data_uses",
website: "data_uses",
};

const MONITOR_TYPE_TO_PERECENT_STATISTIC_LABEL: Record<APIMonitorType, string> =
{
datastore: "Data categories",
infrastructure: "Data uses",
website: "Categories of consent",
};

export const transformStatisticsResponseToCardProps = (
response: AggregateStatisticsResponse,
): MonitorStatCardProps => ({
title: MONITOR_TYPE_TO_LABEL[response.monitor_type],
subtitle: `${response?.total_monitors} ${pluralize(response?.total_monitors ?? 0, "monitor", "monitors")}`,
primaryStat: {
label: MONITOR_TYPE_TO_PRIMARY_STATISTIC[response.monitor_type],
denominator: response?.approval_progress?.total,
numerator: response?.approval_progress?.approved,
percent: response?.approval_progress?.percentage,
},
numericStats: {
label: "Current status",
data: Object.entries(
response?.[MONITOR_TYPE_TO_NUMERIC_STATISTIC[response.monitor_type]] ??
[],
).flatMap(([label, value]) =>
value
? [{ label: getMonitorUpdateName(label, value), count: value }]
: [],
),
},
percentageStats: {
label: MONITOR_TYPE_TO_PERECENT_STATISTIC_LABEL[response.monitor_type],
data: (
response?.top_classifications?.[
MONITOR_TYPE_TO_PERCENT_STATISTIC_KEY[response.monitor_type]
] ?? []
).flatMap(({ name, percentage }) => ({ label: name, value: percentage })),
},
lastUpdated: response?.last_updated ?? undefined,
});

const ActionCenterLayout = ({
children,
monitorId,
Expand All @@ -39,6 +129,26 @@ const ActionCenterLayout = ({
activeItem,
setActiveItem,
} = useActionCenterNavigation(routeConfig);
const { data: websiteStatistics } = useGetAggretateStatisticsQuery(
{
monitor_type: "website",
monitor_config_id: monitorId,
},
{ refetchOnMountOrArgChange: true },
);

const { data: datastoreStatistics } = useGetAggretateStatisticsQuery(
{
monitor_type: "datastore",
monitor_config_id: monitorId,
},
{ refetchOnMountOrArgChange: true },
);

const { data: infrastructureStatistics } = useGetAggretateStatisticsQuery({
monitor_type: "infrastructure",
monitor_config_id: monitorId,
});

return (
<FixedLayout
Expand Down Expand Up @@ -68,6 +178,22 @@ const ActionCenterLayout = ({
)
}
/>
<div
className={classNames(
...["w-full", ...(!monitorId ? ["grid grid-cols-3 gap-4 pb-4"] : [])],
)}
>
{[infrastructureStatistics, datastoreStatistics, websiteStatistics].map(
(response) =>
response && (response.total_monitors ?? 0) > 0 ? (
<MonitorStatCard
{...transformStatisticsResponseToCardProps(response)}
key={response?.monitor_type}
compact={!!monitorId}
/>
) : null,
)}
</div>
<Menu
aria-label="Action center tabs"
mode="horizontal"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ export const MonitorResult = ({
type="link"
className="p-0"
icon={<OpenCloseArrow isOpen={isConfidenceRowExpanded} />}
iconPosition="end"
iconPlacement="end"
onClick={() =>
setIsConfidenceRowExpanded(!isConfidenceRowExpanded)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useMemo } from "react";
import { nFormatter, pluralize } from "~/features/common/utils";

import {
MONITOR_UPDATE_NAMES,
MONITOR_UPDATE_LABELS,
MONITOR_UPDATE_ORDER,
MONITOR_UPDATES_TO_IGNORE,
} from "./constants";
Expand All @@ -12,12 +12,15 @@ import { MonitorUpdates } from "./types";
type MonitorUpdateKey = keyof MonitorUpdates;

const getMonitorUpdateName = (key: string, count: number) => {
const names = MONITOR_UPDATE_NAMES.get(key as MonitorUpdateKey);
const names = Object.entries(MONITOR_UPDATE_LABELS).find(
([k]) => key === k,
)?.[1];

if (!names) {
return key;
}
// names is [singular, plural]
return pluralize(count, names[0], names[1]);
return pluralize(count, ...names);
};

export const MonitorResultDescription = ({
Expand Down
Loading
Loading