mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-19 05:12:16 +03:00
fix(analytics): resolve account email/name in Utilization chart and fix tooltip stacking context (#13029)
Co-authored-by: ZaimMarzuki <ZaimMarzuki@users.noreply.github.com>
This commit is contained in:
1
changelog.d/fixes/utilization-account-chart-display.md
Normal file
1
changelog.d/fixes/utilization-account-chart-display.md
Normal file
@@ -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
|
||||
@@ -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");
|
||||
|
||||
@@ -55,7 +55,9 @@ export default function ProviderCharts({
|
||||
tickLine={{ stroke: "var(--color-border)" }}
|
||||
width={44}
|
||||
/>
|
||||
<Legend />
|
||||
<Tooltip
|
||||
wrapperStyle={{ zIndex: 1000, pointerEvents: "none" }}
|
||||
labelFormatter={(value) => 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 }}
|
||||
/>
|
||||
<Legend />
|
||||
{providers.map((provider) => (
|
||||
<Line
|
||||
key={provider}
|
||||
|
||||
111
tests/unit/ui/provider-utilization-chart.test.tsx
Normal file
111
tests/unit/ui/provider-utilization-chart.test.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
// @vitest-environment jsdom
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import ProviderCharts from "@/app/(dashboard)/dashboard/analytics/components/ProviderCharts";
|
||||
|
||||
vi.mock("recharts", () => {
|
||||
return {
|
||||
ResponsiveContainer: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="responsive-container">{children}</div>
|
||||
),
|
||||
LineChart: ({ children }: { children: React.ReactNode }) => (
|
||||
<div data-testid="line-chart">{children}</div>
|
||||
),
|
||||
CartesianGrid: () => <div data-testid="grid" />,
|
||||
XAxis: () => <div data-testid="xaxis" />,
|
||||
YAxis: () => <div data-testid="yaxis" />,
|
||||
Legend: () => <div data-testid="legend" />,
|
||||
Tooltip: (props: any) => (
|
||||
<div
|
||||
data-testid="chart-tooltip"
|
||||
data-z-index={props.wrapperStyle?.zIndex}
|
||||
data-pointer-events={props.wrapperStyle?.pointerEvents}
|
||||
/>
|
||||
),
|
||||
Line: (props: any) => (
|
||||
<div data-testid="chart-line" data-key={props.dataKey} data-name={props.name} />
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
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(
|
||||
<ProviderCharts
|
||||
chartData={[{ timestamp: "2026-09-08T00:00:00Z", "antigravity:conn-1": 100 }]}
|
||||
providers={["antigravity:conn-1"]}
|
||||
providerColors={new Map([["antigravity:conn-1", "#10b981"]])}
|
||||
range="24h"
|
||||
resolveProviderName={(p) => (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(
|
||||
<ProviderCharts
|
||||
chartData={[{ timestamp: "2026-09-08T00:00:00Z", "antigravity:uuid-1234": 85 }]}
|
||||
providers={["antigravity:uuid-1234"]}
|
||||
providerColors={new Map([["antigravity:uuid-1234", "#10b981"]])}
|
||||
range="24h"
|
||||
resolveProviderName={mockResolve}
|
||||
nodeMap={{}}
|
||||
formatTimestamp={(v) => 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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user