diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index 5e6965516fa..98a8ff7fe7a 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -24,6 +24,8 @@ "src/components/molecules/notifications_manager.tsx", "src/components/molecules/message_manager.tsx", "src/components/ToolPolicies/PolicySelect.tsx", + "src/components/ToolDetail.tsx", + "src/components/ToolPolicies.tsx", "src/components/GuardrailsMonitor/GuardrailConfig.tsx", "src/components/GuardrailsMonitor/GuardrailDetail.tsx", "src/components/GuardrailsMonitor/LogViewer.tsx", diff --git a/ui/litellm-dashboard/src/components/ToolDetail.tsx b/ui/litellm-dashboard/src/components/ToolDetail.tsx index 55adb593fff..bd7071fa19f 100644 --- a/ui/litellm-dashboard/src/components/ToolDetail.tsx +++ b/ui/litellm-dashboard/src/components/ToolDetail.tsx @@ -2,8 +2,16 @@ import { ArrowLeft as ArrowLeftOutlined, History as HistoryOutlined, Wrench as ToolOutlined } from "lucide-react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { Button, Select, Spin } from "antd"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Skeleton } from "@/components/ui/skeleton"; +import React, { useCallback, useMemo, useState } from "react"; import TeamDropdown from "@/components/common_components/team_dropdown"; import { LogViewer } from "@/components/GuardrailsMonitor/LogViewer"; import type { LogEntry } from "@/components/GuardrailsMonitor/mockData"; @@ -16,7 +24,6 @@ import { keyListCall, teamListCall, updateToolPolicy, - type ToolPolicyOption, type ToolPolicyOverrideRow, } from "@/components/networking"; import type { Team } from "@/components/key_team_helpers/key_list"; @@ -210,7 +217,7 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { if (detailLoading && !detail) { return (
- +
); } @@ -218,9 +225,11 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { if (detailError && !detail) { return (
- + {/* eslint-disable-next-line litellm-ui/no-raw-tailwind-colors */}

Failed to load tool details.

); @@ -243,11 +252,11 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) {
@@ -341,9 +350,9 @@ export function ToolDetail({ toolName, onBack, accessToken }: ToolDetailProps) { {!ov.team_id && !ov.key_hash ? "—" : ""}
@@ -505,26 +529,26 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec
- + {tool.team_id ?? "-"} - + - + {tool.key_hash ?? "-"} - + - + {tool.key_alias ?? "-"} - + - + {tool.user_agent ?? "-"} - + )) @@ -559,6 +583,7 @@ export const ToolPolicies: React.FC = ({ accessToken, onSelec + ); }; diff --git a/ui/litellm-dashboard/src/components/organizations.tsx b/ui/litellm-dashboard/src/components/organizations.tsx index 8038893be58..ddf6fe15e44 100644 --- a/ui/litellm-dashboard/src/components/organizations.tsx +++ b/ui/litellm-dashboard/src/components/organizations.tsx @@ -1,30 +1,52 @@ import OrganizationFilters, { FilterState } from "@/app/(dashboard)/organizations/OrganizationFilters"; import { Info as InfoCircleOutlined } from "lucide-react"; -import { ChevronDown as ChevronDownIcon, ChevronRight as ChevronRightIcon, RefreshCcw as RefreshIcon } from "lucide-react"; +import { + ChevronDown as ChevronDownIcon, + ChevronRight as ChevronRightIcon, + RefreshCcw as RefreshIcon, +} from "lucide-react"; // eslint-disable-next-line litellm-ui/no-banned-ui-imports import { - Badge, - Button, - Card, - Col, - Grid, - Icon, - Tab, - TabGroup, Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, - TabList, - TabPanel, - TabPanels, - Text, - TextInput, } from "@tremor/react"; -import { Form, Input, Modal, Select as Select2, Tooltip } from "antd"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger, +} from "@/components/ui/tabs"; +import { Textarea } from "@/components/ui/textarea"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; import React, { useState } from "react"; +import { Controller, FormProvider, useForm } from "react-hook-form"; import { formatNumberWithCommas } from "../utils/dataUtils"; import DeleteResourceModal from "./common_components/DeleteResourceModal"; import TableIconActionButton from "./common_components/IconActionButton/TableIconActionButtons/TableIconActionButton"; @@ -50,6 +72,18 @@ interface OrganizationsTableProps { premiumUser: boolean; } +interface OrgFormValues { + organization_alias: string; + models: string[]; + max_budget: number | null; + budget_duration: string; + tpm_limit: number | null; + rpm_limit: number | null; + allowed_vector_store_ids: string[]; + allowed_mcp_servers_and_groups: { servers?: string[]; accessGroups?: string[] } | null; + metadata: string; +} + export const fetchOrganizations = async ( accessToken: string, setOrganizations: (organizations: Organization[]) => void, @@ -72,13 +106,28 @@ const OrganizationsTable: React.FC = ({ setOrganizations, premiumUser, }) => { + void userModels; + void currentOrg; + void guardrailsList; const [selectedOrgId, setSelectedOrgId] = useState(null); const [editOrg, setEditOrg] = useState(false); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [orgToDelete, setOrgToDelete] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const [isOrgModalVisible, setIsOrgModalVisible] = useState(false); - const [form] = Form.useForm(); + const form = useForm({ + defaultValues: { + organization_alias: "", + models: [], + max_budget: null, + budget_duration: "", + tpm_limit: null, + rpm_limit: null, + allowed_vector_store_ids: [], + allowed_mcp_servers_and_groups: null, + metadata: "", + }, + }); const [expandedAccordions, setExpandedAccordions] = useState>({}); const [showFilters, setShowFilters] = useState(false); const [filters, setFilters] = useState({ @@ -91,7 +140,6 @@ const OrganizationsTable: React.FC = ({ const handleFilterChange = (key: keyof FilterState, value: string) => { const newFilters = { ...filters, [key]: value }; setFilters(newFilters); - // Call organizationListCall with the new filters if (accessToken) { organizationListCall(accessToken, newFilters.org_id || null, newFilters.org_alias || null) .then((response) => { @@ -112,7 +160,6 @@ const OrganizationsTable: React.FC = ({ sort_by: "created_at", sort_order: "desc", }); - // Reset organizations list if (accessToken) { organizationListCall(accessToken, null, null) .then((response) => { @@ -128,7 +175,6 @@ const OrganizationsTable: React.FC = ({ const handleDelete = (orgId: string | null) => { if (!orgId) return; - setOrgToDelete(orgId); setIsDeleteModalOpen(true); }; @@ -143,7 +189,6 @@ const OrganizationsTable: React.FC = ({ setIsDeleteModalOpen(false); setOrgToDelete(null); - // Refresh organizations list await fetchOrganizations(accessToken, setOrganizations, filters.org_id || null, filters.org_alias || null); } catch (error) { console.error("Error deleting organization:", error); @@ -157,69 +202,68 @@ const OrganizationsTable: React.FC = ({ setOrgToDelete(null); }; - const handleCreate = async (values: any) => { + const handleCreate = form.handleSubmit(async (values) => { try { if (!accessToken) return; - console.log(`values in organizations new create call: ${JSON.stringify(values)}`); + const payload: Record = { ...values }; - // Transform allowed_vector_store_ids and allowed_mcp_servers_and_groups into object_permission if ( (values.allowed_vector_store_ids && values.allowed_vector_store_ids.length > 0) || (values.allowed_mcp_servers_and_groups && - (values.allowed_mcp_servers_and_groups.servers?.length > 0 || - values.allowed_mcp_servers_and_groups.accessGroups?.length > 0)) + ((values.allowed_mcp_servers_and_groups.servers?.length ?? 0) > 0 || + (values.allowed_mcp_servers_and_groups.accessGroups?.length ?? 0) > 0)) ) { - values.object_permission = {}; + payload.object_permission = {}; if (values.allowed_vector_store_ids && values.allowed_vector_store_ids.length > 0) { - values.object_permission.vector_stores = values.allowed_vector_store_ids; - delete values.allowed_vector_store_ids; + payload.object_permission.vector_stores = values.allowed_vector_store_ids; + delete payload.allowed_vector_store_ids; } if (values.allowed_mcp_servers_and_groups) { - if (values.allowed_mcp_servers_and_groups.servers?.length > 0) { - values.object_permission.mcp_servers = values.allowed_mcp_servers_and_groups.servers; + if ((values.allowed_mcp_servers_and_groups.servers?.length ?? 0) > 0) { + payload.object_permission.mcp_servers = values.allowed_mcp_servers_and_groups.servers; } - if (values.allowed_mcp_servers_and_groups.accessGroups?.length > 0) { - values.object_permission.mcp_access_groups = values.allowed_mcp_servers_and_groups.accessGroups; + if ((values.allowed_mcp_servers_and_groups.accessGroups?.length ?? 0) > 0) { + payload.object_permission.mcp_access_groups = values.allowed_mcp_servers_and_groups.accessGroups; } - delete values.allowed_mcp_servers_and_groups; + delete payload.allowed_mcp_servers_and_groups; } } - await organizationCreateCall(accessToken, values); + await organizationCreateCall(accessToken, payload); NotificationsManager.success("Organization created successfully"); setIsOrgModalVisible(false); - form.resetFields(); - // Refresh organizations list + form.reset(); fetchOrganizations(accessToken, setOrganizations, filters.org_id || null, filters.org_alias || null); } catch (error) { console.error("Error creating organization:", error); } - }; + }); const handleCancel = () => { setIsOrgModalVisible(false); - form.resetFields(); + form.reset(); }; if (!premiumUser) { return (
- +

This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key{" "} here . - +

); } return ( +
- - +
+
{(userRole === "Admin" || userRole === "Org Admin") && (
- - - - Click on “Organization ID” to view organization details. - - - -
-
- -
+ + +

+ Click on “Organization ID” to view organization details. +

+
+
+ +
+
+
- - - - Organization ID - Organization Name - Created - Spend (USD) - Budget (USD) - Models - TPM / RPM Limits - Info - Actions - - + +
+ + + Organization ID + Organization Name + Created + Spend (USD) + Budget (USD) + Models + TPM / RPM Limits + Info + Actions + + - - {organizations && organizations.length > 0 - ? organizations - .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) - .map((org: Organization) => ( - - -
- + + {organizations && organizations.length > 0 + ? organizations + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) + .map((org: Organization) => ( + + +
+ + - -
-
- {org.organization_alias} - - {org.created_at ? new Date(org.created_at).toLocaleDateString() : "N/A"} - - {formatNumberWithCommas(org.spend, 4)} - - {org.litellm_budget_table?.max_budget !== null && - org.litellm_budget_table?.max_budget !== undefined - ? org.litellm_budget_table?.max_budget - : "No limit"} - - 3 ? "px-0" : ""} - > -
- {Array.isArray(org.models) ? ( -
- {org.models.length === 0 ? ( - - All Proxy Models - - ) : ( - <> -
- {org.models.length > 3 && ( -
- { - setExpandedAccordions((prev) => ({ - ...prev, - [org.organization_id || ""]: - !prev[org.organization_id || ""], - })); - }} - /> -
+ + {org.organization_id} + +
+ + {org.organization_alias} + + {org.created_at ? new Date(org.created_at).toLocaleDateString() : "N/A"} + + {formatNumberWithCommas(org.spend, 4)} + + {org.litellm_budget_table?.max_budget !== null && + org.litellm_budget_table?.max_budget !== undefined + ? org.litellm_budget_table?.max_budget + : "No limit"} + + 3 ? "px-0" : ""} + > +
+ {Array.isArray(org.models) ? ( +
+ {org.models.length === 0 ? ( + + All Proxy Models + + ) : ( +
+ {org.models.length > 3 && ( + + )} +
+ {org.models.slice(0, 3).map((model, index) => + model === "all-proxy-models" ? ( + + All Proxy Models + + ) : ( + + {model.length > 30 + ? `${getModelDisplayName(model).slice(0, 30)}...` + : getModelDisplayName(model)} + + ), + )} + {org.models.length > 3 && + !expandedAccordions[org.organization_id || ""] && ( + + +{org.models.length - 3}{" "} + {org.models.length - 3 === 1 ? "more model" : "more models"} + + )} + {expandedAccordions[org.organization_id || ""] && (
- {org.models.slice(0, 3).map((model, index) => + {org.models.slice(3).map((model, index) => model === "all-proxy-models" ? ( - - All Proxy Models + + All Proxy Models ) : ( - - - {model.length > 30 - ? `${getModelDisplayName(model).slice(0, 30)}...` - : getModelDisplayName(model)} - + + {model.length > 30 + ? `${getModelDisplayName(model).slice(0, 30)}...` + : getModelDisplayName(model)} ), )} - {org.models.length > 3 && - !expandedAccordions[org.organization_id || ""] && ( - - - +{org.models.length - 3}{" "} - {org.models.length - 3 === 1 - ? "more model" - : "more models"} - - - )} - {expandedAccordions[org.organization_id || ""] && ( -
- {org.models.slice(3).map((model, index) => - model === "all-proxy-models" ? ( - - All Proxy Models - - ) : ( - - - {model.length > 30 - ? `${getModelDisplayName(model).slice(0, 30)}...` - : getModelDisplayName(model)} - - - ), - )} -
- )}
-
- - )} -
- ) : null} -
- - - - TPM:{" "} - {org.litellm_budget_table?.tpm_limit - ? org.litellm_budget_table?.tpm_limit - : "Unlimited"} -
- RPM:{" "} - {org.litellm_budget_table?.rpm_limit - ? org.litellm_budget_table?.rpm_limit - : "Unlimited"} -
-
- - {org.members?.length || 0} Members - - - {userRole === "Admin" && ( - <> - { - setSelectedOrgId(org.organization_id); - setEditOrg(true); - }} - /> - handleDelete(org.organization_id)} - /> - - )} - - - )) - : null} - -
- - - - - - + )} +
+
+ )} +
+ ) : null} +
+ + + + TPM:{" "} + {org.litellm_budget_table?.tpm_limit + ? org.litellm_budget_table?.tpm_limit + : "Unlimited"} +
+ RPM:{" "} + {org.litellm_budget_table?.rpm_limit + ? org.litellm_budget_table?.rpm_limit + : "Unlimited"} +
+
+ + {org.members?.length || 0} Members + + + {userRole === "Admin" && ( + <> + { + setSelectedOrgId(org.organization_id); + setEditOrg(true); + }} + /> + handleDelete(org.organization_id)} + /> + + )} + + + )) + : null} + + +
+
+
+ + )} - - - -
- - - - - form.setFieldValue("models", values)} - context="organization" - /> - + + - - - - - - daily - weekly - monthly - - - - - - - - + { + if (!o) handleCancel(); + }} + > + + + Create Organization + + + +
+ +
+ + {form.formState.errors.organization_alias && ( +

+ {form.formState.errors.organization_alias.message as string} +

+ )} +
+
- - Allowed Vector Stores{" "} - - - - - } - name="allowed_vector_store_ids" - className="mt-4" - help="Select vector stores this organization can access. Leave empty for access to all vector stores" - > - form.setFieldValue("allowed_vector_store_ids", values)} - value={form.getFieldValue("allowed_vector_store_ids")} - accessToken={accessToken || ""} - placeholder="Select vector stores (optional)" - /> - +
+ + ( + field.onChange(values)} + context="organization" + /> + )} + /> +
- - Allowed MCP Servers{" "} - - - - - } - name="allowed_mcp_servers_and_groups" - className="mt-4" - help="Select MCP servers and access groups this organization can access." - > - form.setFieldValue("allowed_mcp_servers_and_groups", values)} - value={form.getFieldValue("allowed_mcp_servers_and_groups")} - accessToken={accessToken || ""} - placeholder="Select MCP servers and access groups (optional)" - /> - +
+ + ( + + )} + /> +
- - - +
+ + ( + + )} + /> +
-
- -
- -
+
+ + ( + + )} + /> +
+ +
+ + ( + + )} + /> +
+ +
+ +
+ ( + field.onChange(values)} + value={field.value} + accessToken={accessToken || ""} + placeholder="Select vector stores (optional)" + /> + )} + /> +

+ Select vector stores this organization can access. Leave empty for access to all + vector stores +

+
+
+ +
+ +
+ ( + field.onChange(values)} + value={field.value as any} + accessToken={accessToken || ""} + placeholder="Select MCP servers and access groups (optional)" + /> + )} + /> +

+ Select MCP servers and access groups this organization can access. +

+
+
+ +
+ +