From 911b0cfd736d986ff8abada919c6280fd141818c Mon Sep 17 00:00:00 2001 From: tanjiro <56165694+NANDINI-star@users.noreply.github.com> Date: Wed, 13 Aug 2025 09:21:04 +0900 Subject: [PATCH] Display Error from Backend on the UI - Keys Page (#13435) * fix sso logout - add a new login page with sso button * lint fix * lint fix * lint fix * fix tests * fix test * Revert "fix test" This reverts commit 74eb7345710892d5a9d02baec0ef389b98d0dde3. * Reapply "fix test" This reverts commit 72d0b2d4c62f6bb9351a7656ff88efc2ba91aef7. * add host to add modal * close modal after save is clicked. and auto-refresh * show old values in edit modal * send the whole payload on edit * Update settings.tsx * resolve conflict * fix conflict * merge main * first draft of notifications added to settings * add error compatibility by taking errors from the backend - db errors - auth errors * add support for different types of errors * minor * name change * email alerts page notifications modified * remove unused code * move create_key to organisms/ folder * move view_key_table to templates * keys page notifications * move regenerate_key to organisms * notifications for regenerate key modal * move key info view and key edit view to templates/ folder * replace "Network response was not ok" with error data * resolve merge conflict --- ui/litellm-dashboard/src/app/page.tsx | 635 +++++----- .../src/components/all_keys_table.tsx | 580 +++++----- .../src/components/networking.tsx | 2 +- .../{ => organisms}/create_key_button.tsx | 68 +- .../{ => organisms}/regenerate_key_modal.tsx | 133 +-- .../components/tag_management/tag_info.tsx | 141 +-- .../{ => templates}/key_edit_view.tsx | 261 ++--- .../{ => templates}/key_info_view.tsx | 180 ++- .../{ => templates}/view_key_table.tsx | 396 +++---- .../src/components/top_key_view.tsx | 198 ++-- .../src/components/user_dashboard.tsx | 4 +- .../src/components/view_logs/index.tsx | 1031 ++++++++--------- 12 files changed, 1670 insertions(+), 1959 deletions(-) rename ui/litellm-dashboard/src/components/{ => organisms}/create_key_button.tsx (96%) rename ui/litellm-dashboard/src/components/{ => organisms}/regenerate_key_modal.tsx (71%) rename ui/litellm-dashboard/src/components/{ => templates}/key_edit_view.tsx (58%) rename ui/litellm-dashboard/src/components/{ => templates}/key_info_view.tsx (84%) rename ui/litellm-dashboard/src/components/{ => templates}/view_key_table.tsx (62%) diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 7106a6f884f..22f0e29d11f 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -1,234 +1,220 @@ -"use client"; +"use client" -import React, { Suspense, useEffect, useState } from "react"; -import { useSearchParams } from "next/navigation"; -import { jwtDecode } from "jwt-decode"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { Team } from "@/components/key_team_helpers/key_list"; -import Navbar from "@/components/navbar"; -import { ThemeProvider } from "@/contexts/ThemeContext"; -import UserDashboard from "@/components/user_dashboard"; -import ModelDashboard from "@/components/model_dashboard"; -import ViewUserDashboard from "@/components/view_users"; -import Teams from "@/components/teams"; -import Organizations from "@/components/organizations"; -import { fetchOrganizations } from "@/components/organizations"; -import AdminPanel from "@/components/admins"; -import Settings from "@/components/settings"; -import GeneralSettings from "@/components/general_settings"; -import PassThroughSettings from "@/components/pass_through_settings"; -import BudgetPanel from "@/components/budgets/budget_panel"; -import SpendLogsTable from "@/components/view_logs"; -import ModelHubTable from "@/components/model_hub_table"; -import NewUsagePage from "@/components/new_usage"; -import APIRef from "@/components/api_ref"; -import ChatUI from "@/components/chat_ui"; -import Sidebar from "@/components/leftnav"; -import Usage from "@/components/usage"; -import CacheDashboard from "@/components/cache_dashboard"; -import { - getUiConfig, - proxyBaseUrl, - setGlobalLitellmHeaderName, -} from "@/components/networking"; -import { Organization } from "@/components/networking"; -import GuardrailsPanel from "@/components/guardrails"; -import PromptsPanel from "@/components/prompts"; -import TransformRequestPanel from "@/components/transform_request"; -import { fetchUserModels } from "@/components/create_key_button"; -import { fetchTeams } from "@/components/common_components/fetch_teams"; -import { MCPServers } from "@/components/mcp_tools"; -import TagManagement from "@/components/tag_management"; -import VectorStoreManagement from "@/components/vector_store_management"; -import UIThemeSettings from "@/components/ui_theme_settings"; -import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; -import { cx } from "@/lib/cva.config"; +import React, { Suspense, useEffect, useState } from "react" +import { useSearchParams } from "next/navigation" +import { jwtDecode } from "jwt-decode" +import { QueryClient, QueryClientProvider } from "@tanstack/react-query" +import { Team } from "@/components/key_team_helpers/key_list" +import Navbar from "@/components/navbar" +import { ThemeProvider } from "@/contexts/ThemeContext" +import UserDashboard from "@/components/user_dashboard" +import ModelDashboard from "@/components/model_dashboard" +import ViewUserDashboard from "@/components/view_users" +import Teams from "@/components/teams" +import Organizations from "@/components/organizations" +import { fetchOrganizations } from "@/components/organizations" +import AdminPanel from "@/components/admins" +import Settings from "@/components/settings" +import GeneralSettings from "@/components/general_settings" +import PassThroughSettings from "@/components/pass_through_settings" +import BudgetPanel from "@/components/budgets/budget_panel" +import SpendLogsTable from "@/components/view_logs" +import ModelHubTable from "@/components/model_hub_table" +import NewUsagePage from "@/components/new_usage" +import APIRef from "@/components/api_ref" +import ChatUI from "@/components/chat_ui" +import Sidebar from "@/components/leftnav" +import Usage from "@/components/usage" +import CacheDashboard from "@/components/cache_dashboard" +import { getUiConfig, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking" +import { Organization } from "@/components/networking" +import GuardrailsPanel from "@/components/guardrails" +import PromptsPanel from "@/components/prompts" +import TransformRequestPanel from "@/components/transform_request" +import { fetchUserModels } from "@/components/organisms/create_key_button" +import { fetchTeams } from "@/components/common_components/fetch_teams" +import { MCPServers } from "@/components/mcp_tools" +import TagManagement from "@/components/tag_management" +import VectorStoreManagement from "@/components/vector_store_management" +import UIThemeSettings from "@/components/ui_theme_settings" +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner" +import { cx } from "@/lib/cva.config" function getCookie(name: string) { - const cookieValue = document.cookie - .split("; ") - .find((row) => row.startsWith(name + "=")); - return cookieValue ? cookieValue.split("=")[1] : null; + const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "=")) + return cookieValue ? cookieValue.split("=")[1] : null } function formatUserRole(userRole: string) { if (!userRole) { - return "Undefined Role"; + return "Undefined Role" } switch (userRole.toLowerCase()) { case "app_owner": - return "App Owner"; + return "App Owner" case "demo_app_owner": - return "App Owner"; + return "App Owner" case "app_admin": - return "Admin"; + return "Admin" case "proxy_admin": - return "Admin"; + return "Admin" case "proxy_admin_viewer": - return "Admin Viewer"; + return "Admin Viewer" case "org_admin": - return "Org Admin"; + return "Org Admin" case "internal_user": - return "Internal User"; + return "Internal User" case "internal_user_viewer": case "internal_viewer": // TODO:remove if deprecated - return "Internal Viewer"; + return "Internal Viewer" case "app_user": - return "App User"; + return "App User" default: - return "Unknown Role"; + return "Unknown Role" } } interface ProxySettings { - PROXY_BASE_URL: string; - PROXY_LOGOUT_URL: string; + PROXY_BASE_URL: string + PROXY_LOGOUT_URL: string } -const queryClient = new QueryClient(); +const queryClient = new QueryClient() function LoadingScreen() { return (
-
- 🚅 LiteLLM -
+
🚅 LiteLLM
Loading...
- ); + ) } export default function CreateKeyPage() { - const [userRole, setUserRole] = useState(""); - const [premiumUser, setPremiumUser] = useState(false); - const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] = - useState(false); - const [userEmail, setUserEmail] = useState(null); - const [teams, setTeams] = useState(null); - const [keys, setKeys] = useState([]); - const [organizations, setOrganizations] = useState([]); - const [userModels, setUserModels] = useState([]); + const [userRole, setUserRole] = useState("") + const [premiumUser, setPremiumUser] = useState(false) + const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] = useState(false) + const [userEmail, setUserEmail] = useState(null) + const [teams, setTeams] = useState(null) + const [keys, setKeys] = useState([]) + const [organizations, setOrganizations] = useState([]) + const [userModels, setUserModels] = useState([]) const [proxySettings, setProxySettings] = useState({ PROXY_BASE_URL: "", PROXY_LOGOUT_URL: "", - }); + }) - const [showSSOBanner, setShowSSOBanner] = useState(true); - const searchParams = useSearchParams()!; - const [modelData, setModelData] = useState({ data: [] }); - const [token, setToken] = useState(null); - const [createClicked, setCreateClicked] = useState(false); - const [authLoading, setAuthLoading] = useState(true); - const [userID, setUserID] = useState(null); + const [showSSOBanner, setShowSSOBanner] = useState(true) + const searchParams = useSearchParams()! + const [modelData, setModelData] = useState({ data: [] }) + const [token, setToken] = useState(null) + const [createClicked, setCreateClicked] = useState(false) + const [authLoading, setAuthLoading] = useState(true) + const [userID, setUserID] = useState(null) - const invitation_id = searchParams.get("invitation_id"); + const invitation_id = searchParams.get("invitation_id") // Get page from URL, default to 'api-keys' if not present const [page, setPage] = useState(() => { - return searchParams.get("page") || "api-keys"; - }); + return searchParams.get("page") || "api-keys" + }) // Custom setPage function that updates URL const updatePage = (newPage: string) => { // Update URL without full page reload - const newSearchParams = new URLSearchParams(searchParams); - newSearchParams.set("page", newPage); + const newSearchParams = new URLSearchParams(searchParams) + newSearchParams.set("page", newPage) // Use Next.js router to update URL - window.history.pushState(null, "", `?${newSearchParams.toString()}`); + window.history.pushState(null, "", `?${newSearchParams.toString()}`) - setPage(newPage); - }; + setPage(newPage) + } - const [accessToken, setAccessToken] = useState(null); + const [accessToken, setAccessToken] = useState(null) const addKey = (data: any) => { - setKeys((prevData) => (prevData ? [...prevData, data] : [data])); - setCreateClicked(() => !createClicked); - }; - const redirectToLogin = - authLoading === false && token === null && invitation_id === null; + setKeys((prevData) => (prevData ? [...prevData, data] : [data])) + setCreateClicked(() => !createClicked) + } + const redirectToLogin = authLoading === false && token === null && invitation_id === null useEffect(() => { - const token = getCookie("token"); + const token = getCookie("token") getUiConfig().then((data) => { // get the information for constructing the proxy base url, and then set the token and auth loading - setToken(token); - setAuthLoading(false); - }); - }, []); + setToken(token) + setAuthLoading(false) + }) + }, []) useEffect(() => { if (redirectToLogin) { - window.location.href = (proxyBaseUrl || "") + "/sso/key/generate"; + window.location.href = (proxyBaseUrl || "") + "/sso/key/generate" } - }, [redirectToLogin]); + }, [redirectToLogin]) useEffect(() => { if (!token) { - return; + return } - const decoded = jwtDecode(token) as { [key: string]: any }; + const decoded = jwtDecode(token) as { [key: string]: any } if (decoded) { // set accessToken - setAccessToken(decoded.key); + setAccessToken(decoded.key) - setDisabledPersonalKeyCreation( - decoded.disabled_non_admin_personal_key_creation - ); + setDisabledPersonalKeyCreation(decoded.disabled_non_admin_personal_key_creation) // check if userRole is defined if (decoded.user_role) { - const formattedUserRole = formatUserRole(decoded.user_role); - setUserRole(formattedUserRole); + const formattedUserRole = formatUserRole(decoded.user_role) + setUserRole(formattedUserRole) if (formattedUserRole == "Admin Viewer") { - setPage("usage"); + setPage("usage") } } if (decoded.user_email) { - setUserEmail(decoded.user_email); + setUserEmail(decoded.user_email) } if (decoded.login_method) { - setShowSSOBanner( - decoded.login_method == "username_password" ? true : false - ); + setShowSSOBanner(decoded.login_method == "username_password" ? true : false) } if (decoded.premium_user) { - setPremiumUser(decoded.premium_user); + setPremiumUser(decoded.premium_user) } if (decoded.auth_header_name) { - setGlobalLitellmHeaderName(decoded.auth_header_name); + setGlobalLitellmHeaderName(decoded.auth_header_name) } if (decoded.user_id) { - setUserID(decoded.user_id); + setUserID(decoded.user_id) } } - }, [token]); + }, [token]) useEffect(() => { if (accessToken && userID && userRole) { - fetchUserModels(userID, userRole, accessToken, setUserModels); + fetchUserModels(userID, userRole, accessToken, setUserModels) } if (accessToken && userID && userRole) { - fetchTeams(accessToken, userID, userRole, null, setTeams); + fetchTeams(accessToken, userID, userRole, null, setTeams) } if (accessToken) { - fetchOrganizations(accessToken, setOrganizations); + fetchOrganizations(accessToken, setOrganizations) } - }, [accessToken, userID, userRole]); + }, [accessToken, userID, userRole]) if (authLoading || redirectToLogin) { - return ; + return } return ( @@ -237,226 +223,199 @@ export default function CreateKeyPage() { {invitation_id ? ( - ) : ( -
- -
-
- -
+ ) : ( +
+ +
+
+ +
- {page == "api-keys" ? ( - - ) : page == "models" ? ( - - ) : page == "llm-playground" ? ( - - ) : page == "users" ? ( - - ) : page == "teams" ? ( - - ) : page == "organizations" ? ( - - ) : page == "admin-panel" ? ( - - ) : page == "api_ref" ? ( - - ) : page == "settings" ? ( - - ) : page == "budgets" ? ( - - ) : page == "guardrails" ? ( - - ) : page == "prompts" ? ( - - ) : page == "transform-request" ? ( - - ) : page == "general-settings" ? ( - - ) : page == "ui-theme" ? ( - - ) : page == "model-hub-table" ? ( - - ) : page == "caching" ? ( - - ) : page == "pass-through-settings" ? ( - - ) : page == "logs" ? ( - - ) : page == "mcp-servers" ? ( - - ) : page == "tag-management" ? ( - - ) : page == "vector-stores" ? ( - - ) : page == "new_usage" ? ( - - ) : ( - - )} + {page == "api-keys" ? ( + + ) : page == "models" ? ( + + ) : page == "llm-playground" ? ( + + ) : page == "users" ? ( + + ) : page == "teams" ? ( + + ) : page == "organizations" ? ( + + ) : page == "admin-panel" ? ( + + ) : page == "api_ref" ? ( + + ) : page == "settings" ? ( + + ) : page == "budgets" ? ( + + ) : page == "guardrails" ? ( + + ) : page == "prompts" ? ( + + ) : page == "transform-request" ? ( + + ) : page == "general-settings" ? ( + + ) : page == "ui-theme" ? ( + + ) : page == "model-hub-table" ? ( + + ) : page == "caching" ? ( + + ) : page == "pass-through-settings" ? ( + + ) : page == "logs" ? ( + + ) : page == "mcp-servers" ? ( + + ) : page == "tag-management" ? ( + + ) : page == "vector-stores" ? ( + + ) : page == "new_usage" ? ( + + ) : ( + + )} +
-
- )} + )} - ); + ) } diff --git a/ui/litellm-dashboard/src/components/all_keys_table.tsx b/ui/litellm-dashboard/src/components/all_keys_table.tsx index aa2b92a585f..34116d3f4b1 100644 --- a/ui/litellm-dashboard/src/components/all_keys_table.tsx +++ b/ui/litellm-dashboard/src/components/all_keys_table.tsx @@ -1,68 +1,54 @@ -"use client"; -import React, { useEffect, useState, useCallback, useRef } from "react"; -import { ColumnDef, Row } from "@tanstack/react-table"; -import { DataTable } from "./view_logs/table"; +"use client" +import React, { useEffect, useState, useCallback, useRef } from "react" +import { ColumnDef, Row } from "@tanstack/react-table" +import { DataTable } from "./view_logs/table" import { Select, SelectItem } from "@tremor/react" import { Button } from "@tremor/react" -import KeyInfoView from "./key_info_view"; -import { Tooltip } from "antd"; -import { Team, KeyResponse } from "./key_team_helpers/key_list"; -import FilterComponent from "./common_components/filter"; -import { FilterOption } from "./common_components/filter"; -import { keyListCall, Organization, userListCall } from "./networking"; -import { createTeamSearchFunction } from "./key_team_helpers/team_search_fn"; -import { createOrgSearchFunction } from "./key_team_helpers/organization_search_fn"; -import { useFilterLogic } from "./key_team_helpers/filter_logic"; -import { Setter } from "@/types"; -import { updateExistingKeys } from "@/utils/dataUtils"; -import { debounce } from "lodash"; -import { defaultPageSize } from "./constants"; -import { fetchAllTeams } from "./key_team_helpers/filter_helpers"; -import { fetchAllOrganizations } from "./key_team_helpers/filter_helpers"; -import { - flexRender, - getCoreRowModel, - getSortedRowModel, - SortingState, - useReactTable, -} from "@tanstack/react-table"; -import { - Table, - TableHead, - TableHeaderCell, - TableBody, - TableRow, - TableCell, - Icon, -} from "@tremor/react"; -import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline"; -import { Badge, Text } from "@tremor/react"; -import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; +import KeyInfoView from "./templates/key_info_view" +import { Tooltip } from "antd" +import { Team, KeyResponse } from "./key_team_helpers/key_list" +import FilterComponent from "./common_components/filter" +import { FilterOption } from "./common_components/filter" +import { keyListCall, Organization, userListCall } from "./networking" +import { createTeamSearchFunction } from "./key_team_helpers/team_search_fn" +import { createOrgSearchFunction } from "./key_team_helpers/organization_search_fn" +import { useFilterLogic } from "./key_team_helpers/filter_logic" +import { Setter } from "@/types" +import { updateExistingKeys } from "@/utils/dataUtils" +import { debounce } from "lodash" +import { defaultPageSize } from "./constants" +import { fetchAllTeams } from "./key_team_helpers/filter_helpers" +import { fetchAllOrganizations } from "./key_team_helpers/filter_helpers" +import { flexRender, getCoreRowModel, getSortedRowModel, SortingState, useReactTable } from "@tanstack/react-table" +import { Table, TableHead, TableHeaderCell, TableBody, TableRow, TableCell, Icon } from "@tremor/react" +import { SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline" +import { Badge, Text } from "@tremor/react" +import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key" +import { formatNumberWithCommas } from "@/utils/dataUtils" interface AllKeysTableProps { keys: KeyResponse[] setKeys: (keys: KeyResponse[] | ((prev: KeyResponse[]) => KeyResponse[])) => void isLoading?: boolean pagination: { - currentPage: number; - totalPages: number; - totalCount: number; - }; - onPageChange: (page: number) => void; - pageSize?: number; - teams: Team[] | null; - selectedTeam: Team | null; - setSelectedTeam: (team: Team | null) => void; - selectedKeyAlias: string | null; - setSelectedKeyAlias: Setter; - accessToken: string | null; - userID: string | null; - userRole: string | null; - organizations: Organization[] | null; - setCurrentOrg: React.Dispatch>; - refresh?: () => void; - onSortChange?: (sortBy: string, sortOrder: 'asc' | 'desc') => void; + currentPage: number + totalPages: number + totalCount: number + } + onPageChange: (page: number) => void + pageSize?: number + teams: Team[] | null + selectedTeam: Team | null + setSelectedTeam: (team: Team | null) => void + selectedKeyAlias: string | null + setSelectedKeyAlias: Setter + accessToken: string | null + userID: string | null + userRole: string | null + organizations: Organization[] | null + setCurrentOrg: React.Dispatch> + refresh?: () => void + onSortChange?: (sortBy: string, sortOrder: "asc" | "desc") => void currentSort?: { sortBy: string sortOrder: "asc" | "desc" @@ -74,57 +60,55 @@ interface AllKeysTableProps { // Define columns similar to our logs table interface UserResponse { - user_id: string; - user_email: string; - user_role: string; + user_id: string + user_email: string + user_role: string } -const TeamFilter = ({ - teams, - selectedTeam, - setSelectedTeam -}: { - teams: Team[] | null; - selectedTeam: Team | null; - setSelectedTeam: (team: Team | null) => void; +const TeamFilter = ({ + teams, + selectedTeam, + setSelectedTeam, +}: { + teams: Team[] | null + selectedTeam: Team | null + setSelectedTeam: (team: Team | null) => void }) => { - const handleTeamChange = (value: string) => { - const team = teams?.find(t => t.team_id === value); - setSelectedTeam(team || null); - }; - - return ( -
-
- Where Team is - -
+ const handleTeamChange = (value: string) => { + const team = teams?.find((t) => t.team_id === value) + setSelectedTeam(team || null) + } + + return ( +
+
+ Where Team is +
- ); - }; - +
+ ) +} /** * AllKeysTable – a new table for keys that mimics the table styling used in view_logs. * The team selector and filtering have been removed so that all keys are shown. */ - -export function AllKeysTable({ - keys, +export function AllKeysTable({ + keys, setKeys, isLoading = false, pagination, @@ -146,67 +130,62 @@ export function AllKeysTable({ premiumUser, setAccessToken, }: AllKeysTableProps) { - const [selectedKeyId, setSelectedKeyId] = useState(null); - const [userList, setUserList] = useState([]); + const [selectedKeyId, setSelectedKeyId] = useState(null) + const [userList, setUserList] = useState([]) const [sorting, setSorting] = React.useState(() => { if (currentSort) { - return [{ - id: currentSort.sortBy, - desc: currentSort.sortOrder === 'desc' - }]; + return [ + { + id: currentSort.sortBy, + desc: currentSort.sortOrder === "desc", + }, + ] } - return [{ - id: "created_at", - desc: true - }]; - }); - const [expandedAccordions, setExpandedAccordions] = useState>({}); + return [ + { + id: "created_at", + desc: true, + }, + ] + }) + const [expandedAccordions, setExpandedAccordions] = useState>({}) // Use the filter logic hook - const { - filters, - filteredKeys, - allKeyAliases, - allTeams, - allOrganizations, - handleFilterChange, - handleFilterReset - } = useFilterLogic({ - keys, - teams, - organizations, - accessToken, - }); - - + const { filters, filteredKeys, allKeyAliases, allTeams, allOrganizations, handleFilterChange, handleFilterReset } = + useFilterLogic({ + keys, + teams, + organizations, + accessToken, + }) useEffect(() => { if (accessToken) { - const user_IDs = keys.map(key => key.user_id).filter(id => id !== null); + const user_IDs = keys.map((key) => key.user_id).filter((id) => id !== null) const fetchUserList = async () => { - const userListData = await userListCall(accessToken, user_IDs, 1, 100); - setUserList(userListData.users); - }; - fetchUserList(); + const userListData = await userListCall(accessToken, user_IDs, 1, 100) + setUserList(userListData.users) + } + fetchUserList() } - }, [accessToken, keys]); + }, [accessToken, keys]) // Add a useEffect to call refresh when a key is created useEffect(() => { if (refresh) { const handleStorageChange = () => { - refresh(); - }; - + refresh() + } + // Listen for storage events that might indicate a key was created - window.addEventListener('storage', handleStorageChange); - + window.addEventListener("storage", handleStorageChange) + return () => { - window.removeEventListener('storage', handleStorageChange); - }; + window.removeEventListener("storage", handleStorageChange) + } } - }, [refresh]); + }, [refresh]) const columns: ColumnDef[] = [ { @@ -214,10 +193,7 @@ export function AllKeysTable({ header: () => null, cell: ({ row }) => row.getCanExpand() ? ( - ) : null, @@ -229,7 +205,7 @@ export function AllKeysTable({ cell: (info) => (
-
) : null}
- ); + ) }, }, { id: "rate_limits", header: "Rate Limits", cell: ({ row }) => { - const key = row.original; + const key = row.original return (
TPM: {key.tpm_limit !== null ? key.tpm_limit : "Unlimited"}
RPM: {key.rpm_limit !== null ? key.rpm_limit : "Unlimited"}
- ); + ) }, }, - ]; + ] const filterOptions: FilterOption[] = [ - { - name: 'Team ID', - label: 'Team ID', - isSearchable: true, + { + name: "Team ID", + label: "Team ID", + isSearchable: true, searchFn: async (searchText: string) => { - if (!allTeams || allTeams.length === 0) return []; - - const filteredTeams = allTeams.filter(team => - team.team_id.toLowerCase().includes(searchText.toLowerCase()) || - (team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase())) - ); - - return filteredTeams.map(team => ({ + if (!allTeams || allTeams.length === 0) return [] + + const filteredTeams = allTeams.filter( + (team) => + team.team_id.toLowerCase().includes(searchText.toLowerCase()) || + (team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase())), + ) + + return filteredTeams.map((team) => ({ label: `${team.team_alias || team.team_id} (${team.team_id})`, - value: team.team_id - })); - } + value: team.team_id, + })) + }, }, - { - name: 'Organization ID', - label: 'Organization ID', - isSearchable: true, + { + name: "Organization ID", + label: "Organization ID", + isSearchable: true, searchFn: async (searchText: string) => { - if (!allOrganizations || allOrganizations.length === 0) return []; - - const filteredOrgs = allOrganizations.filter(org => - org.organization_id?.toLowerCase().includes(searchText.toLowerCase()) ?? false - ); - + if (!allOrganizations || allOrganizations.length === 0) return [] + + const filteredOrgs = allOrganizations.filter( + (org) => org.organization_id?.toLowerCase().includes(searchText.toLowerCase()) ?? false, + ) + return filteredOrgs - .filter(org => org.organization_id !== null && org.organization_id !== undefined) - .map(org => ({ - label: `${org.organization_id || 'Unknown'} (${org.organization_id})`, - value: org.organization_id as string - })); - } + .filter((org) => org.organization_id !== null && org.organization_id !== undefined) + .map((org) => ({ + label: `${org.organization_id || "Unknown"} (${org.organization_id})`, + value: org.organization_id as string, + })) + }, }, { name: "Key Alias", label: "Key Alias", isSearchable: true, searchFn: async (searchText) => { - const filteredKeyAliases = allKeyAliases.filter(key => { + const filteredKeyAliases = allKeyAliases.filter((key) => { return key.toLowerCase().includes(searchText.toLowerCase()) - }); + }) return filteredKeyAliases.map((key) => { return { label: key, - value: key + value: key, } - }); - } + }) + }, }, { - name: 'User ID', - label: 'User ID', + name: "User ID", + label: "User ID", isSearchable: false, }, { - name: 'Key Hash', - label: 'Key Hash', + name: "Key Hash", + label: "Key Hash", isSearchable: false, - } + }, + ] - ]; - console.log(`keys: ${JSON.stringify(keys)}`) const table = useReactTable({ data: filteredKeys, - columns: columns.filter(col => col.id !== 'expander'), + columns: columns.filter((col) => col.id !== "expander"), state: { sorting, }, onSortingChange: (updaterOrValue) => { - const newSorting = typeof updaterOrValue === 'function' - ? updaterOrValue(sorting) - : updaterOrValue; + const newSorting = typeof updaterOrValue === "function" ? updaterOrValue(sorting) : updaterOrValue console.log(`newSorting: ${JSON.stringify(newSorting)}`) - setSorting(newSorting); + setSorting(newSorting) if (newSorting && newSorting.length > 0) { - const sortState = newSorting[0]; - const sortBy = sortState.id; - const sortOrder = sortState.desc ? 'desc' : 'asc'; + const sortState = newSorting[0] + const sortBy = sortState.id + const sortOrder = sortState.desc ? "desc" : "asc" console.log(`sortBy: ${sortBy}, sortOrder: ${sortOrder}`) handleFilterChange({ ...filters, - 'Sort By': sortBy, - 'Sort Order': sortOrder - }); - onSortChange?.(sortBy, sortOrder); + "Sort By": sortBy, + "Sort Order": sortOrder, + }) + onSortChange?.(sortBy, sortOrder) } }, getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), enableSorting: true, manualSorting: false, - }); + }) // Update local sorting state when currentSort prop changes React.useEffect(() => { if (currentSort) { - setSorting([{ - id: currentSort.sortBy, - desc: currentSort.sortOrder === 'desc' - }]); + setSorting([ + { + id: currentSort.sortBy, + desc: currentSort.sortOrder === "desc", + }, + ]) } - }, [currentSort]); + }, [currentSort]) return (
{selectedKeyId ? ( - setSelectedKeyId(null)} - keyData={filteredKeys.find(k => k.token === selectedKeyId)} + keyData={filteredKeys.find((k) => k.token === selectedKeyId)} onKeyDataUpdate={(updatedKeyData) => { - setKeys(keys => keys.map(key => { - if (key.token === updatedKeyData.token) { - return updateExistingKeys(key, updatedKeyData) - } - return key - })) - if (refresh) refresh(); // Minimal fix: refresh the full key list after an update + setKeys((keys) => + keys.map((key) => { + if (key.token === updatedKeyData.token) { + return updateExistingKeys(key, updatedKeyData) + } + return key + }), + ) + if (refresh) refresh() // Minimal fix: refresh the full key list after an update }} onDelete={() => { - setKeys(keys => keys.filter(key => key.token !== selectedKeyId)) - if (refresh) refresh(); // Minimal fix: refresh the full key list after a delete + setKeys((keys) => keys.filter((key) => key.token !== selectedKeyId)) + if (refresh) refresh() // Minimal fix: refresh the full key list after a delete }} accessToken={accessToken} userID={userID} @@ -632,19 +602,28 @@ export function AllKeysTable({ ) : (
- +
- Showing {isLoading ? "..." : `${(pagination.currentPage - 1) * pageSize + 1} - ${Math.min(pagination.currentPage * pageSize, pagination.totalCount)}`} of {isLoading ? "..." : pagination.totalCount} results + Showing{" "} + {isLoading + ? "..." + : `${(pagination.currentPage - 1) * pageSize + 1} - ${Math.min(pagination.currentPage * pageSize, pagination.totalCount)}`}{" "} + of {isLoading ? "..." : pagination.totalCount} results - +
Page {isLoading ? "..." : pagination.currentPage} of {isLoading ? "..." : pagination.totalPages} - + - + , - ] : [ - , - , - ]} + footer={ + regeneratedKey + ? [ + , + ] + : [ + , + , + ] + } > {regeneratedKey ? ( Regenerated Key

- Please replace your old key with the new key generated. For - security reasons, you will not be able to view it again{" "} - through your LiteLLM account. If you lose this secret key, you - will need to generate a new one. + Please replace your old key with the new key generated. For security reasons,{" "} + you will not be able to view it again through your LiteLLM account. If you lose this secret key, + you will need to generate a new one.

Key Alias:
-
-                {selectedToken?.key_alias || "No alias set"}
-              
+
{selectedToken?.key_alias || "No alias set"}
New API Key:
{regeneratedKey}
- message.success("API Key copied to clipboard")} - > + NotificationManager.success("API Key copied to clipboard")}> @@ -215,7 +214,7 @@ export function RegenerateKeyModal({ layout="vertical" onValuesChange={(changedValues) => { if ("duration" in changedValues) { - setRegenerateFormData((prev: { duration?: string }) => ({ ...prev, duration: changedValues.duration })); + setRegenerateFormData((prev: { duration?: string }) => ({ ...prev, duration: changedValues.duration })) } }} > @@ -237,13 +236,9 @@ export function RegenerateKeyModal({
Current expiry: {selectedToken?.expires ? new Date(selectedToken.expires).toLocaleString() : "Never"}
- {newExpiryTime && ( -
- New expiry: {newExpiryTime} -
- )} + {newExpiryTime &&
New expiry: {newExpiryTime}
} )} - ); -} \ No newline at end of file + ) +} diff --git a/ui/litellm-dashboard/src/components/tag_management/tag_info.tsx b/ui/litellm-dashboard/src/components/tag_management/tag_info.tsx index ac8f3672581..a727ff72695 100644 --- a/ui/litellm-dashboard/src/components/tag_management/tag_info.tsx +++ b/ui/litellm-dashboard/src/components/tag_management/tag_info.tsx @@ -1,149 +1,116 @@ -import React, { useState, useEffect } from "react"; -import { - Card, - Text, - Title, - Button, - Badge, -} from "@tremor/react"; -import { - Form, - Input, - Select as Select2, - message, - Tooltip, -} from "antd"; -import { InfoCircleOutlined } from '@ant-design/icons'; -import { fetchUserModels } from "../create_key_button"; -import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key"; -import { tagInfoCall, tagUpdateCall } from "../networking"; -import { Tag, TagInfoResponse } from "./types"; +import React, { useState, useEffect } from "react" +import { Card, Text, Title, Button, Badge } from "@tremor/react" +import { Form, Input, Select as Select2, message, Tooltip } from "antd" +import { InfoCircleOutlined } from "@ant-design/icons" +import { fetchUserModels } from "../organisms/create_key_button" +import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key" +import { tagInfoCall, tagUpdateCall } from "../networking" +import { Tag, TagInfoResponse } from "./types" interface TagInfoViewProps { - tagId: string; - onClose: () => void; - accessToken: string | null; - is_admin: boolean; - editTag: boolean; + tagId: string + onClose: () => void + accessToken: string | null + is_admin: boolean + editTag: boolean } -const TagInfoView: React.FC = ({ - tagId, - onClose, - accessToken, - is_admin, - editTag, -}) => { - const [form] = Form.useForm(); - const [tagDetails, setTagDetails] = useState(null); - const [isEditing, setIsEditing] = useState(editTag); - const [userModels, setUserModels] = useState([]); +const TagInfoView: React.FC = ({ tagId, onClose, accessToken, is_admin, editTag }) => { + const [form] = Form.useForm() + const [tagDetails, setTagDetails] = useState(null) + const [isEditing, setIsEditing] = useState(editTag) + const [userModels, setUserModels] = useState([]) const fetchTagDetails = async () => { - if (!accessToken) return; + if (!accessToken) return try { - const response = await tagInfoCall(accessToken, [tagId]); - const tagData = response[tagId]; + const response = await tagInfoCall(accessToken, [tagId]) + const tagData = response[tagId] if (tagData) { - setTagDetails(tagData); + setTagDetails(tagData) if (editTag) { form.setFieldsValue({ name: tagData.name, description: tagData.description, models: tagData.models, - }); + }) } } } catch (error) { - console.error("Error fetching tag details:", error); - message.error("Error fetching tag details: " + error); + console.error("Error fetching tag details:", error) + message.error("Error fetching tag details: " + error) } - }; + } useEffect(() => { - fetchTagDetails(); - }, [tagId, accessToken]); + fetchTagDetails() + }, [tagId, accessToken]) useEffect(() => { if (accessToken) { // Using dummy values for userID and userRole since they're required by the function // TODO: Pass these as props if needed for the actual API implementation - fetchUserModels("dummy-user", "Admin", accessToken, setUserModels); + fetchUserModels("dummy-user", "Admin", accessToken, setUserModels) } - }, [accessToken]); + }, [accessToken]) const handleSave = async (values: any) => { - if (!accessToken) return; + if (!accessToken) return try { await tagUpdateCall(accessToken, { name: values.name, description: values.description, models: values.models, - }); - message.success("Tag updated successfully"); - setIsEditing(false); - fetchTagDetails(); + }) + message.success("Tag updated successfully") + setIsEditing(false) + fetchTagDetails() } catch (error) { - console.error("Error updating tag:", error); - message.error("Error updating tag: " + error); + console.error("Error updating tag:", error) + message.error("Error updating tag: " + error) } - }; + } if (!tagDetails) { - return
Loading...
; + return
Loading...
} return (
- + Tag Name: {tagDetails.name} {tagDetails.description || "No description"}
- {is_admin && !isEditing && ( - - )} + {is_admin && !isEditing && }
{isEditing ? ( -
- + + - + - Allowed LLMs{' '} + Allowed LLMs{" "} - + } name="models" > - + {userModels.map((modelId) => ( {getModelDisplayName(modelId)} @@ -179,9 +146,7 @@ const TagInfoView: React.FC = ({ ) : ( tagDetails.models.map((modelId) => ( - - {tagDetails.model_info?.[modelId] || modelId} - + {tagDetails.model_info?.[modelId] || modelId} )) )} @@ -200,7 +165,7 @@ const TagInfoView: React.FC = ({
)}
- ); -}; + ) +} -export default TagInfoView; \ No newline at end of file +export default TagInfoView diff --git a/ui/litellm-dashboard/src/components/key_edit_view.tsx b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx similarity index 58% rename from ui/litellm-dashboard/src/components/key_edit_view.tsx rename to ui/litellm-dashboard/src/components/templates/key_edit_view.tsx index 69102a1591d..9462a9e1057 100644 --- a/ui/litellm-dashboard/src/components/key_edit_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_edit_view.tsx @@ -1,137 +1,131 @@ -import React, { useState, useEffect } from "react"; -import { Form, Input, Select, Button as AntdButton, Tooltip } from "antd"; -import { Button as TremorButton, TextInput } from "@tremor/react"; -import { KeyResponse } from "./key_team_helpers/key_list"; -import { fetchTeamModels } from "../components/create_key_button"; -import { modelAvailableCall, getPromptsList } from "./networking"; -import NumericalInput from "./shared/numerical_input"; -import VectorStoreSelector from "./vector_store_management/VectorStoreSelector"; -import MCPServerSelector from "./mcp_server_management/MCPServerSelector"; -import EditLoggingSettings from "./team/EditLoggingSettings"; -import { extractLoggingSettings, formatMetadataForDisplay } from "./key_info_utils"; -import { fetchMCPAccessGroups } from "./networking"; -import { mapInternalToDisplayNames, mapDisplayToInternalNames } from "./callback_info_helpers"; +import React, { useState, useEffect } from "react" +import { Form, Input, Select, Button as AntdButton, Tooltip } from "antd" +import { Button as TremorButton, TextInput } from "@tremor/react" +import { KeyResponse } from "../key_team_helpers/key_list" +import { fetchTeamModels } from "../organisms/create_key_button" +import { modelAvailableCall, getPromptsList } from "../networking" +import NumericalInput from "../shared/numerical_input" +import VectorStoreSelector from "../vector_store_management/VectorStoreSelector" +import MCPServerSelector from "../mcp_server_management/MCPServerSelector" +import EditLoggingSettings from "../team/EditLoggingSettings" +import { extractLoggingSettings, formatMetadataForDisplay } from "../key_info_utils" +import { fetchMCPAccessGroups } from "../networking" +import { mapInternalToDisplayNames, mapDisplayToInternalNames } from "../callback_info_helpers" interface KeyEditViewProps { - keyData: KeyResponse; - onCancel: () => void; - onSubmit: (values: any) => Promise; - teams?: any[] | null; - accessToken: string | null; - userID: string | null; - userRole: string | null; - premiumUser?: boolean; + keyData: KeyResponse + onCancel: () => void + onSubmit: (values: any) => Promise + teams?: any[] | null + accessToken: string | null + userID: string | null + userRole: string | null + premiumUser?: boolean } // Add this helper function const getAvailableModelsForKey = (keyData: KeyResponse, teams: any[] | null): string[] => { // If no teams data is available, return empty array - console.log("getAvailableModelsForKey:", teams); + console.log("getAvailableModelsForKey:", teams) if (!teams || !keyData.team_id) { - return []; + return [] } // Find the team that matches the key's team_id - const keyTeam = teams.find(team => team.team_id === keyData.team_id); - + const keyTeam = teams.find((team) => team.team_id === keyData.team_id) + // If team found and has models, return those models if (keyTeam?.models) { - return keyTeam.models; + return keyTeam.models } - return []; -}; + return [] +} -export function KeyEditView({ - keyData, - onCancel, - onSubmit, - teams, - accessToken, - userID, - userRole, - premiumUser = false +export function KeyEditView({ + keyData, + onCancel, + onSubmit, + teams, + accessToken, + userID, + userRole, + premiumUser = false, }: KeyEditViewProps) { - const [form] = Form.useForm(); - const [userModels, setUserModels] = useState([]); - const [promptsList, setPromptsList] = useState([]); - const team = teams?.find(team => team.team_id === keyData.team_id); - const [availableModels, setAvailableModels] = useState([]); - const [mcpAccessGroups, setMcpAccessGroups] = useState([]); - const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false); + const [form] = Form.useForm() + const [userModels, setUserModels] = useState([]) + const [promptsList, setPromptsList] = useState([]) + const team = teams?.find((team) => team.team_id === keyData.team_id) + const [availableModels, setAvailableModels] = useState([]) + const [mcpAccessGroups, setMcpAccessGroups] = useState([]) + const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false) const [disabledCallbacks, setDisabledCallbacks] = useState( - Array.isArray(keyData.metadata?.litellm_disabled_callbacks) + Array.isArray(keyData.metadata?.litellm_disabled_callbacks) ? mapInternalToDisplayNames(keyData.metadata.litellm_disabled_callbacks) - : [] - ); + : [], + ) const fetchMcpAccessGroups = async () => { - if (!accessToken) return; - if (mcpAccessGroupsLoaded) return; + if (!accessToken) return + if (mcpAccessGroupsLoaded) return try { - const groups = await fetchMCPAccessGroups(accessToken); - setMcpAccessGroups(groups); - setMcpAccessGroupsLoaded(true); + const groups = await fetchMCPAccessGroups(accessToken) + setMcpAccessGroups(groups) + setMcpAccessGroupsLoaded(true) } catch (error) { - console.error("Failed to fetch MCP access groups:", error); + console.error("Failed to fetch MCP access groups:", error) } - }; + } useEffect(() => { const fetchModels = async () => { - if (!userID || !userRole || !accessToken) return; + if (!userID || !userRole || !accessToken) return try { if (keyData.team_id === null) { // Fetch user models if no team - const model_available = await modelAvailableCall( - accessToken, - userID, - userRole - ); - const available_model_names = model_available["data"].map( - (element: { id: string }) => element.id - ); - setAvailableModels(available_model_names); + const model_available = await modelAvailableCall(accessToken, userID, userRole) + const available_model_names = model_available["data"].map((element: { id: string }) => element.id) + setAvailableModels(available_model_names) } else if (team?.team_id) { // Fetch team models if team exists - const models = await fetchTeamModels(userID, userRole, accessToken, team.team_id); - setAvailableModels(Array.from(new Set([...team.models, ...models]))); + const models = await fetchTeamModels(userID, userRole, accessToken, team.team_id) + setAvailableModels(Array.from(new Set([...team.models, ...models]))) } } catch (error) { - console.error("Error fetching models:", error); + console.error("Error fetching models:", error) } - }; + } const fetchPrompts = async () => { - if (!accessToken) return; + if (!accessToken) return try { - const response = await getPromptsList(accessToken); - setPromptsList(response.prompts.map(prompt => prompt.prompt_id)); + const response = await getPromptsList(accessToken) + setPromptsList(response.prompts.map((prompt) => prompt.prompt_id)) } catch (error) { - console.error("Failed to fetch prompts:", error); + console.error("Failed to fetch prompts:", error) } - }; + } - fetchPrompts(); - fetchModels(); - }, [userID, userRole, accessToken, team, keyData.team_id]); + fetchPrompts() + fetchModels() + }, [userID, userRole, accessToken, team, keyData.team_id]) // Sync disabled callbacks with form when component mounts useEffect(() => { - form.setFieldValue('disabled_callbacks', disabledCallbacks); - }, [form, disabledCallbacks]); + form.setFieldValue("disabled_callbacks", disabledCallbacks) + }, [form, disabledCallbacks]) // Convert API budget duration to form format const getBudgetDuration = (duration: string | null) => { - if (!duration) return null; + if (!duration) return null const durationMap: Record = { "24h": "daily", "7d": "weekly", - "30d": "monthly" - }; - return durationMap[duration] || null; - }; + "30d": "monthly", + } + return durationMap[duration] || null + } // Set initial form values const initialValues = { @@ -143,39 +137,28 @@ export function KeyEditView({ vector_stores: keyData.object_permission?.vector_stores || [], mcp_servers_and_groups: { servers: keyData.object_permission?.mcp_servers || [], - accessGroups: keyData.object_permission?.mcp_access_groups || [] + accessGroups: keyData.object_permission?.mcp_access_groups || [], }, logging_settings: extractLoggingSettings(keyData.metadata), - disabled_callbacks: Array.isArray(keyData.metadata?.litellm_disabled_callbacks) + disabled_callbacks: Array.isArray(keyData.metadata?.litellm_disabled_callbacks) ? mapInternalToDisplayNames(keyData.metadata.litellm_disabled_callbacks) - : [] - }; + : [], + } - console.log("premiumUser:", premiumUser); + console.log("premiumUser:", premiumUser) return ( - + - {/* Only show All Team Models if team has models */} - {availableModels.length > 0 && ( - All Team Models - )} + {availableModels.length > 0 && All Team Models} {/* Show available team models */} - {availableModels.map(model => ( + {availableModels.map((model) => ( {model} @@ -184,7 +167,7 @@ export function KeyEditView({ - + @@ -196,31 +179,27 @@ export function KeyEditView({ - + - + - - + + - + - + - - + 0 - ? `Current: ${keyData.metadata.prompts.join(', ')}` + ? `Current: ${keyData.metadata.prompts.join(", ")}` : "Select or enter prompts" } - options={promptsList.map(name => ({ value: name, label: name }))} + options={promptsList.map((name) => ({ value: name, label: name }))} /> form.setFieldValue('vector_stores', values)} - value={form.getFieldValue('vector_stores')} + onChange={(values: string[]) => form.setFieldValue("vector_stores", values)} + value={form.getFieldValue("vector_stores")} accessToken={accessToken || ""} placeholder="Select vector stores" /> @@ -268,20 +244,17 @@ export function KeyEditView({ form.setFieldValue('mcp_servers_and_groups', val)} - value={form.getFieldValue('mcp_servers_and_groups')} - accessToken={accessToken || ''} + onChange={(val) => form.setFieldValue("mcp_servers_and_groups", val)} + value={form.getFieldValue("mcp_servers_and_groups")} + accessToken={accessToken || ""} placeholder="Select MCP servers or access groups (optional)" /> - {/* Only show All Team Models if team has models */} - {teams?.map(team => ( + {teams?.map((team) => ( {`${team.team_alias} (${team.team_id})`} @@ -290,25 +263,23 @@ export function KeyEditView({ form.setFieldValue('logging_settings', values)} + value={form.getFieldValue("logging_settings")} + onChange={(values) => form.setFieldValue("logging_settings", values)} disabledCallbacks={disabledCallbacks} onDisabledCallbacksChange={(internalValues) => { // Convert internal values back to display names for UI state - const displayNames = mapInternalToDisplayNames(internalValues); - setDisabledCallbacks(displayNames); + const displayNames = mapInternalToDisplayNames(internalValues) + setDisabledCallbacks(displayNames) // Store internal values in form for submission - form.setFieldValue('disabled_callbacks', internalValues); + form.setFieldValue("disabled_callbacks", internalValues) }} /> - - {/* Hidden form field for token */}
- +
Key Alias {currentKeyData.key_alias || "Not Set"} @@ -548,16 +526,16 @@ export default function KeyInfoView({ : "Unlimited"}
- +
Prompts {Array.isArray(currentKeyData.metadata?.prompts) && currentKeyData.metadata.prompts.length > 0 ? currentKeyData.metadata.prompts.map((prompt, index) => ( - - {prompt} - - )) + + {prompt} + + )) : "No prompts specified"}
@@ -632,5 +610,5 @@ export default function KeyInfoView({
- ); -} \ No newline at end of file + ) +} diff --git a/ui/litellm-dashboard/src/components/view_key_table.tsx b/ui/litellm-dashboard/src/components/templates/view_key_table.tsx similarity index 62% rename from ui/litellm-dashboard/src/components/view_key_table.tsx rename to ui/litellm-dashboard/src/components/templates/view_key_table.tsx index 094ae80ae65..ca4260d4f25 100644 --- a/ui/litellm-dashboard/src/components/view_key_table.tsx +++ b/ui/litellm-dashboard/src/components/templates/view_key_table.tsx @@ -1,26 +1,21 @@ -"use client"; -import React, { useEffect, useState, useMemo } from "react"; -import { - keyDeleteCall, - modelAvailableCall, - getGuardrailsList, - Organization, -} from "./networking"; -import { add } from "date-fns"; +"use client" +import React, { useEffect, useState, useMemo } from "react" +import { keyDeleteCall, modelAvailableCall, getGuardrailsList, Organization } from "../networking" +import { add } from "date-fns" import { InformationCircleIcon, StatusOnlineIcon, TrashIcon, PencilAltIcon, RefreshIcon, -} from "@heroicons/react/outline"; +} from "@heroicons/react/outline" import { keySpendLogsCall, PredictedSpendLogsCall, keyUpdateCall, modelInfoCall, regenerateKeyCall, -} from "./networking"; +} from "../networking" import { Badge, Card, @@ -44,16 +39,13 @@ import { Textarea, Select, SelectItem, -} from "@tremor/react"; -import { InfoCircleOutlined } from "@ant-design/icons"; +} from "@tremor/react" +import { InfoCircleOutlined } from "@ant-design/icons" import { fetchAvailableModelsForTeamOrKey, getModelDisplayName, -} from "./key_team_helpers/fetch_available_models_team_key"; -import { - MultiSelect, - MultiSelectItem, -} from "@tremor/react"; +} from "../key_team_helpers/fetch_available_models_team_key" +import { MultiSelect, MultiSelectItem } from "@tremor/react" import { Button as Button2, Modal, @@ -64,28 +56,30 @@ import { message, Tooltip, DatePicker, -} from "antd"; -import { CopyToClipboard } from "react-copy-to-clipboard"; -import TextArea from "antd/es/input/TextArea"; -import useKeyList from "./key_team_helpers/key_list"; -import { KeyResponse } from "./key_team_helpers/key_list"; -import { AllKeysTable } from "./all_keys_table"; -import { Team } from "./key_team_helpers/key_list"; -import { Setter } from "@/types"; +} from "antd" +import { CopyToClipboard } from "react-copy-to-clipboard" +import TextArea from "antd/es/input/TextArea" +import useKeyList from "../key_team_helpers/key_list" +import { KeyResponse } from "../key_team_helpers/key_list" +import { AllKeysTable } from "../all_keys_table" +import { Team } from "../key_team_helpers/key_list" +import { Setter } from "@/types" + +import NotificationManager from "../molecules/notifications_manager" interface EditKeyModalProps { - visible: boolean; - onCancel: () => void; - token: any; // Assuming TeamType is a type representing your team object - onSubmit: (data: FormData) => void; // Assuming FormData is the type of data to be submitted + visible: boolean + onCancel: () => void + token: any // Assuming TeamType is a type representing your team object + onSubmit: (data: FormData) => void // Assuming FormData is the type of data to be submitted } interface ModelLimitModalProps { - visible: boolean; - onCancel: () => void; - token: KeyResponse; - onSubmit: (updatedMetadata: any) => void; - accessToken: string; + visible: boolean + onCancel: () => void + token: KeyResponse + onSubmit: (updatedMetadata: any) => void + accessToken: string } // Define the props type @@ -94,14 +88,14 @@ interface ViewKeyTableProps { userRole: string | null accessToken: string | null selectedTeam: Team | null - setSelectedTeam: React.Dispatch>; + setSelectedTeam: React.Dispatch> data: KeyResponse[] | null - setData: React.Dispatch>; - teams: Team[] | null; - premiumUser: boolean; - currentOrg: Organization | null; - organizations: Organization[] | null; - setCurrentOrg: React.Dispatch>; + setData: React.Dispatch> + teams: Team[] | null + premiumUser: boolean + currentOrg: Organization | null + organizations: Organization[] | null + setCurrentOrg: React.Dispatch> selectedKeyAlias: string | null setSelectedKeyAlias: Setter createClicked: boolean @@ -109,36 +103,36 @@ interface ViewKeyTableProps { } interface ItemData { - key_alias: string | null; - key_name: string; - spend: string; - max_budget: string | null; - models: string[]; - tpm_limit: string | null; - rpm_limit: string | null; - token: string; - token_id: string | null; - id: number; - team_id: string; - metadata: any; - user_id: string | null; - expires: any; - budget_duration: string | null; - budget_reset_at: string | null; + key_alias: string | null + key_name: string + spend: string + max_budget: string | null + models: string[] + tpm_limit: string | null + rpm_limit: string | null + token: string + token_id: string | null + id: number + team_id: string + metadata: any + user_id: string | null + expires: any + budget_duration: string | null + budget_reset_at: string | null // Add any other properties that exist in the item data } interface ModelLimits { - [key: string]: number; // Index signature allowing string keys + [key: string]: number // Index signature allowing string keys } interface CombinedLimit { - tpm: number; - rpm: number; + tpm: number + rpm: number } interface CombinedLimits { - [key: string]: CombinedLimit; // Index signature allowing string keys + [key: string]: CombinedLimit // Index signature allowing string keys } const ViewKeyTable: React.FC = ({ @@ -159,22 +153,19 @@ const ViewKeyTable: React.FC = ({ createClicked, setAccessToken, }) => { - const [isButtonClicked, setIsButtonClicked] = useState(false); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [keyToDelete, setKeyToDelete] = useState(null); - const [selectedItem, setSelectedItem] = useState(null); - const [spendData, setSpendData] = useState< - { day: string; spend: number }[] | null - >(null); - - // NEW: Declare filter states for team and key alias. - const [teamFilter, setTeamFilter] = useState(selectedTeam?.team_id || ""); + const [isButtonClicked, setIsButtonClicked] = useState(false) + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) + const [keyToDelete, setKeyToDelete] = useState(null) + const [selectedItem, setSelectedItem] = useState(null) + const [spendData, setSpendData] = useState<{ day: string; spend: number }[] | null>(null) + // NEW: Declare filter states for team and key alias. + const [teamFilter, setTeamFilter] = useState(selectedTeam?.team_id || "") // Keep the team filter in sync with the incoming prop. useEffect(() => { - setTeamFilter(selectedTeam?.team_id || ""); - }, [selectedTeam]); + setTeamFilter(selectedTeam?.team_id || "") + }, [selectedTeam]) // Build a memoized filters object for the backend call. @@ -185,46 +176,45 @@ const ViewKeyTable: React.FC = ({ selectedKeyAlias, accessToken: accessToken || "", createClicked, - }); - + }) const handlePageChange = (newPage: number) => { - refresh({ page: newPage }); - }; + refresh({ page: newPage }) + } - const [editModalVisible, setEditModalVisible] = useState(false); - const [infoDialogVisible, setInfoDialogVisible] = useState(false); - const [selectedToken, setSelectedToken] = useState(null); - const [userModels, setUserModels] = useState([]); - const initialKnownTeamIDs: Set = new Set(); - const [modelLimitModalVisible, setModelLimitModalVisible] = useState(false); - const [regenerateDialogVisible, setRegenerateDialogVisible] = useState(false); - const [regeneratedKey, setRegeneratedKey] = useState(null); - const [regenerateFormData, setRegenerateFormData] = useState(null); - const [regenerateForm] = Form.useForm(); - const [newExpiryTime, setNewExpiryTime] = useState(null); + const [editModalVisible, setEditModalVisible] = useState(false) + const [infoDialogVisible, setInfoDialogVisible] = useState(false) + const [selectedToken, setSelectedToken] = useState(null) + const [userModels, setUserModels] = useState([]) + const initialKnownTeamIDs: Set = new Set() + const [modelLimitModalVisible, setModelLimitModalVisible] = useState(false) + const [regenerateDialogVisible, setRegenerateDialogVisible] = useState(false) + const [regeneratedKey, setRegeneratedKey] = useState(null) + const [regenerateFormData, setRegenerateFormData] = useState(null) + const [regenerateForm] = Form.useForm() + const [newExpiryTime, setNewExpiryTime] = useState(null) - const [knownTeamIDs, setKnownTeamIDs] = useState(initialKnownTeamIDs); - const [guardrailsList, setGuardrailsList] = useState([]); + const [knownTeamIDs, setKnownTeamIDs] = useState(initialKnownTeamIDs) + const [guardrailsList, setGuardrailsList] = useState([]) useEffect(() => { const calculateNewExpiryTime = (duration: string | undefined) => { if (!duration) { - return null; + return null } try { - const now = new Date(); - let newExpiry: Date; + const now = new Date() + let newExpiry: Date if (duration.endsWith("s")) { - newExpiry = add(now, { seconds: parseInt(duration) }); + newExpiry = add(now, { seconds: parseInt(duration) }) } else if (duration.endsWith("h")) { - newExpiry = add(now, { hours: parseInt(duration) }); + newExpiry = add(now, { hours: parseInt(duration) }) } else if (duration.endsWith("d")) { - newExpiry = add(now, { days: parseInt(duration) }); + newExpiry = add(now, { days: parseInt(duration) }) } else { - throw new Error("Invalid duration format"); + throw new Error("Invalid duration format") } return newExpiry.toLocaleString("en-US", { @@ -235,143 +225,134 @@ const ViewKeyTable: React.FC = ({ minute: "numeric", second: "numeric", hour12: true, - }); + }) } catch (error) { - return null; + return null } - }; + } - console.log("in calculateNewExpiryTime for selectedToken", selectedToken); + console.log("in calculateNewExpiryTime for selectedToken", selectedToken) // When a new duration is entered if (regenerateFormData?.duration) { - setNewExpiryTime(calculateNewExpiryTime(regenerateFormData.duration)); + setNewExpiryTime(calculateNewExpiryTime(regenerateFormData.duration)) } else { - setNewExpiryTime(null); + setNewExpiryTime(null) } - console.log("calculateNewExpiryTime:", newExpiryTime); - }, [selectedToken, regenerateFormData?.duration]); + console.log("calculateNewExpiryTime:", newExpiryTime) + }, [selectedToken, regenerateFormData?.duration]) useEffect(() => { const fetchUserModels = async () => { try { if (userID === null || userRole === null || accessToken === null) { - return; + return } - const models = await fetchAvailableModelsForTeamOrKey( - userID, - userRole, - accessToken - ); + const models = await fetchAvailableModelsForTeamOrKey(userID, userRole, accessToken) if (models) { - setUserModels(models); + setUserModels(models) } } catch (error) { - console.error("Error fetching user models:", error); + NotificationManager.error({ description: "Error fetching user models" }) } - }; - - fetchUserModels(); - }, [accessToken, userID, userRole]); + } + fetchUserModels() + }, [accessToken, userID, userRole]) useEffect(() => { if (teams) { - const teamIDSet: Set = new Set(); + const teamIDSet: Set = new Set() teams.forEach((team: any, index: number) => { - const team_obj: string = team.team_id; - teamIDSet.add(team_obj); - }); - setKnownTeamIDs(teamIDSet); + const team_obj: string = team.team_id + teamIDSet.add(team_obj) + }) + setKnownTeamIDs(teamIDSet) } - }, [teams]); + }, [teams]) const confirmDelete = async () => { if (keyToDelete == null || data == null) { - return; + return } try { if (!accessToken) return await keyDeleteCall(accessToken, keyToDelete) // Successfully completed the deletion. Update the state to trigger a rerender. - const filteredData = data.filter((item) => item.token !== keyToDelete); - setData(filteredData); + const filteredData = data.filter((item) => item.token !== keyToDelete) + setData(filteredData) } catch (error) { - console.error("Error deleting the key:", error); - // Handle any error situations, such as displaying an error message to the user. + NotificationManager.error({ description: "Error deleting the key" }) } // Close the confirmation modal and reset the keyToDelete - setIsDeleteModalOpen(false); - setKeyToDelete(null); - }; + setIsDeleteModalOpen(false) + setKeyToDelete(null) + } const cancelDelete = () => { // Close the confirmation modal and reset the keyToDelete - setIsDeleteModalOpen(false); - setKeyToDelete(null); - }; + setIsDeleteModalOpen(false) + setKeyToDelete(null) + } const handleRegenerateClick = (token: any) => { - setSelectedToken(token); - setNewExpiryTime(null); + setSelectedToken(token) + setNewExpiryTime(null) regenerateForm.setFieldsValue({ key_alias: token.key_alias, max_budget: token.max_budget, tpm_limit: token.tpm_limit, rpm_limit: token.rpm_limit, duration: token.duration || "", - }); - setRegenerateDialogVisible(true); - }; + }) + setRegenerateDialogVisible(true) + } const handleRegenerateFormChange = (field: string, value: any) => { setRegenerateFormData((prev: any) => ({ ...prev, [field]: value, - })); - }; + })) + } const handleRegenerateKey = async () => { if (!premiumUser) { - message.error( - "Regenerate API Key is an Enterprise feature. Please upgrade to use this feature." - ); - return; + NotificationManager.warning({ + description: "Regenerate API Key is an Enterprise feature. Please upgrade to use this feature.", + }) + return } if (selectedToken == null) { - return; + return } try { const formValues = await regenerateForm.validateFields() - if (!accessToken) return; + if (!accessToken) return const response = await regenerateKeyCall(accessToken, selectedToken.token || selectedToken.token_id, formValues) setRegeneratedKey(response.key) // Update the data state with the new key_name if (data) { const updatedData = data.map((item) => - item.token === selectedToken?.token ? - { ...item, key_name: response.key_name, ...formValues } - : item - ); - setData(updatedData); + item.token === selectedToken?.token ? { ...item, key_name: response.key_name, ...formValues } : item, + ) + setData(updatedData) } - setRegenerateDialogVisible(false); - regenerateForm.resetFields(); - message.success("API Key regenerated successfully"); + setRegenerateDialogVisible(false) + regenerateForm.resetFields() + NotificationManager.success({ description: "API Key regenerated successfully" }) } catch (error) { - console.error("Error regenerating key:", error); - message.error("Failed to regenerate API Key"); + console.error("Error regenerating key:", error) + NotificationManager.error({ description: "Failed to regenerate API Key" }) } - }; - + } return (
@@ -400,18 +381,12 @@ const ViewKeyTable: React.FC = ({ {isDeleteModalOpen && (
- - {viewMode === 'chart' ? ( + {viewMode === "chart" ? (
= ({ layout="vertical" showXAxis={false} showLegend={false} - valueFormatter={(value) => value ? `$${formatNumberWithCommas(value, 2)}` : "No Key Alias"} + valueFormatter={(value) => (value ? `$${formatNumberWithCommas(value, 2)}` : "No Key Alias")} onValueChange={(item) => handleKeyClick(item)} showTooltip={true} customTooltip={(props) => { - const item = props.payload?.[0]?.payload; + const item = props.payload?.[0]?.payload return (
@@ -161,7 +154,7 @@ const TopKeyView: React.FC = ({
- ); + ) }} />
@@ -177,42 +170,45 @@ const TopKeyView: React.FC = ({
)} - {isModalOpen && selectedKey && keyData && ( - console.log('Rendering modal with:', { isModalOpen, selectedKey, keyData }), -
-
- {/* Close button */} - + {isModalOpen && + selectedKey && + keyData && + (console.log("Rendering modal with:", { isModalOpen, selectedKey, keyData }), + ( +
+
+ {/* Close button */} + - {/* Content */} -
- + {/* Content */} +
+ +
-
- )} + ))} - ); -}; + ) +} -export default TopKeyView; +export default TopKeyView diff --git a/ui/litellm-dashboard/src/components/user_dashboard.tsx b/ui/litellm-dashboard/src/components/user_dashboard.tsx index e1b9b2cd811..d78978c495e 100644 --- a/ui/litellm-dashboard/src/components/user_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/user_dashboard.tsx @@ -13,8 +13,8 @@ import { } from "./networking" import { fetchTeams } from "./common_components/fetch_teams" import { Grid, Col, Card, Text, Title } from "@tremor/react" -import CreateKey from "./create_key_button" -import ViewKeyTable from "./view_key_table" +import CreateKey from "./organisms/create_key_button" +import ViewKeyTable from "./templates/view_key_table" import ViewUserSpend from "./view_user_spend" import ViewUserTeam from "./view_user_team" import DashboardTeam from "./dashboard_default_team" diff --git a/ui/litellm-dashboard/src/components/view_logs/index.tsx b/ui/litellm-dashboard/src/components/view_logs/index.tsx index d2041b389fd..7bf49feb7e9 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/index.tsx @@ -1,60 +1,52 @@ -import moment from "moment"; -import { useQuery } from "@tanstack/react-query"; -import { useState, useRef, useEffect, useCallback } from "react"; -import { useQueryClient } from "@tanstack/react-query"; +import moment from "moment" +import { useQuery } from "@tanstack/react-query" +import { useState, useRef, useEffect, useCallback } from "react" +import { useQueryClient } from "@tanstack/react-query" -import { uiSpendLogsCall, keyInfoV1Call, sessionSpendLogsCall, keyListCall, allEndUsersCall } from "../networking"; -import { DataTable } from "./table"; -import { columns, LogEntry } from "./columns"; -import { Row } from "@tanstack/react-table"; -import { prefetchLogDetails } from "./prefetch"; -import { RequestResponsePanel } from './RequestResponsePanel'; -import { ErrorViewer } from './ErrorViewer'; -import { internalUserRoles } from "../../utils/roles"; -import { ConfigInfoMessage } from './ConfigInfoMessage'; -import { Tooltip } from "antd"; -import { KeyResponse, Team } from "../key_team_helpers/key_list"; -import KeyInfoView from "../key_info_view"; -import { SessionView } from './SessionView'; -import { VectorStoreViewer } from './VectorStoreViewer'; -import { GuardrailViewer } from './GuardrailViewer'; -import FilterComponent from "../common_components/filter"; -import { FilterOption } from "../common_components/filter"; -import { useLogFilterLogic } from "./log_filter_logic"; -import { fetchAllKeyAliases } from "../key_team_helpers/filter_helpers"; -import { - Tab, - TabGroup, - TabList, - TabPanels, - TabPanel, - Text, - Switch, -} from "@tremor/react"; -import AuditLogs from "./audit_logs"; -import { getTimeRangeDisplay } from "./logs_utils"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; +import { uiSpendLogsCall, keyInfoV1Call, sessionSpendLogsCall, keyListCall, allEndUsersCall } from "../networking" +import { DataTable } from "./table" +import { columns, LogEntry } from "./columns" +import { Row } from "@tanstack/react-table" +import { prefetchLogDetails } from "./prefetch" +import { RequestResponsePanel } from "./RequestResponsePanel" +import { ErrorViewer } from "./ErrorViewer" +import { internalUserRoles } from "../../utils/roles" +import { ConfigInfoMessage } from "./ConfigInfoMessage" +import { Tooltip } from "antd" +import { KeyResponse, Team } from "../key_team_helpers/key_list" +import KeyInfoView from "../templates/key_info_view" +import { SessionView } from "./SessionView" +import { VectorStoreViewer } from "./VectorStoreViewer" +import { GuardrailViewer } from "./GuardrailViewer" +import FilterComponent from "../common_components/filter" +import { FilterOption } from "../common_components/filter" +import { useLogFilterLogic } from "./log_filter_logic" +import { fetchAllKeyAliases } from "../key_team_helpers/filter_helpers" +import { Tab, TabGroup, TabList, TabPanels, TabPanel, Text, Switch } from "@tremor/react" +import AuditLogs from "./audit_logs" +import { getTimeRangeDisplay } from "./logs_utils" +import { formatNumberWithCommas } from "@/utils/dataUtils" interface SpendLogsTableProps { - accessToken: string | null; - token: string | null; - userRole: string | null; - userID: string | null; - allTeams: Team[]; - premiumUser: boolean; + accessToken: string | null + token: string | null + userRole: string | null + userID: string | null + allTeams: Team[] + premiumUser: boolean } export interface PaginatedResponse { - data: LogEntry[]; - total: number; - page: number; - page_size: number; - total_pages: number; + data: LogEntry[] + total: number + page: number + page_size: number + total_pages: number } interface PrefetchedLog { - messages: any[]; - response: any; + messages: any[] + response: any } export default function SpendLogsTable({ @@ -65,122 +57,100 @@ export default function SpendLogsTable({ allTeams, premiumUser, }: SpendLogsTableProps) { - const [searchTerm, setSearchTerm] = useState(""); - const [showFilters, setShowFilters] = useState(false); - const [showColumnDropdown, setShowColumnDropdown] = useState(false); - const [currentPage, setCurrentPage] = useState(1); - const [pageSize] = useState(50); - const dropdownRef = useRef(null); - const filtersRef = useRef(null); - const quickSelectRef = useRef(null); + const [searchTerm, setSearchTerm] = useState("") + const [showFilters, setShowFilters] = useState(false) + const [showColumnDropdown, setShowColumnDropdown] = useState(false) + const [currentPage, setCurrentPage] = useState(1) + const [pageSize] = useState(50) + const dropdownRef = useRef(null) + const filtersRef = useRef(null) + const quickSelectRef = useRef(null) // New state variables for Start and End Time - const [startTime, setStartTime] = useState( - moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm") - ); - const [endTime, setEndTime] = useState( - moment().format("YYYY-MM-DDTHH:mm") - ); + const [startTime, setStartTime] = useState(moment().subtract(24, "hours").format("YYYY-MM-DDTHH:mm")) + const [endTime, setEndTime] = useState(moment().format("YYYY-MM-DDTHH:mm")) - const [isCustomDate, setIsCustomDate] = useState(false); - const [quickSelectOpen, setQuickSelectOpen] = useState(false); - const [tempTeamId, setTempTeamId] = useState(""); - const [tempKeyHash, setTempKeyHash] = useState(""); - const [selectedTeamId, setSelectedTeamId] = useState(""); - const [selectedKeyHash, setSelectedKeyHash] = useState(""); - const [selectedModel, setSelectedModel] = useState(""); - const [selectedKeyInfo, setSelectedKeyInfo] = useState(null); - const [selectedKeyIdInfoView, setSelectedKeyIdInfoView] = useState(null); - const [selectedStatus, setSelectedStatus] = useState(""); - const [filterByCurrentUser, setFilterByCurrentUser] = useState( - userRole && internalUserRoles.includes(userRole) - ); - const [activeTab, setActiveTab] = useState("request logs"); + const [isCustomDate, setIsCustomDate] = useState(false) + const [quickSelectOpen, setQuickSelectOpen] = useState(false) + const [tempTeamId, setTempTeamId] = useState("") + const [tempKeyHash, setTempKeyHash] = useState("") + const [selectedTeamId, setSelectedTeamId] = useState("") + const [selectedKeyHash, setSelectedKeyHash] = useState("") + const [selectedModel, setSelectedModel] = useState("") + const [selectedKeyInfo, setSelectedKeyInfo] = useState(null) + const [selectedKeyIdInfoView, setSelectedKeyIdInfoView] = useState(null) + const [selectedStatus, setSelectedStatus] = useState("") + const [filterByCurrentUser, setFilterByCurrentUser] = useState(userRole && internalUserRoles.includes(userRole)) + const [activeTab, setActiveTab] = useState("request logs") - const [expandedRequestId, setExpandedRequestId] = useState(null); - const [selectedSessionId, setSelectedSessionId] = useState(null); + const [expandedRequestId, setExpandedRequestId] = useState(null) + const [selectedSessionId, setSelectedSessionId] = useState(null) - const queryClient = useQueryClient(); + const queryClient = useQueryClient() const [isLiveTail, setIsLiveTail] = useState(() => { - const storedValue = sessionStorage.getItem("isLiveTail"); + const storedValue = sessionStorage.getItem("isLiveTail") // default to true if nothing is stored - return storedValue !== null ? JSON.parse(storedValue) : true; - }); + return storedValue !== null ? JSON.parse(storedValue) : true + }) useEffect(() => { - sessionStorage.setItem("isLiveTail", JSON.stringify(isLiveTail)); - }, [isLiveTail]); + sessionStorage.setItem("isLiveTail", JSON.stringify(isLiveTail)) + }, [isLiveTail]) - const [selectedTimeInterval, setSelectedTimeInterval] = useState<{ value: number; unit: string }>({ - value: 24, - unit: "hours" - }); + const [selectedTimeInterval, setSelectedTimeInterval] = useState<{ value: number; unit: string }>({ + value: 24, + unit: "hours", + }) useEffect(() => { const fetchKeyInfo = async () => { if (selectedKeyIdInfoView && accessToken) { - const keyData = await keyInfoV1Call(accessToken, selectedKeyIdInfoView); + const keyData = await keyInfoV1Call(accessToken, selectedKeyIdInfoView) const keyResponse: KeyResponse = { ...keyData["info"], - "token": selectedKeyIdInfoView, - "api_key": selectedKeyIdInfoView, - }; - setSelectedKeyInfo(keyResponse); + token: selectedKeyIdInfoView, + api_key: selectedKeyIdInfoView, + } + setSelectedKeyInfo(keyResponse) } - }; - fetchKeyInfo(); - }, [selectedKeyIdInfoView, accessToken]); + } + fetchKeyInfo() + }, [selectedKeyIdInfoView, accessToken]) // Close dropdown when clicking outside useEffect(() => { function handleClickOutside(event: MouseEvent) { - if ( - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) - ) { - setShowColumnDropdown(false); + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setShowColumnDropdown(false) } - if ( - filtersRef.current && - !filtersRef.current.contains(event.target as Node) - ) { - setShowFilters(false); + if (filtersRef.current && !filtersRef.current.contains(event.target as Node)) { + setShowFilters(false) } - if ( - quickSelectRef.current && - !quickSelectRef.current.contains(event.target as Node) - ) { - setQuickSelectOpen(false); + if (quickSelectRef.current && !quickSelectRef.current.contains(event.target as Node)) { + setQuickSelectOpen(false) } } - document.addEventListener("mousedown", handleClickOutside); - return () => - document.removeEventListener("mousedown", handleClickOutside); - }, []); - + document.addEventListener("mousedown", handleClickOutside) + return () => document.removeEventListener("mousedown", handleClickOutside) + }, []) useEffect(() => { if (userRole && internalUserRoles.includes(userRole)) { - setFilterByCurrentUser(true); + setFilterByCurrentUser(true) } - }, [userRole]); + }, [userRole]) const LiveTailControls = () => { return (
Live Tail - +
- ); - }; + ) + } const logs = useQuery({ queryKey: [ @@ -194,7 +164,7 @@ export default function SpendLogsTable({ selectedKeyHash, filterByCurrentUser ? userID : null, selectedStatus, - selectedModel + selectedModel, ], queryFn: async () => { if (!accessToken || !token || !userRole || !userID) { @@ -204,13 +174,13 @@ export default function SpendLogsTable({ page: 1, page_size: pageSize, total_pages: 0, - }; + } } - const formattedStartTime = moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss"); - const formattedEndTime = isCustomDate + const formattedStartTime = moment(startTime).utc().format("YYYY-MM-DD HH:mm:ss") + const formattedEndTime = isCustomDate ? moment(endTime).utc().format("YYYY-MM-DD HH:mm:ss") - : moment().utc().format("YYYY-MM-DD HH:mm:ss"); + : moment().utc().format("YYYY-MM-DD HH:mm:ss") // Get base response from API const response = await uiSpendLogsCall( @@ -224,45 +194,42 @@ export default function SpendLogsTable({ pageSize, filterByCurrentUser ? userID : undefined, selectedStatus, - selectedModel - ); + selectedModel, + ) // Trigger prefetch for all logs - await prefetchLogDetails( - response.data, - formattedStartTime, - accessToken, - queryClient - ); + await prefetchLogDetails(response.data, formattedStartTime, accessToken, queryClient) // Update logs with prefetched data if available response.data = response.data.map((log: LogEntry) => { - const prefetchedData = queryClient.getQueryData( - ["logDetails", log.request_id, formattedStartTime] - ); + const prefetchedData = queryClient.getQueryData([ + "logDetails", + log.request_id, + formattedStartTime, + ]) if (prefetchedData?.messages && prefetchedData?.response) { - log.messages = prefetchedData.messages; - log.response = prefetchedData.response; - return log; + log.messages = prefetchedData.messages + log.response = prefetchedData.response + return log } - return log; - }); + return log + }) - return response; + return response }, enabled: !!accessToken && !!token && !!userRole && !!userID && activeTab === "request logs", refetchInterval: isLiveTail && currentPage === 1 ? 15000 : false, refetchIntervalInBackground: true, - }); + }) const logsData = logs.data || { data: [], total: 0, page: 1, page_size: pageSize || 10, - total_pages: 1 - }; + total_pages: 1, + } const { filters, @@ -270,7 +237,7 @@ export default function SpendLogsTable({ allTeams: hookAllTeams, allKeyAliases, handleFilterChange, - handleFilterReset + handleFilterReset, } = useLogFilterLogic({ logs: logsData, accessToken, @@ -280,63 +247,55 @@ export default function SpendLogsTable({ isCustomDate, setCurrentPage, userID, - userRole + userRole, }) - const fetchKeyHashForAlias = useCallback(async (keyAlias: string) => { - if (!accessToken) return; - - try { - const response = await keyListCall( - accessToken, - null, - null, - keyAlias, - null, - null, - currentPage, - pageSize - ); + const fetchKeyHashForAlias = useCallback( + async (keyAlias: string) => { + if (!accessToken) return - const selectedKey = response.keys.find( - (key: any) => key.key_alias === keyAlias - ); + try { + const response = await keyListCall(accessToken, null, null, keyAlias, null, null, currentPage, pageSize) - if (selectedKey) { - setSelectedKeyHash(selectedKey.token); + const selectedKey = response.keys.find((key: any) => key.key_alias === keyAlias) + + if (selectedKey) { + setSelectedKeyHash(selectedKey.token) + } + } catch (error) { + console.error("Error fetching key hash for alias:", error) } - } catch (error) { - console.error("Error fetching key hash for alias:", error); - } - }, [accessToken, currentPage, pageSize]); + }, + [accessToken, currentPage, pageSize], + ) // Add this effect to update selected filters when filter changes useEffect(() => { - if(!accessToken) return; + if (!accessToken) return - if (filters['Team ID']) { - setSelectedTeamId(filters['Team ID']); + if (filters["Team ID"]) { + setSelectedTeamId(filters["Team ID"]) } else { - setSelectedTeamId(""); + setSelectedTeamId("") } - setSelectedStatus(filters['Status'] || ""); - setSelectedModel(filters['Model'] || ""); - - if (filters['Key Hash']) { - setSelectedKeyHash(filters['Key Hash']); - } else if (filters['Key Alias']) { - fetchKeyHashForAlias(filters['Key Alias']); + setSelectedStatus(filters["Status"] || "") + setSelectedModel(filters["Model"] || "") + + if (filters["Key Hash"]) { + setSelectedKeyHash(filters["Key Hash"]) + } else if (filters["Key Alias"]) { + fetchKeyHashForAlias(filters["Key Alias"]) } else { - setSelectedKeyHash(""); + setSelectedKeyHash("") } - }, [filters, accessToken, fetchKeyHashForAlias]); + }, [filters, accessToken, fetchKeyHashForAlias]) // Fetch logs for a session if selected const sessionLogs = useQuery({ queryKey: ["sessionLogs", selectedSessionId], queryFn: async () => { - if (!accessToken || !selectedSessionId) return { data: [], total: 0, page: 1, page_size: 50, total_pages: 1 }; - const response = await sessionSpendLogsCall(accessToken, selectedSessionId); + if (!accessToken || !selectedSessionId) return { data: [], total: 0, page: 1, page_size: 50, total_pages: 1 } + const response = await sessionSpendLogsCall(accessToken, selectedSessionId) // If the API returns an array, wrap it in the same shape as PaginatedResponse return { data: response.data || response || [], @@ -344,130 +303,127 @@ export default function SpendLogsTable({ page: 1, page_size: 1000, total_pages: 1, - }; + } }, enabled: !!accessToken && !!selectedSessionId, - }); + }) // Add this effect to preserve expanded state when data refreshes useEffect(() => { if (logs.data?.data && expandedRequestId) { // Check if the expanded request ID still exists in the new data - const stillExists = logs.data.data.some(log => log.request_id === expandedRequestId); + const stillExists = logs.data.data.some((log) => log.request_id === expandedRequestId) if (!stillExists) { // If the request ID no longer exists in the data, clear the expanded state - setExpandedRequestId(null); + setExpandedRequestId(null) } } - }, [logs.data?.data, expandedRequestId]); + }, [logs.data?.data, expandedRequestId]) if (!accessToken || !token || !userRole || !userID) { - return null; + return null } const filteredData = - filteredLogs.data.filter((log) => { - const matchesSearch = - !searchTerm || - log.request_id.includes(searchTerm) || - log.model.includes(searchTerm) || - (log.user && log.user.includes(searchTerm)); - - // No need for additional filtering since we're now handling this in the API call - return matchesSearch; - - }).map(log => ({ - ...log, - duration: (Date.parse(log.endTime) - Date.parse(log.startTime)) / 1000, - onKeyHashClick: (keyHash: string) => setSelectedKeyIdInfoView(keyHash), - onSessionClick: (sessionId: string) => { - if (sessionId) setSelectedSessionId(sessionId); - }, - })) || []; + filteredLogs.data + .filter((log) => { + const matchesSearch = + !searchTerm || + log.request_id.includes(searchTerm) || + log.model.includes(searchTerm) || + (log.user && log.user.includes(searchTerm)) + + // No need for additional filtering since we're now handling this in the API call + return matchesSearch + }) + .map((log) => ({ + ...log, + duration: (Date.parse(log.endTime) - Date.parse(log.startTime)) / 1000, + onKeyHashClick: (keyHash: string) => setSelectedKeyIdInfoView(keyHash), + onSessionClick: (sessionId: string) => { + if (sessionId) setSelectedSessionId(sessionId) + }, + })) || [] // For session logs, add onKeyHashClick/onSessionClick as well const sessionData = - sessionLogs.data?.data?.map(log => ({ + sessionLogs.data?.data?.map((log) => ({ ...log, onKeyHashClick: (keyHash: string) => setSelectedKeyIdInfoView(keyHash), onSessionClick: (sessionId: string) => {}, - })) || []; + })) || [] // Add this function to handle manual refresh const handleRefresh = () => { - logs.refetch(); - }; - - + logs.refetch() + } const handleRowExpand = (requestId: string | null) => { - setExpandedRequestId(requestId); - }; + setExpandedRequestId(requestId) + } const logFilterOptions: FilterOption[] = [ { - name: 'Team ID', - label: 'Team ID', + name: "Team ID", + label: "Team ID", isSearchable: true, searchFn: async (searchText: string) => { - if (!allTeams || allTeams.length === 0) return []; - const filtered = allTeams.filter((team: Team) =>{ - return team.team_id.toLowerCase().includes(searchText.toLowerCase()) || - (team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase())) - }); + if (!allTeams || allTeams.length === 0) return [] + const filtered = allTeams.filter((team: Team) => { + return ( + team.team_id.toLowerCase().includes(searchText.toLowerCase()) || + (team.team_alias && team.team_alias.toLowerCase().includes(searchText.toLowerCase())) + ) + }) return filtered.map((team: Team) => ({ label: `${team.team_alias || team.team_id} (${team.team_id})`, - value: team.team_id - })); - } + value: team.team_id, + })) + }, }, { - name:'Status', - label:'Status', + name: "Status", + label: "Status", isSearchable: false, options: [ - { label: 'Success', value: 'success' }, - { label: 'Failure', value: 'failure' } - ] + { label: "Success", value: "success" }, + { label: "Failure", value: "failure" }, + ], }, { - name: 'Model', - label: 'Model', + name: "Model", + label: "Model", isSearchable: false, }, { - name: 'Key Alias', - label: 'Key Alias', + name: "Key Alias", + label: "Key Alias", isSearchable: true, searchFn: async (searchText: string) => { - if (!accessToken) return []; - const keyAliases = await fetchAllKeyAliases(accessToken); - const filtered = keyAliases.filter(alias => - alias.toLowerCase().includes(searchText.toLowerCase()) - ); - return filtered.map(alias => ({ + if (!accessToken) return [] + const keyAliases = await fetchAllKeyAliases(accessToken) + const filtered = keyAliases.filter((alias) => alias.toLowerCase().includes(searchText.toLowerCase())) + return filtered.map((alias) => ({ label: alias, - value: alias - })); - } + value: alias, + })) + }, }, { - name: 'End User', - label: 'End User', + name: "End User", + label: "End User", isSearchable: true, searchFn: async (searchText: string) => { - if (!accessToken) return []; - const data = await allEndUsersCall(accessToken); - const users = data?.end_users || []; - const filtered = users.filter((u: string) => - u.toLowerCase().includes(searchText.toLowerCase()) - ); - return filtered.map((u: string) => ({ label: u, value: u })); - } + if (!accessToken) return [] + const data = await allEndUsersCall(accessToken) + const users = data?.end_users || [] + const filtered = users.filter((u: string) => u.toLowerCase().includes(searchText.toLowerCase())) + return filtered.map((u: string) => ({ label: u, value: u })) + }, }, { - name: 'Key Hash', - label: 'Key Hash', + name: "Key Hash", + label: "Key Hash", isSearchable: false, }, ] @@ -482,17 +438,17 @@ export default function SpendLogsTable({ onBack={() => setSelectedSessionId(null)} />
- ); + ) } const formatTimeUnit = (value: number, unit: string) => { if (value === 1) { - if (unit === 'minutes') return 'minute'; - if (unit === 'hours') return 'hour'; - if (unit === 'days') return 'day'; + if (unit === "minutes") return "minute" + if (unit === "hours") return "hour" + if (unit === "days") return "day" } - return unit; - }; + return unit + } const quickSelectOptions = [ { label: "Last 15 Minutes", value: 15, unit: "minutes" }, @@ -500,27 +456,20 @@ export default function SpendLogsTable({ { label: "Last 4 Hours", value: 4, unit: "hours" }, { label: "Last 24 Hours", value: 24, unit: "hours" }, { label: "Last 7 Days", value: 7, unit: "days" }, - ]; + ] const selectedOption = quickSelectOptions.find( - (option) => - option.value === selectedTimeInterval.value && option.unit === selectedTimeInterval.unit - ); + (option) => option.value === selectedTimeInterval.value && option.unit === selectedTimeInterval.unit, + ) - const displayLabel = isCustomDate - ? getTimeRangeDisplay(isCustomDate, startTime, endTime) - : selectedOption?.label; + const displayLabel = isCustomDate ? getTimeRangeDisplay(isCustomDate, startTime, endTime) : selectedOption?.label return (
setActiveTab(index === 0 ? "request logs" : "audit logs")}> - - Request Logs - - - Audit Logs - + Request Logs + Audit Logs @@ -542,7 +491,16 @@ export default function SpendLogsTable({
{selectedKeyInfo && selectedKeyIdInfoView && selectedKeyInfo.api_key === selectedKeyIdInfoView ? ( - setSelectedKeyIdInfoView(null)} premiumUser={premiumUser} /> + setSelectedKeyIdInfoView(null)} + premiumUser={premiumUser} + /> ) : selectedSessionId ? (
) : ( <> - -
-
-
-
-
- setSearchTerm(e.target.value)} - /> - - +
+
+
+
+
+ setSearchTerm(e.target.value)} /> - -
- -
-
- - - {quickSelectOpen && ( -
-
- {quickSelectOptions.map((option) => ( - - ))} -
- -
-
- )} -
- - - - +
+ +
+
+ + + {quickSelectOpen && ( +
+
+ {quickSelectOptions.map((option) => ( + + ))} +
+ +
+
+ )} +
+ + + + +
+ + {isCustomDate && ( +
+
+ { + setStartTime(e.target.value) + setCurrentPage(1) + }} + className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+ to +
+ { + setEndTime(e.target.value) + setCurrentPage(1) + }} + className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + /> +
+
+ )}
- {isCustomDate && ( -
-
- { - setStartTime(e.target.value); - setCurrentPage(1); - }} - className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> -
- to -
- { - setEndTime(e.target.value); - setCurrentPage(1); - }} - className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" - /> -
-
- )} -
- -
- - Showing{" "} - {logs.isLoading - ? "..." - : filteredLogs - ? (currentPage - 1) * pageSize + 1 - : 0}{" "} - -{" "} - {logs.isLoading - ? "..." - : filteredLogs - ? Math.min(currentPage * pageSize, filteredLogs.total) - : 0}{" "} - of{" "} - {logs.isLoading - ? "..." - : filteredLogs - ? filteredLogs.total - : 0}{" "} - results - -
+
- Page {logs.isLoading ? "..." : currentPage} of{" "} + Showing {logs.isLoading ? "..." : filteredLogs ? (currentPage - 1) * pageSize + 1 : 0} -{" "} {logs.isLoading ? "..." : filteredLogs - ? filteredLogs.total_pages - : 1} + ? Math.min(currentPage * pageSize, filteredLogs.total) + : 0}{" "} + of {logs.isLoading ? "..." : filteredLogs ? filteredLogs.total : 0} results - - +
+ + Page {logs.isLoading ? "..." : currentPage} of{" "} + {logs.isLoading ? "..." : filteredLogs ? filteredLogs.total_pages : 1} + + + +
-
- {isLiveTail && currentPage === 1 && ( -
-
- - Auto-refreshing every 15 seconds - + {isLiveTail && currentPage === 1 && ( +
+
+ Auto-refreshing every 15 seconds +
+
- -
- )} - true} - /> -
+ )} + true} + /> +
- )} + )} -
- ); + ) } export function RequestViewer({ row }: { row: Row }) { @@ -804,35 +731,38 @@ export function RequestViewer({ row }: { row: Row }) { const formatData = (input: any) => { if (typeof input === "string") { try { - return JSON.parse(input); + return JSON.parse(input) } catch { - return input; + return input } } - return input; - }; + return input + } // New helper function to get raw request const getRawRequest = () => { // First check if proxy_server_request exists in metadata if (row.original?.proxy_server_request) { - return formatData(row.original.proxy_server_request); + return formatData(row.original.proxy_server_request) } // Fall back to messages if proxy_server_request is empty - return formatData(row.original.messages); - }; + return formatData(row.original.messages) + } // Extract error information from metadata if available - const metadata = row.original.metadata || {}; - const hasError = metadata.status === "failure"; - const errorInfo = hasError ? metadata.error_information : null; - + const metadata = row.original.metadata || {} + const hasError = metadata.status === "failure" + const errorInfo = hasError ? metadata.error_information : null + // Check if request/response data is missing - const hasMessages = row.original.messages && - (Array.isArray(row.original.messages) ? row.original.messages.length > 0 : Object.keys(row.original.messages).length > 0); - const hasResponse = row.original.response && Object.keys(formatData(row.original.response)).length > 0; - const missingData = !hasMessages && !hasResponse; - + const hasMessages = + row.original.messages && + (Array.isArray(row.original.messages) + ? row.original.messages.length > 0 + : Object.keys(row.original.messages).length > 0) + const hasResponse = row.original.response && Object.keys(formatData(row.original.response)).length > 0 + const missingData = !hasMessages && !hasResponse + // Format the response with error details if present const formattedResponse = () => { if (hasError && errorInfo) { @@ -840,32 +770,35 @@ export function RequestViewer({ row }: { row: Row }) { error: { message: errorInfo.error_message || "An error occurred", type: errorInfo.error_class || "error", - code: errorInfo.error_code || "unknown", - param: null - } - }; + code: errorInfo.error_code || "unknown", + param: null, + }, + } } - return formatData(row.original.response); - }; - + return formatData(row.original.response) + } + // Extract vector store request metadata if available - const hasVectorStoreData = metadata.vector_store_request_metadata && - Array.isArray(metadata.vector_store_request_metadata) && - metadata.vector_store_request_metadata.length > 0; + const hasVectorStoreData = + metadata.vector_store_request_metadata && + Array.isArray(metadata.vector_store_request_metadata) && + metadata.vector_store_request_metadata.length > 0 // Extract guardrail information from metadata if available - const hasGuardrailData = row.original.metadata && row.original.metadata.guardrail_information; - + const hasGuardrailData = row.original.metadata && row.original.metadata.guardrail_information + // Calculate total masked entities if guardrail data exists const getTotalMaskedEntities = (): number => { if (!hasGuardrailData || !row.original.metadata?.guardrail_information.masked_entity_count) { - return 0; + return 0 } - return Object.values(row.original.metadata.guardrail_information.masked_entity_count) - .reduce((sum: number, count: any) => sum + (typeof count === 'number' ? count : 0), 0); - }; - - const totalMaskedEntities = getTotalMaskedEntities(); + return Object.values(row.original.metadata.guardrail_information.masked_entity_count).reduce( + (sum: number, count: any) => sum + (typeof count === "number" ? count : 0), + 0, + ) + } + + const totalMaskedEntities = getTotalMaskedEntities() return (
@@ -925,15 +858,22 @@ export function RequestViewer({ row }: { row: Row }) {
Tokens: - {row.original.total_tokens} ({row.original.prompt_tokens} prompt tokens + {row.original.completion_tokens} completion tokens) + + {row.original.total_tokens} ({row.original.prompt_tokens} prompt tokens +{" "} + {row.original.completion_tokens} completion tokens) +
Cache Read Tokens: - {formatNumberWithCommas(row.original.metadata?.additional_usage_values?.cache_read_input_tokens || 0)} + + {formatNumberWithCommas(row.original.metadata?.additional_usage_values?.cache_read_input_tokens || 0)} +
Cache Creation Tokens: - {formatNumberWithCommas(row.original.metadata?.additional_usage_values.cache_creation_input_tokens)} + + {formatNumberWithCommas(row.original.metadata?.additional_usage_values.cache_creation_input_tokens)} +
Cost: @@ -943,17 +883,18 @@ export function RequestViewer({ row }: { row: Row }) { Cache Hit: {row.original.cache_hit}
- +
Status: - + {(row.original.metadata?.status || "Success").toLowerCase() !== "failure" ? "Success" : "Failure"} -
Start Time: @@ -986,14 +927,10 @@ export function RequestViewer({ row }: { row: Row }) { /> {/* Guardrail Data - Show only if present */} - {hasGuardrailData && ( - - )} + {hasGuardrailData && } {/* Vector Store Request Data - Show only if present */} - {hasVectorStoreData && ( - - )} + {hasVectorStoreData && } {/* Error Card - Only show for failures */} {hasError && errorInfo && } @@ -1021,14 +958,24 @@ export function RequestViewer({ row }: { row: Row }) {

Metadata

-
)}
- ); -} \ No newline at end of file + ) +}