From fb87cb78af69158219a7e3169ac6a7483c61107f Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Tue, 3 Mar 2026 19:48:02 -0800 Subject: [PATCH] UI - Add ToolDetails --- .../src/components/ToolDetail.tsx | 126 ++++++++++++++---- 1 file changed, 99 insertions(+), 27 deletions(-) diff --git a/ui/litellm-dashboard/src/components/ToolDetail.tsx b/ui/litellm-dashboard/src/components/ToolDetail.tsx index 4665a72eaea..ed0f866acb8 100644 --- a/ui/litellm-dashboard/src/components/ToolDetail.tsx +++ b/ui/litellm-dashboard/src/components/ToolDetail.tsx @@ -3,7 +3,7 @@ import { ArrowLeftOutlined, HistoryOutlined, ToolOutlined } from "@ant-design/icons"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Button, Select, Spin } from "antd"; -import React, { useCallback, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import TeamDropdown from "@/components/common_components/team_dropdown"; import { LogViewer } from "@/components/GuardrailsMonitor/LogViewer"; import type { LogEntry } from "@/components/GuardrailsMonitor/mockData"; @@ -11,10 +11,12 @@ import { PolicySelect } from "@/components/ToolPolicies/PolicySelect"; import { deleteToolPolicyOverride, fetchToolDetail, + fetchToolPolicyOptions, getToolUsageLogs, keyListCall, teamListCall, updateToolPolicy, + type ToolPolicyOption, type ToolPolicyOverrideRow, } from "@/components/networking"; import type { Team } from "@/components/key_team_helpers/key_list"; @@ -25,11 +27,6 @@ interface ToolDetailProps { accessToken: string | null; } -interface TeamOption { - team_id: string; - team_alias?: string; -} - interface KeyOption { token: string; key_alias?: string; @@ -51,7 +48,8 @@ function getDefaultLogsDateRange(): { start: string; end: string } { export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { const queryClient = useQueryClient(); const [overrideSaving, setOverrideSaving] = useState(false); - const [policySaving, setPolicySaving] = useState(false); + const [inputPolicySaving, setInputPolicySaving] = useState(false); + const [outputPolicySaving, setOutputPolicySaving] = useState(false); const [blockScope, setBlockScope] = useState<"team" | "key">("team"); const [blockTeamId, setBlockTeamId] = useState(null); const [blockKey, setBlockKey] = useState(null); @@ -64,6 +62,13 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { enabled: !!accessToken && !!toolName, }); + const { data: policyOptions } = useQuery({ + queryKey: ["tool-policy-options"], + queryFn: () => fetchToolPolicyOptions(accessToken!), + enabled: !!accessToken, + staleTime: 60_000, + }); + const { data: teamsData } = useQuery({ queryKey: ["teams-list-tool-detail"], queryFn: () => teamListCall(accessToken!, null, null), @@ -129,20 +134,36 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { queryClient.invalidateQueries({ queryKey: [TOOL_DETAIL_QUERY_KEY, toolName] }); }, [queryClient, toolName]); - const handlePolicyChange = useCallback( - async (name: string, newPolicy: string) => { + const handleInputPolicyChange = useCallback( + async (_name: string, newPolicy: string) => { if (!accessToken) return; - setPolicySaving(true); + setInputPolicySaving(true); try { - await updateToolPolicy(accessToken, name, newPolicy); + await updateToolPolicy(accessToken, toolName, { input_policy: newPolicy }); invalidateDetail(); } catch (e: unknown) { - alert(`Failed to update policy: ${e instanceof Error ? e.message : String(e)}`); + alert(`Failed to update input policy: ${e instanceof Error ? e.message : String(e)}`); } finally { - setPolicySaving(false); + setInputPolicySaving(false); } }, - [accessToken, invalidateDetail] + [accessToken, toolName, invalidateDetail] + ); + + const handleOutputPolicyChange = useCallback( + async (_name: string, newPolicy: string) => { + if (!accessToken) return; + setOutputPolicySaving(true); + try { + await updateToolPolicy(accessToken, toolName, { output_policy: newPolicy }); + invalidateDetail(); + } catch (e: unknown) { + alert(`Failed to update output policy: ${e instanceof Error ? e.message : String(e)}`); + } finally { + setOutputPolicySaving(false); + } + }, + [accessToken, toolName, invalidateDetail] ); const handleAddOverride = useCallback(async () => { @@ -152,7 +173,7 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { if (!isTeam && !blockKey?.token) return; setOverrideSaving(true); try { - await updateToolPolicy(accessToken, toolName, "blocked", { + await updateToolPolicy(accessToken, toolName, { input_policy: "blocked" }, { team_id: isTeam ? blockTeamId : undefined, key_hash: !isTeam ? blockKey!.token : undefined, key_alias: !isTeam ? blockKey!.key_alias : undefined, @@ -211,6 +232,13 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { const { tool, overrides } = detail; + const inputDesc = policyOptions?.input_policies?.find( + (p) => p.value === tool.input_policy + )?.description; + const outputDesc = policyOptions?.output_policies?.find( + (p) => p.value === tool.output_policy + )?.description; + return (
@@ -235,23 +263,67 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { {(tool.call_count ?? 0).toLocaleString()} calls
+
+ {tool.user_agent && ( +
+
User Agent:
+
{tool.user_agent}
+
+ )} + {tool.created_at && ( +
+
First Discovered:
+
{new Date(tool.created_at).toLocaleString()}
+
+ )} + {tool.last_used_at && ( +
+
Last Used:
+
{new Date(tool.last_used_at).toLocaleString()}
+
+ )} +
-
-

Global policy

- -
+ {/* Two-panel policy layout */} +
+
+

Input Policy

+

+ {inputDesc ?? "Controls what data this tool is allowed to accept."} +

+ +
+ +
+

Output Policy

+

+ {outputDesc ?? "Controls how this tool's output is trusted by downstream tools."} +

+ +
+
{overrides.length > 0 && (