From 41d24b0c9176bcfa77d482c9f10d1849da0fdf68 Mon Sep 17 00:00:00 2001 From: Krrish Dholakia Date: Wed, 25 Feb 2026 23:05:12 -0800 Subject: [PATCH] refactor: more ui improvements --- .../litellm_proxy_extras/schema.prisma | 1 + litellm/proxy/schema.prisma | 1 + schema.prisma | 1 + ui/litellm-dashboard/src/app/page.tsx | 4 +- .../src/components/ToolDetail.tsx | 317 +++++++++++++++++ .../src/components/ToolPolicies.tsx | 333 +----------------- .../components/ToolPolicies/PolicySelect.tsx | 90 +++++ .../src/components/ToolPoliciesView.tsx | 44 +++ 8 files changed, 467 insertions(+), 324 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/ToolDetail.tsx create mode 100644 ui/litellm-dashboard/src/components/ToolPolicies/PolicySelect.tsx create mode 100644 ui/litellm-dashboard/src/components/ToolPoliciesView.tsx diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index e48c0fe3027..08f756e4fb0 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -390,6 +390,7 @@ model LiteLLM_DeletedVerificationToken { config Json @default("{}") user_id String? team_id String? + agent_id String? project_id String? permissions Json @default("{}") max_parallel_requests Int? diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index e48c0fe3027..08f756e4fb0 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -390,6 +390,7 @@ model LiteLLM_DeletedVerificationToken { config Json @default("{}") user_id String? team_id String? + agent_id String? project_id String? permissions Json @default("{}") max_parallel_requests Int? diff --git a/schema.prisma b/schema.prisma index 440c9c1d829..61de0073e0b 100644 --- a/schema.prisma +++ b/schema.prisma @@ -390,6 +390,7 @@ model LiteLLM_DeletedVerificationToken { config Json @default("{}") user_id String? team_id String? + agent_id String? project_id String? permissions Json @default("{}") max_parallel_requests Int? diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 258c2ccb0e0..df360d36dd8 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -38,7 +38,7 @@ import Usage from "@/components/usage"; import UserDashboard from "@/components/user_dashboard"; import { AccessGroupsPage } from "@/components/AccessGroups/AccessGroupsPage"; import VectorStoreManagement from "@/components/vector_store_management"; -import ToolPolicies from "@/components/ToolPolicies"; +import ToolPoliciesView from "@/components/ToolPoliciesView"; import SpendLogsTable from "@/components/view_logs"; import ViewUserDashboard from "@/components/view_users"; import { ThemeProvider } from "@/contexts/ThemeContext"; @@ -550,7 +550,7 @@ function CreateKeyPageContent() { ) : page == "vector-stores" ? ( ) : page == "tool-policies" ? ( - + ) : page == "guardrails-monitor" ? ( ) : page == "new_usage" ? ( diff --git a/ui/litellm-dashboard/src/components/ToolDetail.tsx b/ui/litellm-dashboard/src/components/ToolDetail.tsx new file mode 100644 index 00000000000..fa83190d7e3 --- /dev/null +++ b/ui/litellm-dashboard/src/components/ToolDetail.tsx @@ -0,0 +1,317 @@ +"use client"; + +import { ArrowLeftOutlined, 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 TeamDropdown from "@/components/common_components/team_dropdown"; +import { PolicySelect } from "@/components/ToolPolicies/PolicySelect"; +import { + deleteToolPolicyOverride, + fetchToolDetail, + keyListCall, + teamListCall, + updateToolPolicy, + type ToolPolicyOverrideRow, +} from "@/components/networking"; +import type { Team } from "@/components/key_team_helpers/key_list"; + +interface ToolDetailProps { + toolName: string; + onBack: () => void; + accessToken: string | null; +} + +interface TeamOption { + team_id: string; + team_alias?: string; +} + +interface KeyOption { + token: string; + key_alias?: string; +} + +const TOOL_DETAIL_QUERY_KEY = "tool-detail"; + +export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { + const queryClient = useQueryClient(); + const [overrideSaving, setOverrideSaving] = useState(false); + const [policySaving, setPolicySaving] = useState(false); + const [blockScope, setBlockScope] = useState<"team" | "key">("team"); + const [blockTeamId, setBlockTeamId] = useState(null); + const [blockKey, setBlockKey] = useState(null); + + const { data: detail, isLoading: detailLoading, error: detailError } = useQuery({ + queryKey: [TOOL_DETAIL_QUERY_KEY, toolName], + queryFn: () => fetchToolDetail(accessToken!, toolName), + enabled: !!accessToken && !!toolName, + }); + + const { data: teamsData } = useQuery({ + queryKey: ["teams-list-tool-detail"], + queryFn: () => teamListCall(accessToken!, null, null), + enabled: !!accessToken, + }); + + const { data: keysData } = useQuery({ + queryKey: ["keys-list-tool-detail"], + queryFn: () => keyListCall(accessToken!, null, null, null, null, null, 1, 100), + enabled: !!accessToken, + }); + + const teams: Team[] = useMemo(() => { + const arr = Array.isArray(teamsData) ? teamsData : teamsData?.data ?? []; + return arr.map((t: { team_id?: string; id?: string; team_alias?: string }) => ({ + team_id: t.team_id ?? t.id ?? "", + team_alias: t.team_alias ?? t.team_id ?? "", + models: [], + max_budget: null, + budget_duration: null, + tpm_limit: null, + rpm_limit: null, + organization_id: "", + created_at: "", + keys: [], + members_with_roles: [], + spend: 0, + })); + }, [teamsData]); + + const keys: KeyOption[] = useMemo(() => { + const keysRes = keysData?.keys ?? keysData?.data ?? []; + return keysRes.map((k: { token?: string; api_key?: string; key_hash?: string; key_alias?: string }) => ({ + token: k.token ?? k.api_key ?? k.key_hash ?? "", + key_alias: k.key_alias ?? (k.token ?? k.api_key ?? k.key_hash)?.toString?.()?.substring?.(0, 8), + })); + }, [keysData]); + + const invalidateDetail = useCallback(() => { + queryClient.invalidateQueries({ queryKey: [TOOL_DETAIL_QUERY_KEY, toolName] }); + }, [queryClient, toolName]); + + const handlePolicyChange = useCallback( + async (name: string, newPolicy: string) => { + if (!accessToken) return; + setPolicySaving(true); + try { + await updateToolPolicy(accessToken, name, newPolicy); + invalidateDetail(); + } catch (e: unknown) { + alert(`Failed to update policy: ${e instanceof Error ? e.message : String(e)}`); + } finally { + setPolicySaving(false); + } + }, + [accessToken, invalidateDetail] + ); + + const handleAddOverride = useCallback(async () => { + if (!accessToken || !toolName) return; + const isTeam = blockScope === "team"; + if (isTeam && !blockTeamId) return; + if (!isTeam && !blockKey?.token) return; + setOverrideSaving(true); + try { + await updateToolPolicy(accessToken, toolName, "blocked", { + team_id: isTeam ? blockTeamId : undefined, + key_hash: !isTeam ? blockKey!.token : undefined, + key_alias: !isTeam ? blockKey!.key_alias : undefined, + }); + invalidateDetail(); + setBlockTeamId(null); + setBlockKey(null); + } catch (e: unknown) { + alert(`Failed to add override: ${e instanceof Error ? e.message : String(e)}`); + } finally { + setOverrideSaving(false); + } + }, [accessToken, toolName, blockScope, blockTeamId, blockKey, invalidateDetail]); + + const handleRemoveOverride = useCallback( + async (override: ToolPolicyOverrideRow) => { + if (!accessToken || !toolName) return; + setOverrideSaving(true); + try { + await deleteToolPolicyOverride(accessToken, toolName, { + team_id: override.team_id ?? undefined, + key_hash: override.key_hash ?? undefined, + }); + invalidateDetail(); + } catch (e: unknown) { + alert(`Failed to remove override: ${e instanceof Error ? e.message : String(e)}`); + } finally { + setOverrideSaving(false); + } + }, + [accessToken, toolName, invalidateDetail] + ); + + if (detailLoading && !detail) { + return ( +
+ +
+ ); + } + + if (detailError && !detail) { + return ( +
+ +

Failed to load tool details.

+
+ ); + } + + if (!detail) { + return null; + } + + const { tool, overrides } = detail; + + return ( +
+
+ + +
+
+
+ +

{tool.tool_name}

+ + {tool.origin ?? "—"} + + + {(tool.call_count ?? 0).toLocaleString()} calls + +
+
+
+
+ +
+
+

Global policy

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

Blocked for team or key

+
    + {overrides.map((ov) => ( +
  • + + {ov.team_id ? `Team: ${ov.team_id}` : ""} + {ov.team_id && ov.key_hash ? " · " : ""} + {ov.key_hash ? `Key: ${ov.key_alias || ov.key_hash.substring(0, 8)}` : ""} + {!ov.team_id && !ov.key_hash ? "—" : ""} + + +
  • + ))} +
+
+ )} + +
+

Block for team or key

+
+
+ Scope +
+ + +
+
+
+ + {blockScope === "team" ? "Team" : "Key"} + + {blockScope === "team" ? ( + setBlockTeamId(id || null)} + /> + ) : ( + onChange(toolName, v)} - onClick={(e) => e.stopPropagation()} - style={{ - minWidth: 110, - fontWeight: 500, - }} - styles={{ - selector: { - backgroundColor: style.bg, - borderColor: style.border, - color: style.color, - borderRadius: 999, - fontSize: 11, - fontWeight: 600, - paddingLeft: 8, - paddingRight: 4, - }, - }} - popupMatchSelectWidth={false} - options={POLICY_OPTIONS.map((o) => ({ - value: o.value, - label: ( - - - {o.label} - - ), - }))} - /> - ); -}; - -interface TeamOption { - team_id: string; - team_alias?: string; -} - -interface KeyOption { - token: string; - key_alias?: string; -} - -export const ToolPolicies: React.FC = ({ accessToken }) => { +export const ToolPolicies: React.FC = ({ accessToken, onSelectTool }) => { const [tools, setTools] = useState([]); const [loading, setLoading] = useState(true); const [isFetching, setIsFetching] = useState(false); const [error, setError] = useState(null); const [saving, setSaving] = useState(null); - const [detailModalOpen, setDetailModalOpen] = useState(false); - const [detailToolName, setDetailToolName] = useState(null); - const [detail, setDetail] = useState(null); - const [detailLoading, setDetailLoading] = useState(false); - const [teams, setTeams] = useState([]); - const [keys, setKeys] = useState([]); - const [overrideSaving, setOverrideSaving] = useState(false); - const [blockScope, setBlockScope] = useState<"team" | "key">("team"); - const [blockTeamId, setBlockTeamId] = useState(null); - const [blockKey, setBlockKey] = useState<{ token: string; key_alias?: string } | null>(null); - const [searchTerm, setSearchTerm] = useState(""); const [sortField, setSortField] = useState("created_at"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); @@ -199,9 +102,6 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { try { await updateToolPolicy(accessToken, toolName, newPolicy); setTools((prev) => prev.map((t) => (t.tool_name === toolName ? { ...t, call_policy: newPolicy } : t))); - if (detailToolName === toolName && detail) { - setDetail((d) => (d ? { ...d, tool: { ...d.tool, call_policy: newPolicy } } : null)); - } } catch (e: any) { alert(`Failed to update policy: ${e.message}`); } finally { @@ -209,91 +109,6 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { } }; - const openDetailModal = useCallback( - async (toolName: string) => { - if (!accessToken) return; - setDetailToolName(toolName); - setDetailModalOpen(true); - setDetail(null); - setDetailLoading(true); - setBlockTeamId(null); - setBlockKey(null); - try { - const [detailRes, teamsRes, keysRes] = await Promise.all([ - fetchToolDetail(accessToken, toolName), - teamListCall(accessToken, null, null), - keyListCall(accessToken, null, null, null, null, null, 1, 100), - ]); - setDetail(detailRes); - const teamsArray = Array.isArray(teamsRes) ? teamsRes : teamsRes?.data ?? []; - setTeams( - teamsArray.map((t: any) => ({ team_id: t.team_id ?? t.id, team_alias: t.team_alias ?? t.team_id })) - ); - const keysArray = keysRes?.keys ?? keysRes?.data ?? []; - setKeys( - keysArray.map((k: any) => ({ - token: k.token ?? k.api_key ?? k.key_hash ?? "", - key_alias: k.key_alias ?? k.token?.substring?.(0, 8), - })) - ); - } catch (e: any) { - setError(e.message ?? "Failed to load tool detail"); - } finally { - setDetailLoading(false); - } - }, - [accessToken] - ); - - const closeDetailModal = useCallback(() => { - setDetailModalOpen(false); - setDetailToolName(null); - setDetail(null); - }, []); - - const handleAddOverride = useCallback(async () => { - if (!accessToken || !detailToolName) return; - const isTeam = blockScope === "team"; - if (isTeam && !blockTeamId) return; - if (!isTeam && !blockKey?.token) return; - setOverrideSaving(true); - try { - await updateToolPolicy(accessToken, detailToolName, "blocked", { - team_id: isTeam ? blockTeamId! : undefined, - key_hash: !isTeam ? blockKey!.token : undefined, - key_alias: !isTeam ? blockKey!.key_alias : undefined, - }); - const refreshed = await fetchToolDetail(accessToken, detailToolName); - setDetail(refreshed); - setBlockTeamId(null); - setBlockKey(null); - } catch (e: any) { - alert(`Failed to add override: ${e.message}`); - } finally { - setOverrideSaving(false); - } - }, [accessToken, detailToolName, blockScope, blockTeamId, blockKey]); - - const handleRemoveOverride = useCallback( - async (override: ToolPolicyOverrideRow) => { - if (!accessToken || !detailToolName) return; - setOverrideSaving(true); - try { - await deleteToolPolicyOverride(accessToken, detailToolName, { - team_id: override.team_id ?? undefined, - key_hash: override.key_hash ?? undefined, - }); - const refreshed = await fetchToolDetail(accessToken, detailToolName); - setDetail(refreshed); - } catch (e: any) { - alert(`Failed to remove override: ${e.message}`); - } finally { - setOverrideSaving(false); - } - }, - [accessToken, detailToolName] - ); - const handleSortChange = (field: SortField, newState: SortState) => { if (newState === false) { setSortField("created_at"); @@ -431,7 +246,7 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { }; return ( -
+

Tool Policies

{/* Summary cards */} @@ -644,10 +459,10 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { @@ -660,8 +475,10 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { onChange={handlePolicyChange} /> - - {(tool.call_count ?? 0).toLocaleString()} + +
+ {(tool.call_count ?? 0).toLocaleString()} +
@@ -718,134 +535,6 @@ export const ToolPolicies: React.FC = ({ accessToken }) => { )}
- {/* Tool detail modal: view tool, global policy, overrides, block for team/key */} - - {detailLoading ? ( -

Loading…

- ) : detail ? ( -
-
- - Origin: {detail.tool.origin ?? "—"} - - - # Calls: {(detail.tool.call_count ?? 0).toLocaleString()} - -
-
- Global policy - -
- - {detail.overrides.length > 0 && ( -
- Blocked for team/key -
    - {detail.overrides.map((ov) => ( -
  • - - {ov.team_id ? `Team: ${ov.team_id}` : ""} - {ov.team_id && ov.key_hash ? " · " : ""} - {ov.key_hash ? `Key: ${ov.key_alias || ov.key_hash.substring(0, 8)}` : ""} - {!ov.team_id && !ov.key_hash ? "—" : ""} - - -
  • - ))} -
-
- )} - -
- Block for team or key -
-
- - -
- {blockScope === "team" ? ( -
- setBlockTeamId(id ?? null)} - /> -
- ) : ( - onChange(toolName, v)} + onClick={(e) => stopPropagation && e.stopPropagation()} + style={{ + minWidth, + fontWeight: 500, + }} + styles={{ + selector: { + backgroundColor: style.bg, + borderColor: style.border, + color: style.color, + borderRadius: 999, + fontSize: size === "small" ? 11 : 12, + fontWeight: 600, + paddingLeft: 8, + paddingRight: 4, + }, + }} + popupMatchSelectWidth={false} + options={POLICY_OPTIONS.map((o) => ({ + value: o.value, + label: ( + + + {o.label} + + ), + }))} + /> + ); +}; diff --git a/ui/litellm-dashboard/src/components/ToolPoliciesView.tsx b/ui/litellm-dashboard/src/components/ToolPoliciesView.tsx new file mode 100644 index 00000000000..3e4964f7e36 --- /dev/null +++ b/ui/litellm-dashboard/src/components/ToolPoliciesView.tsx @@ -0,0 +1,44 @@ +"use client"; + +import React, { useState } from "react"; +import { ToolDetail } from "@/components/ToolDetail"; +import { ToolPolicies } from "@/components/ToolPolicies"; + +type View = + | { type: "overview" } + | { type: "detail"; toolName: string }; + +interface ToolPoliciesViewProps { + accessToken: string | null; + userRole?: string; +} + +export default function ToolPoliciesView({ accessToken, userRole }: ToolPoliciesViewProps) { + const [view, setView] = useState({ type: "overview" }); + + const handleSelectTool = (toolName: string) => { + setView({ type: "detail", toolName }); + }; + + const handleBack = () => { + setView({ type: "overview" }); + }; + + return ( +
+ {view.type === "detail" ? ( + + ) : ( + + )} +
+ ); +}