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"); + }); +});