diff --git a/changelog.d/fixes/utilization-account-chart-display.md b/changelog.d/fixes/utilization-account-chart-display.md
new file mode 100644
index 0000000000..eeec2cf14a
--- /dev/null
+++ b/changelog.d/fixes/utilization-account-chart-display.md
@@ -0,0 +1 @@
+- **fix(analytics):** resolve account email/name in Utilization Account Split chart and fix legend bleeding through tooltip ([#13029](https://github.com/diegosouzapw/OmniRoute/pull/13029)) — thanks @ZaimMarzuki
diff --git a/src/app/(dashboard)/dashboard/analytics/ProviderUtilizationTab.tsx b/src/app/(dashboard)/dashboard/analytics/ProviderUtilizationTab.tsx
index 8be4d821ca..e521441224 100644
--- a/src/app/(dashboard)/dashboard/analytics/ProviderUtilizationTab.tsx
+++ b/src/app/(dashboard)/dashboard/analytics/ProviderUtilizationTab.tsx
@@ -172,6 +172,25 @@ export default function ProviderUtilizationTab() {
const latestPoints = useMemo(() => getLatestPoints(data?.data ?? []), [data?.data]);
+ const resolveDisplayName = useCallback(
+ (key: string) => {
+ const colonIdx = key.indexOf(":");
+ const isConnectionKey = aggregateBy === "connection" && colonIdx !== -1;
+ if (isConnectionKey) {
+ const connectionId = key.slice(colonIdx + 1);
+ const connMeta = data?.connectionMeta?.[connectionId];
+ return getAccountDisplayName({
+ id: connectionId,
+ email: connMeta?.email,
+ name: connMeta?.name,
+ displayName: connMeta?.displayName,
+ });
+ }
+ return resolveProviderName(key, nodeMap);
+ },
+ [aggregateBy, data?.connectionMeta, nodeMap]
+ );
+
const hasData = Boolean(data?.data.length);
const [retrying, setRetrying] = useState(false);
@@ -303,7 +322,7 @@ export default function ProviderUtilizationTab() {
providers={data?.providers ?? []}
providerColors={providerColors}
range={range}
- resolveProviderName={resolveProviderName}
+ resolveProviderName={resolveDisplayName}
nodeMap={nodeMap}
formatTimestamp={formatTimestamp}
formatPercent={formatPercent}
@@ -321,15 +340,7 @@ export default function ProviderUtilizationTab() {
? point.provider.slice(0, colonIdx)
: point.provider;
const connectionId = isConnectionKey ? point.provider.slice(colonIdx + 1) : null;
- const connMeta = connectionId ? data?.connectionMeta?.[connectionId] : null;
- const cardTitle = isConnectionKey
- ? getAccountDisplayName({
- id: connectionId ?? undefined,
- email: connMeta?.email,
- name: connMeta?.name,
- displayName: connMeta?.displayName,
- })
- : resolveProviderName(point.provider, nodeMap);
+ const cardTitle = resolveDisplayName(point.provider);
const cardSubtitle = isConnectionKey
? `${providerPart} · account ${(connectionId ?? "").slice(0, 8)}…`
: t("providerUtilizationLatestSnapshot");
diff --git a/src/app/(dashboard)/dashboard/analytics/components/ProviderCharts.tsx b/src/app/(dashboard)/dashboard/analytics/components/ProviderCharts.tsx
index 2c46511fc3..3c54b54cf8 100644
--- a/src/app/(dashboard)/dashboard/analytics/components/ProviderCharts.tsx
+++ b/src/app/(dashboard)/dashboard/analytics/components/ProviderCharts.tsx
@@ -55,7 +55,9 @@ export default function ProviderCharts({
tickLine={{ stroke: "var(--color-border)" }}
width={44}
/>
+
formatTooltipTimestamp(String(value), range)}
formatter={(value: number, name: string) => [formatPercent(value), name]}
contentStyle={{
@@ -63,12 +65,12 @@ export default function ProviderCharts({
borderColor: "var(--color-border)",
borderRadius: 12,
color: "var(--color-text-main)",
- boxShadow: "var(--shadow-soft)",
+ boxShadow: "var(--shadow-elevated)",
+ opacity: 1,
}}
itemStyle={{ color: "var(--color-text-main)" }}
labelStyle={{ color: "var(--color-text-main)", fontWeight: 600 }}
/>
-
{providers.map((provider) => (
{
+ return {
+ ResponsiveContainer: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ LineChart: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ CartesianGrid: () => ,
+ XAxis: () => ,
+ YAxis: () => ,
+ Legend: () => ,
+ Tooltip: (props: any) => (
+
+ ),
+ Line: (props: any) => (
+
+ ),
+ };
+});
+
+describe("ProviderCharts Component", () => {
+ let container: HTMLElement;
+
+ beforeEach(() => {
+ (globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
+ container = document.createElement("div");
+ document.body.appendChild(container);
+ });
+
+ afterEach(() => {
+ container.remove();
+ document.body.innerHTML = "";
+ });
+
+ it("renders tooltip with zIndex 1000 wrapperStyle and positioned after Legend", async () => {
+ const root = createRoot(container);
+ await act(async () => {
+ root.render(
+ (p === "antigravity:conn-1" ? "user@example.com" : p)}
+ nodeMap={{}}
+ formatTimestamp={(v) => v}
+ formatPercent={(v) => `${v}%`}
+ formatTooltipTimestamp={(v) => v}
+ />
+ );
+ });
+
+ const tooltip = container.querySelector('[data-testid="chart-tooltip"]');
+ expect(tooltip).not.toBeNull();
+ expect(tooltip?.getAttribute("data-z-index")).toBe("1000");
+ expect(tooltip?.getAttribute("data-pointer-events")).toBe("none");
+
+ // Verify DOM order: Legend appears before Tooltip so Tooltip is painted on top
+ const lineChart = container.querySelector('[data-testid="line-chart"]');
+ const children = Array.from(lineChart?.children || []);
+ const legendIndex = children.findIndex((el) => el.getAttribute("data-testid") === "legend");
+ const tooltipIndex = children.findIndex(
+ (el) => el.getAttribute("data-testid") === "chart-tooltip"
+ );
+ expect(legendIndex).toBeGreaterThan(-1);
+ expect(tooltipIndex).toBeGreaterThan(legendIndex);
+ });
+
+ it("resolves account connection IDs to display names for Line and Legend labels", async () => {
+ const root = createRoot(container);
+ const mockResolve = vi.fn((key: string) => {
+ if (key === "antigravity:uuid-1234") return "games.zaim@gmail.com";
+ return key;
+ });
+
+ await act(async () => {
+ root.render(
+ v}
+ formatPercent={(v) => `${v}%`}
+ formatTooltipTimestamp={(v) => v}
+ />
+ );
+ });
+
+ expect(mockResolve).toHaveBeenCalledWith("antigravity:uuid-1234", expect.anything());
+
+ const line = container.querySelector('[data-testid="chart-line"]');
+ expect(line).not.toBeNull();
+ expect(line?.getAttribute("data-key")).toBe("antigravity:uuid-1234");
+ expect(line?.getAttribute("data-name")).toBe("games.zaim@gmail.com");
+ });
+});