feat(ui): click a tool in the cost dashboard to open its logs

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Krrish Dholakia 2026-07-24 20:26:04 +00:00
parent 35dc982692
commit b15e0e6ade
5 changed files with 124 additions and 16 deletions

View file

@ -5,9 +5,15 @@ import type { ToolSpendResponse } from "@/components/networking";
import type { DailyData, SpendMetrics } from "@/components/UsagePage/types";
const mockGetToolSpend = vi.fn();
const mockPush = vi.fn();
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: mockPush }),
}));
vi.mock("@/components/networking", () => ({
getToolSpend: (...args: unknown[]) => mockGetToolSpend(...args),
serverRootPath: "",
}));
vi.mock("@/components/shared/advanced_date_picker", () => ({
@ -22,8 +28,26 @@ vi.mock("@/components/shared/charts", () => ({
DonutChart: ({ data, label }: { data: unknown; label: string }) => (
<div data-testid="donut-chart" data-label={label} data-slices={JSON.stringify(data)} />
),
BarChart: ({ data, categories }: { data: unknown; categories: string[] }) => (
<div data-testid="bar-chart" data-categories={categories.join(",")} data-series={JSON.stringify(data)} />
BarChart: ({
data,
categories,
onValueChange,
}: {
data: Record<string, unknown>[];
categories: string[];
onValueChange?: (item: Record<string, unknown> & { categoryClicked: string }) => void;
}) => (
<div data-testid="bar-chart" data-categories={categories.join(",")} data-series={JSON.stringify(data)}>
{data.map((datum, i) =>
categories.map((category) => (
<button
key={`${i}-${category}`}
data-testid={`bar-${i}-${category}`}
onClick={() => onValueChange?.({ ...datum, categoryClicked: category })}
/>
)),
)}
</div>
),
DEFAULT_COLOR_CYCLE: ["emerald", "blue", "violet", "amber"],
}));
@ -137,4 +161,21 @@ describe("UsageTab", () => {
const series = JSON.parse(bars[0].getAttribute("data-series") ?? "[]");
expect(series[0]).toMatchObject({ tool_name: "search", spend: 4.0 });
});
it("opens the tool's logs when a bar is clicked", async () => {
const toolSpend = {
by_tool: [{ tool_name: "my tool/read", spend: 4.0, call_count: 3, total_tokens: 150 }],
daily: [{ date: "2026-07-12", tool_name: "my tool/read", spend: 4.0, call_count: 3 }],
total_spend: 4.0,
start_date: "2026-07-12",
end_date: "2026-07-12",
};
const { findByTestId } = renderWith([day("2026-07-12", {})], toolSpend);
(await findByTestId("bar-0-spend")).click();
expect(mockPush).toHaveBeenCalledWith("/ui/tool-policies?tool=my+tool%2Fread");
(await findByTestId("bar-0-my tool/read")).click();
expect(mockPush).toHaveBeenLastCalledWith("/ui/tool-policies?tool=my+tool%2Fread");
});
});

View file

@ -1,7 +1,8 @@
"use client";
import React, { useEffect, useMemo, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { Collapse } from "antd";
import { useRouter } from "next/navigation";
import { AreaChart, BarChart, DonutChart, DEFAULT_COLOR_CYCLE } from "@/components/shared/charts";
import AdvancedDatePicker from "@/components/shared/advanced_date_picker";
@ -11,6 +12,8 @@ import { SpendMetrics } from "@/components/UsagePage/types";
import { formatNumberWithCommas } from "@/utils/dataUtils";
import { buildDailyToolSeries, topToolsBySpend, usd } from "./costOptimizationUtils";
import { DailyActivityRange } from "./useDailyActivityRange";
import { migratedHref } from "@/utils/migratedPages";
import { TOOL_QUERY_PARAM } from "@/components/ToolPoliciesView";
interface UsageTabProps {
accessToken: string | null;
@ -82,8 +85,17 @@ const SummaryCard = ({ label, value, hint }: { label: string; value: string; hin
);
const UsageTab: React.FC<UsageTabProps> = ({ accessToken, activity }) => {
const router = useRouter();
const { dateValue, onDateChange, results, loading, isFetchingMore } = activity;
const openToolLogs = useCallback(
(toolName: string) => {
const params = new URLSearchParams({ [TOOL_QUERY_PARAM]: toolName });
router.push(`${migratedHref("tool-policies")}?${params.toString()}`);
},
[router],
);
const startTime = dateValue.from ?? null;
const endTime = dateValue.to ?? null;
@ -209,7 +221,8 @@ const UsageTab: React.FC<UsageTabProps> = ({ accessToken, activity }) => {
<CardTitle>Spend by tool</CardTitle>
<p className="text-sm text-muted-foreground">
Spend on requests that called each tool (MCP and client-side tools). A request that used multiple tools
counts its full spend toward each, so this attributes rather than partitions spend.
counts its full spend toward each, so this attributes rather than partitions spend. Click a bar to see the
logs for that tool.
</p>
</CardHeader>
<CardContent>
@ -230,6 +243,7 @@ const UsageTab: React.FC<UsageTabProps> = ({ accessToken, activity }) => {
yAxisWidth={140}
showLegend={false}
valueFormatter={usd}
onValueChange={(item) => openToolLogs(String(item.tool_name))}
/>
</div>
<div>
@ -241,6 +255,7 @@ const UsageTab: React.FC<UsageTabProps> = ({ accessToken, activity }) => {
colors={toolColors}
stack
valueFormatter={usd}
onValueChange={(item) => openToolLogs(item.categoryClicked)}
/>
</div>
</div>

View file

@ -1,10 +1,36 @@
import React from "react";
import { describe, it, expect, vi } from "vitest";
import { beforeEach, describe, it, expect, vi } from "vitest";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { renderWithProviders } from "../../tests/test-utils";
import ToolPoliciesView from "./ToolPoliciesView";
let currentUrl = "/ui/tool-policies";
const urlListeners = new Set<() => void>();
vi.mock("next/navigation", async () => {
const react = await import("react");
return {
useRouter: () => ({
push: (url: string) => {
currentUrl = url;
urlListeners.forEach((listener) => listener());
},
}),
usePathname: () => currentUrl.split("?")[0],
useSearchParams: () => {
const [, rerender] = react.useReducer((n: number) => n + 1, 0);
react.useEffect(() => {
urlListeners.add(rerender);
return () => {
urlListeners.delete(rerender);
};
}, [rerender]);
return new URLSearchParams(currentUrl.split("?")[1] ?? "");
},
};
});
vi.mock("@/components/ToolDetail", () => ({
ToolDetail: ({ toolName, onBack }: { toolName: string; onBack: () => void }) => (
<div>
@ -26,6 +52,10 @@ vi.mock("@/components/ToolPolicies/ToolPoliciesPanel", () => ({
}));
describe("ToolPoliciesView", () => {
beforeEach(() => {
currentUrl = "/ui/tool-policies";
});
it("should render the overview by default", () => {
renderWithProviders(<ToolPoliciesView accessToken="token" />);
@ -40,6 +70,14 @@ describe("ToolPoliciesView", () => {
expect(screen.getByText("Detail: my-tool")).toBeInTheDocument();
expect(screen.queryByText("Tool Policies Overview")).not.toBeInTheDocument();
expect(currentUrl).toBe("/ui/tool-policies?tool=my-tool");
});
it("should open a tool's detail directly from the ?tool= deep link", () => {
currentUrl = "/ui/tool-policies?tool=deep-linked-tool";
renderWithProviders(<ToolPoliciesView accessToken="token" />);
expect(screen.getByText("Detail: deep-linked-tool")).toBeInTheDocument();
});
it("should navigate back to overview when back is clicked", async () => {

View file

@ -1,30 +1,43 @@
"use client";
import React, { useState } from "react";
import React, { useCallback } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { ToolDetail } from "@/components/ToolDetail";
import { ToolPoliciesPanel } from "@/components/ToolPolicies/ToolPoliciesPanel";
type View = { type: "overview" } | { type: "detail"; toolName: string };
export const TOOL_QUERY_PARAM = "tool";
interface ToolPoliciesViewProps {
accessToken: string | null;
}
export default function ToolPoliciesView({ accessToken }: ToolPoliciesViewProps) {
const [view, setView] = useState<View>({ type: "overview" });
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
const selectedTool = searchParams.get(TOOL_QUERY_PARAM);
const handleSelectTool = (toolName: string) => {
setView({ type: "detail", toolName });
};
const navigateToTool = useCallback(
(toolName: string | null) => {
const params = new URLSearchParams(searchParams.toString());
if (toolName) {
params.set(TOOL_QUERY_PARAM, toolName);
} else {
params.delete(TOOL_QUERY_PARAM);
}
const query = params.toString();
router.push(query ? `${pathname}?${query}` : pathname);
},
[pathname, router, searchParams],
);
const handleBack = () => {
setView({ type: "overview" });
};
const handleSelectTool = useCallback((toolName: string) => navigateToTool(toolName), [navigateToTool]);
const handleBack = useCallback(() => navigateToTool(null), [navigateToTool]);
return (
<div className="p-6 w-full min-w-0 flex-1">
{view.type === "detail" ? (
<ToolDetail toolName={view.toolName} onBack={handleBack} accessToken={accessToken} />
{selectedTool ? (
<ToolDetail toolName={selectedTool} onBack={handleBack} accessToken={accessToken} />
) : (
<ToolPoliciesPanel accessToken={accessToken} onSelectTool={handleSelectTool} />
)}

View file

@ -104,6 +104,7 @@ export function BarChart<TDatum extends Record<string, unknown>>({
fill={fills[i]}
stackId={stack ? "stack" : undefined}
isAnimationActive={false}
className={onValueChange ? "cursor-pointer" : undefined}
onClick={
onValueChange
? (item: { payload?: TDatum }) => {