diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 9fca59315d..d9b5a0be91 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -5,6 +5,7 @@ import { useSearchParams } from "next/navigation"; import { jwtDecode } from "jwt-decode"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { defaultOrg } from "@/components/common_components/default_org"; +import { Team } from "@/components/key_team_helpers/key_list"; import Navbar from "@/components/navbar"; import UserDashboard from "@/components/user_dashboard"; import ModelDashboard from "@/components/model_dashboard"; @@ -76,7 +77,7 @@ export default function CreateKeyPage() { const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] = useState(false); const [userEmail, setUserEmail] = useState(null); - const [teams, setTeams] = useState(null); + const [teams, setTeams] = useState(null); const [keys, setKeys] = useState(null); const [currentOrg, setCurrentOrg] = useState(defaultOrg); const [organizations, setOrganizations] = useState([]); diff --git a/ui/litellm-dashboard/src/components/admins.tsx b/ui/litellm-dashboard/src/components/admins.tsx index d10c9bda42..5d5b8c2ccb 100644 --- a/ui/litellm-dashboard/src/components/admins.tsx +++ b/ui/litellm-dashboard/src/components/admins.tsx @@ -16,6 +16,7 @@ import { } from "antd"; import { CopyToClipboard } from "react-copy-to-clipboard"; import { Select, SelectItem, Subtitle } from "@tremor/react"; +import { Team } from "./key_team_helpers/key_list"; import { Table, TableBody, @@ -38,7 +39,7 @@ import { InvitationLink } from "./onboarding_link"; interface AdminPanelProps { searchParams: any; accessToken: string | null; - setTeams: React.Dispatch>; + setTeams: React.Dispatch>; showSSOBanner: boolean; premiumUser: boolean; } diff --git a/ui/litellm-dashboard/src/components/all_keys_table.tsx b/ui/litellm-dashboard/src/components/all_keys_table.tsx new file mode 100644 index 0000000000..e520f84b36 --- /dev/null +++ b/ui/litellm-dashboard/src/components/all_keys_table.tsx @@ -0,0 +1,340 @@ +"use client"; +import React, { useState } 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"; + +interface AllKeysTableProps { + keys: KeyResponse[]; + 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; + accessToken: string | null; + userID: string | null; + userRole: string | null; +} + +// Define columns similar to our logs table + +function KeyViewer({ row }: { row: Row }) { + return ( +
+
+

Key Details

+
+

+ Key Alias: {row.original.key_alias || "Not Set"} +

+

+ Secret Key: {row.original.key_name} +

+

+ Created:{" "} + {new Date(row.original.created_at).toLocaleString()} +

+

+ Expires:{" "} + {row.original.expires + ? new Date(row.original.expires).toLocaleString() + : "Never"} +

+

+ Spend: {Number(row.original.spend).toFixed(4)} +

+

+ Budget:{" "} + {row.original.max_budget !== null + ? row.original.max_budget + : "Unlimited"} +

+

+ Budget Reset:{" "} + {row.original.budget_reset_at + ? new Date(row.original.budget_reset_at).toLocaleString() + : "Never"} +

+

+ Models:{" "} + {row.original.models && row.original.models.length > 0 + ? row.original.models.join(", ") + : "-"} +

+

+ Rate Limits: TPM:{" "} + {row.original.tpm_limit !== null + ? row.original.tpm_limit + : "Unlimited"} + , RPM:{" "} + {row.original.rpm_limit !== null + ? row.original.rpm_limit + : "Unlimited"} +

+

+ Metadata: +

+
+            {JSON.stringify(row.original.metadata, null, 2)}
+          
+
+
+
+ ); +} + +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 + +
+
+ ); + }; + + +/** + * 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, + isLoading = false, + pagination, + onPageChange, + pageSize = 50, + teams, + selectedTeam, + setSelectedTeam, + accessToken, + userID, + userRole +}: AllKeysTableProps) { + const [selectedKeyId, setSelectedKeyId] = useState(null); + + const columns: ColumnDef[] = [ + { + id: "expander", + header: () => null, + cell: ({ row }) => + row.getCanExpand() ? ( + + ) : null, + }, + { + header: "Key ID", + accessorKey: "token", + cell: (info) => ( +
+ + + +
+ ), + }, + { + header: "Organization", + accessorKey: "organization_id", + cell: (info) => info.getValue() ? info.renderValue() : "Not Set", + }, + { + header: "Team ID", + accessorKey: "team_id", + cell: (info) => info.getValue() ? info.renderValue() : "Not Set", + }, + { + header: "Key Alias", + accessorKey: "key_alias", + cell: (info) => info.getValue() ? info.renderValue() : "Not Set", + }, + { + header: "Secret Key", + accessorKey: "key_name", + cell: (info) => {info.getValue() as string}, + }, + { + header: "Created", + accessorKey: "created_at", + cell: (info) => { + const value = info.getValue(); + return value ? new Date(value as string).toLocaleDateString() : "-"; + }, + }, + { + header: "Expires", + accessorKey: "expires", + cell: (info) => { + const value = info.getValue(); + return value ? new Date(value as string).toLocaleDateString() : "Never"; + }, + }, + { + header: "Spend (USD)", + accessorKey: "spend", + cell: (info) => Number(info.getValue()).toFixed(4), + }, + { + header: "Budget (USD)", + accessorKey: "max_budget", + cell: (info) => + info.getValue() !== null && info.getValue() !== undefined + ? info.getValue() + : "Unlimited", + }, + { + header: "Budget Reset", + accessorKey: "budget_reset_at", + cell: (info) => { + const value = info.getValue(); + return value ? new Date(value as string).toLocaleString() : "Never"; + }, + }, + { + header: "Models", + accessorKey: "models", + cell: (info) => { + const models = info.getValue() as string[]; + return ( +
+ {models && models.length > 0 ? ( + models.map((model, index) => ( + + {model} + + )) + ) : ( + "-" + )} +
+ ); + }, + }, + { + header: "Rate Limits", + cell: ({ row }) => { + const key = row.original; + return ( +
+
TPM: {key.tpm_limit !== null ? key.tpm_limit : "Unlimited"}
+
RPM: {key.rpm_limit !== null ? key.rpm_limit : "Unlimited"}
+
+ ); + }, + }, + ]; + + return ( +
+ {selectedKeyId ? ( + setSelectedKeyId(null)} + keyData={keys.find(k => k.token === selectedKeyId)} + accessToken={accessToken} + userID={userID} + userRole={userRole} + teams={teams} + /> + ) : ( +
+
+ +
+ + 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} + + + + + +
+
+
+ col.id !== 'expander')} + data={keys} + isLoading={isLoading} + getRowCanExpand={() => false} + renderSubComponent={() => <>} + /> +
+ + )} + +
+ ); +} \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/create_key_button.tsx b/ui/litellm-dashboard/src/components/create_key_button.tsx index 95ce38429d..38dc242ccc 100644 --- a/ui/litellm-dashboard/src/components/create_key_button.tsx +++ b/ui/litellm-dashboard/src/components/create_key_button.tsx @@ -30,6 +30,7 @@ import { modelAvailableCall, getGuardrailsList, } from "./networking"; +import { Team } from "./key_team_helpers/key_list"; import { InfoCircleOutlined } from '@ant-design/icons'; import { Tooltip } from 'antd'; @@ -37,11 +38,12 @@ const { Option } = Select; interface CreateKeyProps { userID: string; - team: any | null; + team: Team | null; userRole: string | null; accessToken: string; data: any[] | null; setData: React.Dispatch>; + teams: Team[] | null; } const getPredefinedTags = (data: any[] | null) => { @@ -68,10 +70,34 @@ const getPredefinedTags = (data: any[] | null) => { return uniqueTags; } +export const getTeamModels = (team: Team | null, allAvailableModels: string[]): string[] => { + let tempModelsToPick = []; + + if (team) { + if (team.models.length > 0) { + if (team.models.includes("all-proxy-models")) { + // if the team has all-proxy-models show all available models + tempModelsToPick = allAvailableModels; + } else { + // show team models + tempModelsToPick = team.models; + } + } else { + // show all available models if the team has no models set + tempModelsToPick = allAvailableModels; + } + } else { + // no team set, show all available models + tempModelsToPick = allAvailableModels; + } + + return unfurlWildcardModelsInList(tempModelsToPick, allAvailableModels); +}; const CreateKey: React.FC = ({ userID, team, + teams, userRole, accessToken, data, @@ -86,6 +112,7 @@ const CreateKey: React.FC = ({ const [keyOwner, setKeyOwner] = useState("you"); const [predefinedTags, setPredefinedTags] = useState(getPredefinedTags(data)); const [guardrailsList, setGuardrailsList] = useState([]); + const [selectedCreateKeyTeam, setSelectedCreateKeyTeam] = useState(team); const handleOk = () => { setIsModalVisible(false); @@ -197,30 +224,10 @@ const CreateKey: React.FC = ({ }; useEffect(() => { - let tempModelsToPick = []; - - if (team) { - if (team.models.length > 0) { - if (team.models.includes("all-proxy-models")) { - // if the team has all-proxy-models show all available models - tempModelsToPick = userModels; - } else { - // show team models - tempModelsToPick = team.models; - } - } else { - // show all available models if the team has no models set - tempModelsToPick = userModels; - } - } else { - // no team set, show all available models - tempModelsToPick = userModels; - } - - tempModelsToPick = unfurlWildcardModelsInList(tempModelsToPick, userModels); - - setModelsToPick(tempModelsToPick); - }, [team, userModels]); + const models = getTeamModels(selectedCreateKeyTeam, userModels); + setModelsToPick(models); + form.setFieldValue('models', []); + }, [selectedCreateKeyTeam, userModels]); return (
@@ -278,18 +285,45 @@ const CreateKey: React.FC = ({ + Models{' '} + + + + + } name="models" rules={[{ required: true, message: "Please select a model" }]} help="required" @@ -299,15 +333,8 @@ const CreateKey: React.FC = ({ placeholder="Select models" style={{ width: "100%" }} onChange={(values) => { - // Check if "All Team Models" is selected - const isAllTeamModelsSelected = - values.includes("all-team-models"); - - // If "All Team Models" is selected, deselect all other models - if (isAllTeamModelsSelected) { - const newValues = ["all-team-models"]; - // You can call the form's setFieldsValue method to update the value - form.setFieldsValue({ models: newValues }); + if (values.includes("all-team-models")) { + form.setFieldsValue({ models: ["all-team-models"] }); } }} > diff --git a/ui/litellm-dashboard/src/components/key_edit_view.tsx b/ui/litellm-dashboard/src/components/key_edit_view.tsx new file mode 100644 index 0000000000..770c5c2bb9 --- /dev/null +++ b/ui/litellm-dashboard/src/components/key_edit_view.tsx @@ -0,0 +1,170 @@ +import React, { useState, useEffect } from "react"; +import { Form, Input, InputNumber, Select } from "antd"; +import { Button, TextInput } from "@tremor/react"; +import { KeyResponse } from "./key_team_helpers/key_list"; +import { getTeamModels } from "../components/create_key_button"; +import { modelAvailableCall } from "./networking"; + +interface KeyEditViewProps { + keyData: KeyResponse; + onCancel: () => void; + onSubmit: (values: any) => Promise; + teams?: any[] | null; + accessToken: string | null; + userID: string | null; + userRole: string | null; +} + +// 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); + if (!teams || !keyData.team_id) { + return []; + } + + // Find the team that matches the key's 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 []; +}; + +export function KeyEditView({ + keyData, + onCancel, + onSubmit, + teams, + accessToken, + userID, + userRole }: KeyEditViewProps) { + const [form] = Form.useForm(); + const [userModels, setUserModels] = useState([]); + const team = teams?.find(team => team.team_id === keyData.team_id); + const availableModels = getTeamModels(team, userModels); + + + useEffect(() => { + const fetchUserModels = async () => { + try { + if (accessToken && userID && userRole) { + const model_available = await modelAvailableCall( + accessToken, + userID, + userRole + ); + let available_model_names = model_available["data"].map( + (element: { id: string }) => element.id + ); + console.log("available_model_names:", available_model_names); + setUserModels(available_model_names); + } + } catch (error) { + console.error("Error fetching user models:", error); + } + }; + + fetchUserModels(); + }, []); + + // Convert API budget duration to form format + const getBudgetDuration = (duration: string | null) => { + if (!duration) return null; + const durationMap: Record = { + "24h": "daily", + "7d": "weekly", + "30d": "monthly" + }; + return durationMap[duration] || null; + }; + + // Set initial form values + const initialValues = { + ...keyData, + budget_duration: getBudgetDuration(keyData.budget_duration), + metadata: keyData.metadata ? JSON.stringify(keyData.metadata, null, 2) : "", + guardrails: keyData.metadata?.guardrails || [] + }; + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/key_info_view.tsx b/ui/litellm-dashboard/src/components/key_info_view.tsx new file mode 100644 index 0000000000..1c7826fe85 --- /dev/null +++ b/ui/litellm-dashboard/src/components/key_info_view.tsx @@ -0,0 +1,331 @@ +import React, { useState } from "react"; +import { + Card, + Text, + Button, + Grid, + Col, + Tab, + TabList, + TabGroup, + TabPanel, + TabPanels, + Title, + Badge, + TextInput, + Select as TremorSelect +} from "@tremor/react"; +import { ArrowLeftIcon, TrashIcon } from "@heroicons/react/outline"; +import { keyDeleteCall, keyUpdateCall } from "./networking"; +import { KeyResponse } from "./key_team_helpers/key_list"; +import { Form, Input, InputNumber, message, Select } from "antd"; +import { KeyEditView } from "./key_edit_view"; + +interface KeyInfoViewProps { + keyId: string; + onClose: () => void; + keyData: KeyResponse | undefined; + accessToken: string | null; + userID: string | null; + userRole: string | null; + teams: any[] | null; +} + +export default function KeyInfoView({ keyId, onClose, keyData, accessToken, userID, userRole, teams }: KeyInfoViewProps) { + const [isEditing, setIsEditing] = useState(false); + const [form] = Form.useForm(); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + + if (!keyData) { + return ( +
+ + Key not found +
+ ); + } + + const handleKeyUpdate = async (formValues: Record) => { + try { + if (!accessToken) return; + + const currentKey = formValues.token; + formValues.key = currentKey; + + // Convert metadata back to an object if it exists and is a string + if (formValues.metadata && typeof formValues.metadata === "string") { + try { + const parsedMetadata = JSON.parse(formValues.metadata); + formValues.metadata = { + ...parsedMetadata, + ...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}), + }; + } catch (error) { + console.error("Error parsing metadata JSON:", error); + message.error("Invalid metadata JSON"); + return; + } + } else { + formValues.metadata = { + ...(formValues.metadata || {}), + ...(formValues.guardrails?.length > 0 ? { guardrails: formValues.guardrails } : {}), + }; + } + + // Convert budget_duration to API format + if (formValues.budget_duration) { + const durationMap: Record = { + daily: "24h", + weekly: "7d", + monthly: "30d" + }; + formValues.budget_duration = durationMap[formValues.budget_duration]; + } + + const newKeyValues = await keyUpdateCall(accessToken, formValues); + message.success("Key updated successfully"); + setIsEditing(false); + // Refresh key data here if needed + } catch (error) { + message.error("Failed to update key"); + console.error("Error updating key:", error); + } + }; + + const handleDelete = async () => { + try { + if (!accessToken) return; + await keyDeleteCall(accessToken as string, keyData.token); + message.success("Key deleted successfully"); + onClose(); + } catch (error) { + console.error("Error deleting the key:", error); + message.error("Failed to delete key"); + } + }; + + return ( +
+
+
+ + {keyData.key_alias || "API Key"} + {keyData.token} +
+ +
+ + {/* Delete Confirmation Modal */} + {isDeleteModalOpen && ( +
+
+ + + + +
+
+
+
+

+ Delete Key +

+
+

+ Are you sure you want to delete this key? +

+
+
+
+
+
+ + +
+
+
+
+ )} + + + + Overview + Settings + + + + {/* Overview Panel */} + + + + Spend +
+ ${Number(keyData.spend).toFixed(4)} + of {keyData.max_budget !== null ? `$${keyData.max_budget}` : "Unlimited"} +
+
+ + + Rate Limits +
+ TPM: {keyData.tpm_limit !== null ? keyData.tpm_limit : "Unlimited"} + RPM: {keyData.rpm_limit !== null ? keyData.rpm_limit : "Unlimited"} +
+
+ + + Models +
+ {keyData.models && keyData.models.length > 0 ? ( + keyData.models.map((model, index) => ( + + {model} + + )) + ) : ( + No models specified + )} +
+
+
+
+ + {/* Settings Panel */} + + +
+ Key Settings + {!isEditing && ( + + )} +
+ + {isEditing ? ( + setIsEditing(false)} + onSubmit={handleKeyUpdate} + teams={teams} + accessToken={accessToken} + userID={userID} + userRole={userRole} + /> + ) : ( +
+
+ Key ID + {keyData.token} +
+ +
+ Key Alias + {keyData.key_alias || "Not Set"} +
+ +
+ Secret Key + {keyData.key_name} +
+ +
+ Team ID + {keyData.team_id || "Not Set"} +
+ +
+ Organization + {keyData.organization_id || "Not Set"} +
+ +
+ Created + {new Date(keyData.created_at).toLocaleString()} +
+ +
+ Expires + {keyData.expires ? new Date(keyData.expires).toLocaleString() : "Never"} +
+ +
+ Spend + ${Number(keyData.spend).toFixed(4)} USD +
+ +
+ Budget + {keyData.max_budget !== null ? `$${keyData.max_budget} USD` : "Unlimited"} +
+ +
+ Models +
+ {keyData.models && keyData.models.length > 0 ? ( + keyData.models.map((model, index) => ( + + {model} + + )) + ) : ( + No models specified + )} +
+
+ +
+ Rate Limits + TPM: {keyData.tpm_limit !== null ? keyData.tpm_limit : "Unlimited"} + RPM: {keyData.rpm_limit !== null ? keyData.rpm_limit : "Unlimited"} +
+ +
+ Metadata +
+                      {JSON.stringify(keyData.metadata, null, 2)}
+                    
+
+
+ )} +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx index 8bb7cf98b3..0f36c86dc5 100644 --- a/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx +++ b/ui/litellm-dashboard/src/components/key_team_helpers/key_list.tsx @@ -1,68 +1,76 @@ import { useState, useEffect } from 'react'; import { keyListCall, Organization } from '../networking'; -interface Team { -team_id: string; -team_alias: string; + +export interface Team { + team_id: string; + team_alias: string; + models: string[]; + max_budget: number | null; + budget_duration: string | null; + tpm_limit: number | null; + rpm_limit: number | null; + organization_id: string; + created_at: string; } export interface KeyResponse { -token: string; -key_name: string; -key_alias: string; -spend: number; -max_budget: number; -expires: string; -models: string[]; -aliases: Record; -config: Record; -user_id: string; -team_id: string | null; -max_parallel_requests: number; -metadata: Record; -tpm_limit: number; -rpm_limit: number; -budget_duration: string; -budget_reset_at: string; -allowed_cache_controls: string[]; -permissions: Record; -model_spend: Record; -model_max_budget: Record; -soft_budget_cooldown: boolean; -blocked: boolean; -litellm_budget_table: Record; -org_id: string | null; -created_at: string; -updated_at: string; -team_spend: number; -team_alias: string; -team_tpm_limit: number; -team_rpm_limit: number; -team_max_budget: number; -team_models: string[]; -team_blocked: boolean; -soft_budget: number; -team_model_aliases: Record; -team_member_spend: number; -team_member?: { + token: string; + key_name: string; + key_alias: string; + spend: number; + max_budget: number; + expires: string; + models: string[]; + aliases: Record; + config: Record; user_id: string; + team_id: string | null; + max_parallel_requests: number; + metadata: Record; + tpm_limit: number; + rpm_limit: number; + budget_duration: string; + budget_reset_at: string; + allowed_cache_controls: string[]; + permissions: Record; + model_spend: Record; + model_max_budget: Record; + soft_budget_cooldown: boolean; + blocked: boolean; + litellm_budget_table: Record; + organization_id: string | null; + created_at: string; + updated_at: string; + team_spend: number; + team_alias: string; + team_tpm_limit: number; + team_rpm_limit: number; + team_max_budget: number; + team_models: string[]; + team_blocked: boolean; + soft_budget: number; + team_model_aliases: Record; + team_member_spend: number; + team_member?: { + user_id: string; + user_email: string; + role: 'admin' | 'user'; + }; + team_metadata: Record; + end_user_id: string; + end_user_tpm_limit: number; + end_user_rpm_limit: number; + end_user_max_budget: number; + last_refreshed_at: number; + api_key: string; + user_role: 'proxy_admin' | 'user'; + allowed_model_region?: 'eu' | 'us' | string; + parent_otel_span?: string; + rpm_limit_per_model: Record; + tpm_limit_per_model: Record; + user_tpm_limit: number; + user_rpm_limit: number; user_email: string; - role: 'admin' | 'user'; -}; -team_metadata: Record; -end_user_id: string; -end_user_tpm_limit: number; -end_user_rpm_limit: number; -end_user_max_budget: number; -last_refreshed_at: number; -api_key: string; -user_role: 'proxy_admin' | 'user'; -allowed_model_region?: 'eu' | 'us' | string; -parent_otel_span?: string; -rpm_limit_per_model: Record; -tpm_limit_per_model: Record; -user_tpm_limit: number; -user_rpm_limit: number; -user_email: string; } interface KeyListResponse { @@ -76,6 +84,7 @@ interface UseKeyListProps { selectedTeam?: Team; currentOrg: Organization | null; accessToken: string; +currentPage?: number; } interface PaginationData { @@ -92,7 +101,13 @@ pagination: PaginationData; refresh: (params?: Record) => Promise; } -const useKeyList = ({ selectedTeam, currentOrg, accessToken }: UseKeyListProps): UseKeyListReturn => { +const useKeyList = ({ + selectedTeam, + currentOrg, + accessToken, + currentPage = 1, + +}: UseKeyListProps): UseKeyListReturn => { const [keyData, setKeyData] = useState({ keys: [], total_count: 0, @@ -105,20 +120,25 @@ const useKeyList = ({ selectedTeam, currentOrg, accessToken }: UseKeyListProps): const fetchKeys = async (params: Record = {}): Promise => { try { console.log("calling fetchKeys"); - if (!currentOrg || !selectedTeam || !accessToken) { + if (!currentOrg || !accessToken) { console.log("currentOrg", currentOrg); - console.log("selectedTeam", selectedTeam); console.log("accessToken", accessToken); - return + return; } setIsLoading(true); - const data = await keyListCall(accessToken, currentOrg.organization_id, selectedTeam.team_id); + const data = await keyListCall( + accessToken, + currentOrg.organization_id, + selectedTeam?.team_id || "", + params.page as number || 1, + 50 + ); console.log("data", data); setKeyData(data); setError(null); } catch (err) { - setError(err instanceof Error ? err : new Error('An error occurred')); + setError(err instanceof Error ? err : new Error("An error occurred")); } finally { setIsLoading(false); } @@ -126,7 +146,8 @@ const useKeyList = ({ selectedTeam, currentOrg, accessToken }: UseKeyListProps): useEffect(() => { fetchKeys(); - }, [selectedTeam, currentOrg]); + console.log("selectedTeam", selectedTeam, "currentOrg", currentOrg, "accessToken", accessToken); + }, [selectedTeam, currentOrg, accessToken]); return { keys: keyData.keys, diff --git a/ui/litellm-dashboard/src/components/networking.tsx b/ui/litellm-dashboard/src/components/networking.tsx index 0a529ecf6c..d4ecd3e0e1 100644 --- a/ui/litellm-dashboard/src/components/networking.tsx +++ b/ui/litellm-dashboard/src/components/networking.tsx @@ -2079,6 +2079,8 @@ export const keyListCall = async ( accessToken: String, organizationID: string | null, teamID: string | null, + page: number, + pageSize: number ) => { /** * Get all available teams on proxy @@ -2096,6 +2098,14 @@ export const keyListCall = async ( queryParams.append('organization_id', organizationID.toString()); } + if (page) { + queryParams.append('page', page.toString()); + } + + if (pageSize) { + queryParams.append('size', pageSize.toString()); + } + queryParams.append('return_full_object', 'true'); const queryString = queryParams.toString(); diff --git a/ui/litellm-dashboard/src/components/organizations.tsx b/ui/litellm-dashboard/src/components/organizations.tsx index 3a60d4392f..87ec9d0c2a 100644 --- a/ui/litellm-dashboard/src/components/organizations.tsx +++ b/ui/litellm-dashboard/src/components/organizations.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from "react"; import { organizationListCall, organizationMemberAddCall, Member, modelAvailableCall } from "./networking"; +import { Team } from "./key_team_helpers/key_list"; import { Col, Grid, @@ -15,10 +16,10 @@ if (isLocal != true) { console.log = function() {}; } interface TeamProps { - teams: any[] | null; + teams: Team[] | null; searchParams: any; accessToken: string | null; - setTeams: React.Dispatch>; + setTeams: React.Dispatch>; userID: string | null; userRole: string | null; premiumUser: boolean; diff --git a/ui/litellm-dashboard/src/components/teams.tsx b/ui/litellm-dashboard/src/components/teams.tsx index 7cb7d440a4..03879a1154 100644 --- a/ui/litellm-dashboard/src/components/teams.tsx +++ b/ui/litellm-dashboard/src/components/teams.tsx @@ -53,16 +53,17 @@ import { } from "@tremor/react"; import { CogIcon } from "@heroicons/react/outline"; import AvailableTeamsPanel from "@/components/team/available_teams"; +import type { Team } from "./key_team_helpers/key_list"; const isLocal = process.env.NODE_ENV === "development"; const proxyBaseUrl = isLocal ? "http://localhost:4000" : null; if (isLocal != true) { console.log = function() {}; } interface TeamProps { - teams: any[] | null; + teams: Team[] | null; searchParams: any; accessToken: string | null; - setTeams: React.Dispatch>; + setTeams: React.Dispatch>; userID: string | null; userRole: string | null; currentOrg: Organization | null; diff --git a/ui/litellm-dashboard/src/components/user_dashboard.tsx b/ui/litellm-dashboard/src/components/user_dashboard.tsx index 5468fab9cd..9ae5f1ecf7 100644 --- a/ui/litellm-dashboard/src/components/user_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/user_dashboard.tsx @@ -19,6 +19,7 @@ import ViewUserTeam from "./view_user_team"; import DashboardTeam from "./dashboard_default_team"; import Onboarding from "../app/onboarding/page"; import { useSearchParams, useRouter } from "next/navigation"; +import { Team } from "./key_team_helpers/key_list"; import { jwtDecode } from "jwt-decode"; import { Typography } from "antd"; const isLocal = process.env.NODE_ENV === "development"; @@ -56,11 +57,11 @@ interface UserDashboardProps { userID: string | null; userRole: string | null; userEmail: string | null; - teams: any[] | null; + teams: Team[] | null; keys: any[] | null; setUserRole: React.Dispatch>; setUserEmail: React.Dispatch>; - setTeams: React.Dispatch>; + setTeams: React.Dispatch>; setKeys: React.Dispatch>; setOrganizations: React.Dispatch>; premiumUser: boolean; @@ -108,9 +109,7 @@ const UserDashboard: React.FC = ({ team_alias: "Default Team", team_id: null, }; - const [selectedTeam, setSelectedTeam] = useState( - teams ? teams[0] : defaultTeam - ); + const [selectedTeam, setSelectedTeam] = useState(null); // check if window is not undefined if (typeof window !== "undefined") { window.addEventListener("beforeunload", function () { @@ -213,16 +212,6 @@ const UserDashboard: React.FC = ({ } - const teamsArray = [...response["teams"]]; - if (teamsArray.length > 0) { - console.log(`response['teams']: ${JSON.stringify(teamsArray)}`); - setSelectedTeam(teamsArray[0]); - } else { - setSelectedTeam(defaultTeam); - - } - - sessionStorage.setItem( "userData" + userID, JSON.stringify(response["keys"]) @@ -348,20 +337,16 @@ const UserDashboard: React.FC = ({ return (
- - + - = ({ userRole={userRole} accessToken={accessToken} selectedTeam={selectedTeam ? selectedTeam : null} + setSelectedTeam={setSelectedTeam} data={keys} setData={setKeys} premiumUser={premiumUser} teams={teams} currentOrg={currentOrg} /> - -
diff --git a/ui/litellm-dashboard/src/components/view_key_table.tsx b/ui/litellm-dashboard/src/components/view_key_table.tsx index 012782b8bf..005a08c5f5 100644 --- a/ui/litellm-dashboard/src/components/view_key_table.tsx +++ b/ui/litellm-dashboard/src/components/view_key_table.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useState, useMemo } from "react"; import { keyDeleteCall, modelAvailableCall, @@ -42,6 +42,8 @@ import { BarChart, TextInput, Textarea, + Select, + SelectItem, } from "@tremor/react"; import { InfoCircleOutlined } from "@ant-design/icons"; import { @@ -49,8 +51,6 @@ import { getModelDisplayName, } from "./key_team_helpers/fetch_available_models_team_key"; import { - Select as Select3, - SelectItem, MultiSelect, MultiSelectItem, } from "@tremor/react"; @@ -62,16 +62,15 @@ import { Select as Select2, InputNumber, message, - Select, 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"; -const { Option } = Select; +import { AllKeysTable } from "./all_keys_table"; +import { Team } from "./key_team_helpers/key_list"; const isLocal = process.env.NODE_ENV === "development"; const proxyBaseUrl = isLocal ? "http://localhost:4000" : null; @@ -100,9 +99,10 @@ interface ViewKeyTableProps { userRole: string | null; accessToken: string; selectedTeam: any | null; + setSelectedTeam: React.Dispatch>; data: any[] | null; setData: React.Dispatch>; - teams: any[] | null; + teams: Team[] | null; premiumUser: boolean; currentOrg: Organization | null; } @@ -145,6 +145,7 @@ const ViewKeyTable: React.FC = ({ userRole, accessToken, selectedTeam, + setSelectedTeam, data, setData, teams, @@ -159,13 +160,43 @@ const ViewKeyTable: React.FC = ({ { day: string; spend: number }[] | null >(null); - const { keys, isLoading, error, refresh } = useKeyList({ + // NEW: Declare filter states for team and key alias. + const [teamFilter, setTeamFilter] = useState(selectedTeam?.team_id || ""); + const [keyAliasFilter, setKeyAliasFilter] = useState(""); + + // Keep the team filter in sync with the incoming prop. + useEffect(() => { + setTeamFilter(selectedTeam?.team_id || ""); + }, [selectedTeam]); + + // Build a memoized filters object for the backend call. + const filters = useMemo( + () => { + const f: { team_id?: string; key_alias?: string } = {}; + + if (teamFilter) { + f.team_id = teamFilter; + } + + if (keyAliasFilter) { + f.key_alias = keyAliasFilter; + } + + return f; + }, + [teamFilter, keyAliasFilter] + ); + + // Pass filters into the hook so the API call includes these query parameters. + const { keys, isLoading, error, pagination, refresh } = useKeyList({ selectedTeam, currentOrg, - accessToken + accessToken, }); - console.log("keys", keys); + const handlePageChange = (newPage: number) => { + refresh({ page: newPage }); + }; const [editModalVisible, setEditModalVisible] = useState(false); const [infoDialogVisible, setInfoDialogVisible] = useState(false); @@ -298,311 +329,7 @@ const ViewKeyTable: React.FC = ({ setKnownTeamIDs(teamIDSet); } }, [teams]); - const EditKeyModal: React.FC = ({ - visible, - onCancel, - token, - onSubmit, - }) => { - const [form] = Form.useForm(); - const [keyTeam, setKeyTeam] = useState(selectedTeam); - const [errorModels, setErrorModels] = useState([]); - const [errorBudget, setErrorBudget] = useState(false); - const [guardrailsList, setGuardrailsList] = useState([]); - useEffect(() => { - const fetchGuardrails = async () => { - try { - const response = await getGuardrailsList(accessToken); - const guardrailNames = response.guardrails.map( - (g: { guardrail_name: string }) => g.guardrail_name - ); - setGuardrailsList(guardrailNames); - } catch (error) { - console.error("Failed to fetch guardrails:", error); - } - }; - - fetchGuardrails(); - }, [accessToken]); - - let metadataString = ""; - try { - // Create a copy of metadata without guardrails for display - const displayMetadata = { ...token.metadata }; - delete displayMetadata.guardrails; - metadataString = JSON.stringify(displayMetadata, null, 2); - } catch (error) { - console.error("Error stringifying metadata:", error); - metadataString = ""; - } - - // Extract existing guardrails from metadata - let existingGuardrails: string[] = []; - try { - existingGuardrails = token.metadata?.guardrails || []; - } catch (error) { - console.error("Error extracting guardrails:", error); - } - - const initialValues = - token ? - { - ...token, - budget_duration: token.budget_duration, - metadata: metadataString, - guardrails: existingGuardrails, - } - : { metadata: metadataString, guardrails: [] }; - - const handleOk = () => { - form - .validateFields() - .then((values) => { - // const updatedValues = {...values, team_id: team.team_id}; - // onSubmit(updatedValues); - form.resetFields(); - }) - .catch((error) => { - console.error("Validation failed:", error); - }); - }; - - return ( - -
- <> - - - - - { - if (keyTeam.team_alias === "Default Team") { - return Promise.resolve(); - } - - const errorModels = value.filter( - (model: string) => - !keyTeam.models.includes(model) && - model !== "all-team-models" && - model !== "all-proxy-models" && - !keyTeam.models.includes("all-proxy-models") - ); - console.log(`errorModels: ${errorModels}`); - if (errorModels.length > 0) { - return Promise.reject( - `Some models are not part of the new team's models - ${errorModels} Team models: ${keyTeam.models}` - ); - } else { - return Promise.resolve(); - } - }, - }, - ]} - > - - - { - if ( - value && - keyTeam && - keyTeam.max_budget !== null && - value > keyTeam.max_budget - ) { - console.log(`keyTeam.max_budget: ${keyTeam.max_budget}`); - throw new Error( - `Budget cannot exceed team max budget: $${keyTeam.max_budget}` - ); - } - }, - }, - ]} - > - - - - - - - - - - - {teams?.map((team_obj, index) => ( - setKeyTeam(team_obj)} - > - {team_obj.team_alias} - - ))} - - - - { - if ( - value && - keyTeam && - keyTeam.tpm_limit !== null && - value > keyTeam.tpm_limit - ) { - console.log(`keyTeam.tpm_limit: ${keyTeam.tpm_limit}`); - throw new Error( - `tpm_limit cannot exceed team max tpm_limit: $${keyTeam.tpm_limit}` - ); - } - }, - }, - ]} - > - - - { - if ( - value && - keyTeam && - keyTeam.rpm_limit !== null && - value > keyTeam.rpm_limit - ) { - console.log(`keyTeam.rpm_limit: ${keyTeam.rpm_limit}`); - throw new Error( - `rpm_limit cannot exceed team max rpm_limit: $${keyTeam.rpm_limit}` - ); - } - }, - }, - ]} - > - - - - Guardrails{" "} - - e.stopPropagation()} - > - - - - - } - name="guardrails" - className="mt-8" - help="Select existing guardrails or enter new ones" - > -