diff --git a/ui/litellm-dashboard/.prettierrc.json b/ui/litellm-dashboard/.prettierrc.json new file mode 100644 index 00000000000..69cb9796325 --- /dev/null +++ b/ui/litellm-dashboard/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": false, + "tabWidth": 2, + "printWidth": 120, + "trailingComma": "all", + "jsxBracketSameLine": false +} \ No newline at end of file diff --git a/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx b/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx index 2dac677b4b0..f7a892d6466 100644 --- a/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx +++ b/ui/litellm-dashboard/src/components/bulk_create_users_button.tsx @@ -1,42 +1,49 @@ -import React, { useState, useEffect } from "react"; -import { Button as TremorButton, Text } from "@tremor/react"; -import { Modal, Table, Upload, message, Alert, Typography } from "antd"; -import { UploadOutlined, DownloadOutlined, WarningOutlined, FileTextOutlined, DeleteOutlined, FileExclamationOutlined } from "@ant-design/icons"; -import { userCreateCall, invitationCreateCall, getProxyUISettings } from "./networking"; -import Papa from "papaparse"; -import { CheckCircleIcon, XCircleIcon, ExclamationIcon } from "@heroicons/react/outline"; -import { CopyToClipboard } from "react-copy-to-clipboard"; -import { InvitationLink } from "./onboarding_link"; +import React, { useState, useEffect } from "react" +import { Button as TremorButton, Text } from "@tremor/react" +import { Modal, Table, Upload, message, Alert, Typography } from "antd" +import { + UploadOutlined, + DownloadOutlined, + WarningOutlined, + FileTextOutlined, + DeleteOutlined, + FileExclamationOutlined, +} from "@ant-design/icons" +import { userCreateCall, invitationCreateCall, getProxyUISettings } from "./networking" +import Papa from "papaparse" +import { CheckCircleIcon, XCircleIcon, ExclamationIcon } from "@heroicons/react/outline" +import { CopyToClipboard } from "react-copy-to-clipboard" +import { InvitationLink } from "./onboarding_link" interface BulkCreateUsersProps { - accessToken: string; - teams: any[] | null; - possibleUIRoles: null | Record>; - onUsersCreated?: () => void; + accessToken: string + teams: any[] | null + possibleUIRoles: null | Record> + onUsersCreated?: () => void } interface UserData { - user_email: string; - user_role: string; - teams?: string | string[]; - metadata?: string; - max_budget?: string | number; - budget_duration?: string; - models?: string | string[]; - status?: string; - error?: string; - rowNumber?: number; - isValid?: boolean; - key?: string; - invitation_link?: string; + user_email: string + user_role: string + teams?: string | string[] + metadata?: string + max_budget?: string | number + budget_duration?: string + models?: string | string[] + status?: string + error?: string + rowNumber?: number + isValid?: boolean + key?: string + invitation_link?: string } // Define an interface for the UI settings interface UISettings { - PROXY_BASE_URL: string | null; - PROXY_LOGOUT_URL: string | null; - DEFAULT_TEAM_DISABLED: boolean; - SSO_ENABLED: boolean; + PROXY_BASE_URL: string | null + PROXY_LOGOUT_URL: string | null + DEFAULT_TEAM_DISABLED: boolean + SSO_ENABLED: boolean } const BulkCreateUsersButton: React.FC = ({ @@ -45,389 +52,408 @@ const BulkCreateUsersButton: React.FC = ({ possibleUIRoles, onUsersCreated, }) => { - const [isModalVisible, setIsModalVisible] = useState(false); - const [parsedData, setParsedData] = useState([]); - const [isProcessing, setIsProcessing] = useState(false); - const [parseError, setParseError] = useState(null); - const [csvStructureError, setCsvStructureError] = useState(null); - const [fileError, setFileError] = useState(null); - const [selectedFile, setSelectedFile] = useState(null); - const [uiSettings, setUISettings] = useState(null); - const [baseUrl, setBaseUrl] = useState("http://localhost:4000"); + const [isModalVisible, setIsModalVisible] = useState(false) + const [parsedData, setParsedData] = useState([]) + const [isProcessing, setIsProcessing] = useState(false) + const [parseError, setParseError] = useState(null) + const [csvStructureError, setCsvStructureError] = useState(null) + const [fileError, setFileError] = useState(null) + const [selectedFile, setSelectedFile] = useState(null) + const [uiSettings, setUISettings] = useState(null) + const [baseUrl, setBaseUrl] = useState("http://localhost:4000") useEffect(() => { // Get UI settings const fetchUISettings = async () => { try { - const uiSettingsResponse = await getProxyUISettings(accessToken); - setUISettings(uiSettingsResponse); + const uiSettingsResponse = await getProxyUISettings(accessToken) + setUISettings(uiSettingsResponse) } catch (error) { - console.error("Error fetching UI settings:", error); + console.error("Error fetching UI settings:", error) } - }; + } - fetchUISettings(); + fetchUISettings() // Set base URL - const base = new URL("/", window.location.href); - setBaseUrl(base.toString()); - }, [accessToken]); + const base = new URL("/", window.location.href) + setBaseUrl(base.toString()) + }, [accessToken]) const downloadTemplate = () => { const template = [ ["user_email", "user_role", "teams", "max_budget", "budget_duration", "models"], ["user@example.com", "internal_user", "team-id-1,team-id-2", "100", "30d", "gpt-3.5-turbo,gpt-4"], - ]; - - const csv = Papa.unparse(template); - const blob = new Blob([csv], { type: "text/csv" }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "bulk_users_template.csv"; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - }; + ] + + const csv = Papa.unparse(template) + const blob = new Blob([csv], { type: "text/csv" }) + const url = window.URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = "bulk_users_template.csv" + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + window.URL.revokeObjectURL(url) + } const handleFileUpload = (file: File) => { // Reset all error states - setParseError(null); - setCsvStructureError(null); - setFileError(null); - + setParseError(null) + setCsvStructureError(null) + setFileError(null) + // Set the selected file - always show the file even if it's invalid - setSelectedFile(file); - + setSelectedFile(file) + // Check file type - if (file.type !== 'text/csv' && !file.name.endsWith('.csv')) { - setFileError(`Invalid file type: ${file.name}. Please upload a CSV file (.csv extension).`); - message.error("Invalid file type. Please upload a CSV file."); - return false; + if (file.type !== "text/csv" && !file.name.endsWith(".csv")) { + setFileError(`Invalid file type: ${file.name}. Please upload a CSV file (.csv extension).`) + message.error("Invalid file type. Please upload a CSV file.") + return false } - + // Check file size (limit to 5MB) if (file.size > 5 * 1024 * 1024) { - setFileError(`File is too large (${(file.size / (1024 * 1024)).toFixed(1)} MB). Please upload a CSV file smaller than 5MB.`); - return false; + setFileError( + `File is too large (${(file.size / (1024 * 1024)).toFixed(1)} MB). Please upload a CSV file smaller than 5MB.`, + ) + return false } Papa.parse(file, { complete: (results) => { // Check if file is empty if (!results.data || results.data.length === 0) { - setCsvStructureError("The CSV file appears to be empty. Please upload a file with data."); - setParsedData([]); - return; + setCsvStructureError("The CSV file appears to be empty. Please upload a file with data.") + setParsedData([]) + return } - + // Check if there's only header row if (results.data.length === 1) { - setCsvStructureError("The CSV file only contains headers but no user data. Please add user data to your CSV."); - setParsedData([]); - return; + setCsvStructureError("The CSV file only contains headers but no user data. Please add user data to your CSV.") + setParsedData([]) + return } - - const headers = results.data[0] as string[]; - + + const headers = results.data[0] as string[] + // Check if headers exist - if (headers.length === 0 || (headers.length === 1 && headers[0] === '')) { - setCsvStructureError("The CSV file doesn't contain any column headers. Please make sure your CSV has headers."); - setParsedData([]); - return; + if (headers.length === 0 || (headers.length === 1 && headers[0] === "")) { + setCsvStructureError( + "The CSV file doesn't contain any column headers. Please make sure your CSV has headers.", + ) + setParsedData([]) + return } - - const requiredColumns = ['user_email', 'user_role']; - + + const requiredColumns = ["user_email", "user_role"] + // Check if all required columns are present - const missingColumns = requiredColumns.filter(col => !headers.includes(col)); + const missingColumns = requiredColumns.filter((col) => !headers.includes(col)) if (missingColumns.length > 0) { - setCsvStructureError(`Your CSV is missing these required columns: ${missingColumns.join(', ')}. Please add these columns to your CSV file.`); - setParsedData([]); - return; + setCsvStructureError( + `Your CSV is missing these required columns: ${missingColumns.join(", ")}. Please add these columns to your CSV file.`, + ) + setParsedData([]) + return } try { - const userData = results.data.slice(1).map((row: any, index: number) => { - // Skip empty rows - if (row.length === 0 || (row.length === 1 && row[0] === '')) { - return null; - } - - // Check if row has enough columns - if (row.length < headers.length) { - return { - rowNumber: index + 2, - isValid: false, - error: `Row ${index + 2} has fewer columns than the header row. Please ensure all data is properly formatted.`, - user_email: '', - user_role: '' - } as UserData; - } - - const user: UserData = { - user_email: row[headers.indexOf("user_email")]?.trim() || '', - user_role: row[headers.indexOf("user_role")]?.trim() || '', - teams: row[headers.indexOf("teams")]?.trim(), - max_budget: row[headers.indexOf("max_budget")]?.trim(), - budget_duration: row[headers.indexOf("budget_duration")]?.trim(), - models: row[headers.indexOf("models")]?.trim(), - rowNumber: index + 2, - isValid: true, - error: '', - }; + const userData = results.data + .slice(1) + .map((row: any, index: number) => { + // Skip empty rows + if (row.length === 0 || (row.length === 1 && row[0] === "")) { + return null + } - // Validate the row - const errors: string[] = []; - - // Email validation - if (!user.user_email) { - errors.push('Email is required'); - } else if (!user.user_email.includes('@') || !user.user_email.includes('.')) { - errors.push('Invalid email format (must contain @ and domain)'); - } - - // Role validation - if (!user.user_role) { - errors.push('Role is required'); - } else { - // Validate user role - const validRoles = ['proxy_admin', 'proxy_admin_view_only', 'internal_user', 'internal_user_view_only']; - if (!validRoles.includes(user.user_role)) { - errors.push(`Invalid role "${user.user_role}". Must be one of: ${validRoles.join(', ')}`); + // Check if row has enough columns + if (row.length < headers.length) { + return { + rowNumber: index + 2, + isValid: false, + error: `Row ${index + 2} has fewer columns than the header row. Please ensure all data is properly formatted.`, + user_email: "", + user_role: "", + } as UserData } - } - - // Budget validation - if (user.max_budget && user.max_budget.toString().trim() !== '') { - if (isNaN(parseFloat(user.max_budget.toString()))) { - errors.push(`Max budget "${user.max_budget}" must be a number`); - } else if (parseFloat(user.max_budget.toString()) <= 0) { - errors.push('Max budget must be greater than 0'); + + const user: UserData = { + user_email: row[headers.indexOf("user_email")]?.trim() || "", + user_role: row[headers.indexOf("user_role")]?.trim() || "", + teams: row[headers.indexOf("teams")]?.trim(), + max_budget: row[headers.indexOf("max_budget")]?.trim(), + budget_duration: row[headers.indexOf("budget_duration")]?.trim(), + models: row[headers.indexOf("models")]?.trim(), + rowNumber: index + 2, + isValid: true, + error: "", } - } - - // Budget duration validation - if (user.budget_duration && !user.budget_duration.match(/^\d+[dhmwy]$|^\d+mo$/)) { - errors.push(`Invalid budget duration format "${user.budget_duration}". Use format like "30d", "1mo", "2w", "6h"`); - } - - // Teams validation - if (user.teams && typeof user.teams === 'string') { - // Check if teams exist (if teams data is available) - if (teams && teams.length > 0) { - const teamIds = teams.map(t => t.team_id); - const userTeams = user.teams.split(',').map(t => t.trim()); - const invalidTeams = userTeams.filter(t => !teamIds.includes(t)); - if (invalidTeams.length > 0) { - errors.push(`Unknown team(s): ${invalidTeams.join(', ')}`); + + // Validate the row + const errors: string[] = [] + + // Email validation + if (!user.user_email) { + errors.push("Email is required") + } else if (!user.user_email.includes("@") || !user.user_email.includes(".")) { + errors.push("Invalid email format (must contain @ and domain)") + } + + // Role validation + if (!user.user_role) { + errors.push("Role is required") + } else { + // Validate user role + const validRoles = ["proxy_admin", "proxy_admin_view_only", "internal_user", "internal_user_view_only"] + if (!validRoles.includes(user.user_role)) { + errors.push(`Invalid role "${user.user_role}". Must be one of: ${validRoles.join(", ")}`) } } - } - if (errors.length > 0) { - user.isValid = false; - user.error = errors.join(', '); - } + // Budget validation + if (user.max_budget && user.max_budget.toString().trim() !== "") { + if (isNaN(parseFloat(user.max_budget.toString()))) { + errors.push(`Max budget "${user.max_budget}" must be a number`) + } else if (parseFloat(user.max_budget.toString()) <= 0) { + errors.push("Max budget must be greater than 0") + } + } - return user; - }).filter(Boolean) as UserData[]; // Filter out null values (empty rows) + // Budget duration validation + if (user.budget_duration && !user.budget_duration.match(/^\d+[dhmwy]$|^\d+mo$/)) { + errors.push( + `Invalid budget duration format "${user.budget_duration}". Use format like "30d", "1mo", "2w", "6h"`, + ) + } - const validData = userData.filter(user => user.isValid); - setParsedData(userData); + // Teams validation + if (user.teams && typeof user.teams === "string") { + // Check if teams exist (if teams data is available) + if (teams && teams.length > 0) { + const teamIds = teams.map((t) => t.team_id) + const userTeams = user.teams.split(",").map((t) => t.trim()) + const invalidTeams = userTeams.filter((t) => !teamIds.includes(t)) + if (invalidTeams.length > 0) { + errors.push(`Unknown team(s): ${invalidTeams.join(", ")}`) + } + } + } + + if (errors.length > 0) { + user.isValid = false + user.error = errors.join(", ") + } + + return user + }) + .filter(Boolean) as UserData[] // Filter out null values (empty rows) + + const validData = userData.filter((user) => user.isValid) + setParsedData(userData) if (userData.length === 0) { - setCsvStructureError("No valid data rows found in the CSV file. Please check your file format."); + setCsvStructureError("No valid data rows found in the CSV file. Please check your file format.") } else if (validData.length === 0) { - setParseError('No valid users found in the CSV. Please check the errors below and fix your CSV file.'); + setParseError("No valid users found in the CSV. Please check the errors below and fix your CSV file.") } else if (validData.length < userData.length) { - setParseError(`Found ${userData.length - validData.length} row(s) with errors out of ${userData.length} total rows. Please correct them before proceeding.`); + setParseError( + `Found ${userData.length - validData.length} row(s) with errors out of ${userData.length} total rows. Please correct them before proceeding.`, + ) } else { - message.success(`Successfully parsed ${validData.length} users`); + message.success(`Successfully parsed ${validData.length} users`) } } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error'; - setParseError(`Error parsing CSV: ${errorMessage}`); - setParsedData([]); + const errorMessage = error instanceof Error ? error.message : "Unknown error" + setParseError(`Error parsing CSV: ${errorMessage}`) + setParsedData([]) } }, error: (error) => { - setParseError(`Failed to parse CSV file: ${error.message}`); - setParsedData([]); + setParseError(`Failed to parse CSV file: ${error.message}`) + setParsedData([]) }, header: false, - }); - return false; - }; + }) + return false + } const removeSelectedFile = () => { - setSelectedFile(null); - setParsedData([]); - setParseError(null); - setCsvStructureError(null); - setFileError(null); - }; + setSelectedFile(null) + setParsedData([]) + setParseError(null) + setCsvStructureError(null) + setFileError(null) + } const handleBulkCreate = async () => { - setIsProcessing(true); - const updatedData = parsedData.map(user => ({ ...user, status: 'pending' })); - setParsedData(updatedData); - - let anySuccessful = false; + setIsProcessing(true) + const updatedData = parsedData.map((user) => ({ ...user, status: "pending" })) + setParsedData(updatedData) + + let anySuccessful = false for (let index = 0; index < updatedData.length; index++) { - const user = updatedData[index]; + const user = updatedData[index] try { // Create a clean user object with only non-empty values const cleanUser: Partial = { user_email: user.user_email, - user_role: user.user_role - }; - - // Only add optional fields if they have values - if (user.teams && typeof user.teams === 'string' && user.teams.trim() !== '') { - cleanUser.teams = user.teams.split(',').map(team => team.trim()).filter(Boolean); - // Only include teams if there's at least one valid team - if (cleanUser.teams.length === 0) { - delete cleanUser.teams; - } - } - - // Only add models if provided and non-empty - if (user.models && typeof user.models === 'string' && user.models.trim() !== '') { - cleanUser.models = user.models.split(',').map(model => model.trim()).filter(Boolean); - // Only include models if there's at least one valid model - if (cleanUser.models.length === 0) { - delete cleanUser.models; - } - } - - // Only add max_budget if it's a valid number - if (user.max_budget && user.max_budget.toString().trim() !== '') { - const budgetValue = parseFloat(user.max_budget.toString()); - if (!isNaN(budgetValue) && budgetValue > 0) { - cleanUser.max_budget = budgetValue; - } - } - - // Only add budget_duration if provided and non-empty - if (user.budget_duration && user.budget_duration.trim() !== '') { - cleanUser.budget_duration = user.budget_duration.trim(); - } - - // Only add metadata if provided and non-empty - if (user.metadata && typeof user.metadata === 'string' && user.metadata.trim() !== '') { - cleanUser.metadata = user.metadata.trim(); + user_role: user.user_role, } - console.log('Sending user data:', cleanUser); - const response = await userCreateCall(accessToken, null, cleanUser); - console.log('Full response:', response); - + // Only add optional fields if they have values + if (user.teams && typeof user.teams === "string" && user.teams.trim() !== "") { + cleanUser.teams = user.teams + .split(",") + .map((team) => team.trim()) + .filter(Boolean) + // Only include teams if there's at least one valid team + if (cleanUser.teams.length === 0) { + delete cleanUser.teams + } + } + + // Only add models if provided and non-empty + if (user.models && typeof user.models === "string" && user.models.trim() !== "") { + cleanUser.models = user.models + .split(",") + .map((model) => model.trim()) + .filter(Boolean) + // Only include models if there's at least one valid model + if (cleanUser.models.length === 0) { + delete cleanUser.models + } + } + + // Only add max_budget if it's a valid number + if (user.max_budget && user.max_budget.toString().trim() !== "") { + const budgetValue = parseFloat(user.max_budget.toString()) + if (!isNaN(budgetValue) && budgetValue > 0) { + cleanUser.max_budget = budgetValue + } + } + + // Only add budget_duration if provided and non-empty + if (user.budget_duration && user.budget_duration.trim() !== "") { + cleanUser.budget_duration = user.budget_duration.trim() + } + + // Only add metadata if provided and non-empty + if (user.metadata && typeof user.metadata === "string" && user.metadata.trim() !== "") { + cleanUser.metadata = user.metadata.trim() + } + + console.log("Sending user data:", cleanUser) + const response = await userCreateCall(accessToken, null, cleanUser) + console.log("Full response:", response) + // Check if response has key or user_id, indicating success if (response && (response.key || response.user_id)) { - anySuccessful = true; - console.log('Success case triggered'); - const user_id = response.data?.user_id || response.user_id; - + anySuccessful = true + console.log("Success case triggered") + const user_id = response.data?.user_id || response.user_id + // Create invitation link for the user try { if (!uiSettings?.SSO_ENABLED) { // Regular invitation flow - const invitationData = await invitationCreateCall(accessToken, user_id); - const invitationUrl = new URL(`/ui?invitation_id=${invitationData.id}`, baseUrl).toString(); - - setParsedData(current => - current.map((u, i) => - i === index ? { - ...u, - status: 'success', - key: response.key || response.user_id, - invitation_link: invitationUrl - } : u - ) - ); + const invitationData = await invitationCreateCall(accessToken, user_id) + const invitationUrl = new URL(`/ui?invitation_id=${invitationData.id}`, baseUrl).toString() + + setParsedData((current) => + current.map((u, i) => + i === index + ? { + ...u, + status: "success", + key: response.key || response.user_id, + invitation_link: invitationUrl, + } + : u, + ), + ) } else { // SSO flow - just use the base URL - const invitationUrl = new URL("/ui", baseUrl).toString(); - - setParsedData(current => - current.map((u, i) => - i === index ? { - ...u, - status: 'success', - key: response.key || response.user_id, - invitation_link: invitationUrl - } : u - ) - ); + const invitationUrl = new URL("/ui", baseUrl).toString() + + setParsedData((current) => + current.map((u, i) => + i === index + ? { + ...u, + status: "success", + key: response.key || response.user_id, + invitation_link: invitationUrl, + } + : u, + ), + ) } } catch (inviteError) { - console.error('Error creating invitation:', inviteError); - setParsedData(current => - current.map((u, i) => - i === index ? { - ...u, - status: 'success', - key: response.key || response.user_id, - error: 'User created but failed to generate invitation link' - } : u - ) - ); + console.error("Error creating invitation:", inviteError) + setParsedData((current) => + current.map((u, i) => + i === index + ? { + ...u, + status: "success", + key: response.key || response.user_id, + error: "User created but failed to generate invitation link", + } + : u, + ), + ) } } else { - console.log('Error case triggered'); - const errorMessage = response?.error || 'Failed to create user'; - console.log('Error message:', errorMessage); - setParsedData(current => - current.map((u, i) => - i === index ? { ...u, status: 'failed', error: errorMessage } : u + console.log("Error case triggered") + const errorMessage = response?.error || "Failed to create user" + console.log("Error message:", errorMessage) + setParsedData((current) => + current.map((u, i) => (i === index ? { ...u, status: "failed", error: errorMessage } : u)), ) - ); } } catch (error) { - console.error('Caught error:', error); - const errorMessage = (error as any)?.response?.data?.error || - (error as Error)?.message || - String(error); - setParsedData(current => - current.map((u, i) => - i === index ? { ...u, status: 'failed', error: errorMessage } : u - ) - ); + console.error("Caught error:", error) + const errorMessage = (error as any)?.response?.data?.error || (error as Error)?.message || String(error) + setParsedData((current) => + current.map((u, i) => (i === index ? { ...u, status: "failed", error: errorMessage } : u)), + ) } } - setIsProcessing(false); - + setIsProcessing(false) + // Call the callback if any users were successfully created if (anySuccessful && onUsersCreated) { - onUsersCreated(); + onUsersCreated() } - }; + } const downloadResults = () => { - const results = parsedData.map(user => ({ + const results = parsedData.map((user) => ({ user_email: user.user_email, user_role: user.user_role, status: user.status, - key: user.key || '', - invitation_link: user.invitation_link || '', - error: user.error || '' - })); - - const csv = Papa.unparse(results); - const blob = new Blob([csv], { type: "text/csv" }); - const url = window.URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = "bulk_users_results.csv"; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - window.URL.revokeObjectURL(url); - }; + key: user.key || "", + invitation_link: user.invitation_link || "", + error: user.error || "", + })) + + const csv = Papa.unparse(results) + const blob = new Blob([csv], { type: "text/csv" }) + const url = window.URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = url + a.download = "bulk_users_results.csv" + document.body.appendChild(a) + a.click() + document.body.removeChild(a) + window.URL.revokeObjectURL(url) + } const columns = [ { @@ -457,8 +483,8 @@ const BulkCreateUsersButton: React.FC = ({ key: "max_budget", }, { - title: 'Status', - key: 'status', + title: "Status", + key: "status", render: (_: any, record: UserData) => { if (!record.isValid) { return ( @@ -467,16 +493,14 @@ const BulkCreateUsersButton: React.FC = ({ Invalid - {record.error && ( - {record.error} - )} + {record.error && {record.error}} - ); + ) } - if (!record.status || record.status === 'pending') { - return Pending; + if (!record.status || record.status === "pending") { + return Pending } - if (record.status === 'success') { + if (record.status === "success") { return (
@@ -486,22 +510,18 @@ const BulkCreateUsersButton: React.FC = ({ {record.invitation_link && (
- - {record.invitation_link} - + {record.invitation_link} message.success("Invitation link copied!")} > - +
)}
- ); + ) } return (
@@ -509,29 +529,25 @@ const BulkCreateUsersButton: React.FC = ({ Failed
- {record.error && ( - - {JSON.stringify(record.error)} - - )} + {record.error && {JSON.stringify(record.error)}}
- ); + ) }, }, - ]; + ] return ( <> - setIsModalVisible(true)}> + setIsModalVisible(true)}> + Bulk Invite Users - + setIsModalVisible(false)} - bodyStyle={{ maxHeight: '70vh', overflow: 'auto' }} + bodyStyle={{ maxHeight: "70vh", overflow: "auto" }} footer={null} >
@@ -539,10 +555,12 @@ const BulkCreateUsersButton: React.FC = ({ {parsedData.length === 0 ? (
-
1
+
+ 1 +

Download and fill the template

- +

Add multiple users at once by following these steps:

    @@ -551,7 +569,7 @@ const BulkCreateUsersButton: React.FC = ({
  1. Save the file and upload it here
  2. After creation, download the results file containing the API keys for each user
- +

Template Column Names

@@ -566,14 +584,19 @@ const BulkCreateUsersButton: React.FC = ({

user_role

-

User's role (one of: "proxy_admin", "proxy_admin_view_only", "internal_user", "internal_user_view_only")

+

+ User's role (one of: "proxy_admin", "proxy_admin_view_only", + "internal_user", "internal_user_view_only") +

teams

-

Comma-separated team IDs (e.g., "team-1,team-2")

+

+ Comma-separated team IDs (e.g., "team-1,team-2") +

@@ -587,36 +610,40 @@ const BulkCreateUsersButton: React.FC = ({

budget_duration

-

Budget reset period (e.g., "30d", "1mo")

+

+ Budget reset period (e.g., "30d", "1mo") +

models

-

Comma-separated allowed models (e.g., "gpt-3.5-turbo,gpt-4")

+

+ Comma-separated allowed models (e.g., "gpt-3.5-turbo,gpt-4") +

- - + + Download CSV Template
- +
-
2
+
+ 2 +

Upload your completed CSV

- +
{selectedFile ? ( -
+
{fileError ? ( @@ -642,28 +669,25 @@ const BulkCreateUsersButton: React.FC = ({ Remove
- + {fileError ? (
{fileError}
- ) : !csvStructureError && ( -
-
-
+ ) : ( + !csvStructureError && ( +
+
+
+
+ Processing...
- Processing... -
+ ) )}
) : ( - +

Drag and drop your CSV file here

@@ -673,13 +697,15 @@ const BulkCreateUsersButton: React.FC = ({
)} - + {csvStructureError && (
- CSV Structure Error + + CSV Structure Error + {csvStructureError} @@ -695,24 +721,29 @@ const BulkCreateUsersButton: React.FC = ({ ) : (
-
3
+
+ 3 +

- {parsedData.some(user => user.status === 'success' || user.status === 'failed') - ? "User Creation Results" + {parsedData.some((user) => user.status === "success" || user.status === "failed") + ? "User Creation Results" : "Review and create users"}

- + {parseError && (
{parseError} - {parsedData.some(user => !user.isValid) && ( + {parsedData.some((user) => !user.isValid) && (
  • Check the table below for specific errors in each row
  • -
  • Common issues include invalid email formats, missing required fields, or incorrect role values
  • +
  • + Common issues include invalid email formats, missing required fields, or incorrect role + values +
  • Fix these issues in your CSV file and upload again
)} @@ -720,19 +751,19 @@ const BulkCreateUsersButton: React.FC = ({
)} - +
- {parsedData.some(user => user.status === 'success' || user.status === 'failed') ? ( + {parsedData.some((user) => user.status === "success" || user.status === "failed") ? (
Creation Summary - {parsedData.filter(d => d.status === 'success').length} Successful + {parsedData.filter((d) => d.status === "success").length} Successful - {parsedData.some(d => d.status === 'failed') && ( + {parsedData.some((d) => d.status === "failed") && ( - {parsedData.filter(d => d.status === 'failed').length} Failed + {parsedData.filter((d) => d.status === "failed").length} Failed )}
@@ -740,34 +771,34 @@ const BulkCreateUsersButton: React.FC = ({
User Preview - {parsedData.filter(d => d.isValid).length} of {parsedData.length} users valid + {parsedData.filter((d) => d.isValid).length} of {parsedData.length} users valid
)}
- - {!parsedData.some(user => user.status === 'success' || user.status === 'failed') && ( + + {!parsedData.some((user) => user.status === "success" || user.status === "failed") && (
- { - setParsedData([]); - setParseError(null); - }} + setParsedData([]) + setParseError(null) + }} variant="secondary" > Back d.isValid).length === 0 || isProcessing} + disabled={parsedData.filter((d) => d.isValid).length === 0 || isProcessing} > - {isProcessing ? 'Creating...' : `Create ${parsedData.filter(d => d.isValid).length} Users`} + {isProcessing ? "Creating..." : `Create ${parsedData.filter((d) => d.isValid).length} Users`}
)}
- - {parsedData.some(user => user.status === 'success') && ( + + {parsedData.some((user) => user.status === "success") && (
@@ -776,30 +807,31 @@ const BulkCreateUsersButton: React.FC = ({
User creation complete - Next step: Download the credentials file containing API keys and invitation links. - Users will need these API keys to make LLM requests through LiteLLM. + Next step: Download the credentials file containing API + keys and invitation links. Users will need these API keys to make LLM requests through + LiteLLM.
)} - + !record.isValid ? 'bg-red-50' : ''} + rowClassName={(record) => (!record.isValid ? "bg-red-50" : "")} /> - - {!parsedData.some(user => user.status === 'success' || user.status === 'failed') && ( + + {!parsedData.some((user) => user.status === "success" || user.status === "failed") && (
- { - setParsedData([]); - setParseError(null); - }} + setParsedData([]) + setParseError(null) + }} variant="secondary" className="mr-3" > @@ -807,30 +839,26 @@ const BulkCreateUsersButton: React.FC = ({ d.isValid).length === 0 || isProcessing} + disabled={parsedData.filter((d) => d.isValid).length === 0 || isProcessing} > - {isProcessing ? 'Creating...' : `Create ${parsedData.filter(d => d.isValid).length} Users`} + {isProcessing ? "Creating..." : `Create ${parsedData.filter((d) => d.isValid).length} Users`}
)} - - {parsedData.some(user => user.status === 'success' || user.status === 'failed') && ( + + {parsedData.some((user) => user.status === "success" || user.status === "failed") && (
- { - setParsedData([]); - setParseError(null); - }} + setParsedData([]) + setParseError(null) + }} variant="secondary" className="mr-3" > Start New Bulk Import - + Download User Credentials
@@ -841,7 +869,7 @@ const BulkCreateUsersButton: React.FC = ({ - ); -}; + ) +} -export default BulkCreateUsersButton; \ No newline at end of file +export default BulkCreateUsersButton diff --git a/ui/litellm-dashboard/src/components/create_user_button.tsx b/ui/litellm-dashboard/src/components/create_user_button.tsx index 5f7e93deb8d..3edfe93a076 100644 --- a/ui/litellm-dashboard/src/components/create_user_button.tsx +++ b/ui/litellm-dashboard/src/components/create_user_button.tsx @@ -1,47 +1,47 @@ -import React, { useState, useEffect } from "react"; -import { useRouter } from "next/navigation"; +import React, { useState, useEffect } from "react" +import { useRouter } from "next/navigation" +import { Button, Modal, Form, Input, message, Select, InputNumber, Select as Select2 } from "antd" import { - Button, - Modal, - Form, - Input, - message, - Select, - InputNumber, - Select as Select2, -} from "antd"; -import { Button as Button2, Text, TextInput, SelectItem, Accordion, AccordionHeader, AccordionBody, Title, } from "@tremor/react"; -import OnboardingModal from "./onboarding_link"; -import { InvitationLink } from "./onboarding_link"; + Button as Button2, + Text, + TextInput, + SelectItem, + Accordion, + AccordionHeader, + AccordionBody, + Title, +} from "@tremor/react" +import OnboardingModal from "./onboarding_link" +import { InvitationLink } from "./onboarding_link" import { userCreateCall, modelAvailableCall, invitationCreateCall, getProxyUISettings, getProxyBaseUrl, -} from "./networking"; -import BulkCreateUsers from "./bulk_create_users_button"; -const { Option } = Select; -import { Tooltip } from "antd"; -import { InfoCircleOutlined } from '@ant-design/icons'; -import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key"; -import { useQueryClient } from "@tanstack/react-query"; +} from "./networking" +import BulkCreateUsers from "./bulk_create_users_button" +const { Option } = Select +import { Tooltip } from "antd" +import { InfoCircleOutlined } from "@ant-design/icons" +import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key" +import { useQueryClient } from "@tanstack/react-query" interface CreateuserProps { - userID: string; - accessToken: string; - teams: any[] | null; - possibleUIRoles: null | Record>; - onUserCreated?: (userId: string) => void; - isEmbedded?: boolean; + userID: string + accessToken: string + teams: any[] | null + possibleUIRoles: null | Record> + onUserCreated?: (userId: string) => void + isEmbedded?: boolean } // Define an interface for the UI settings interface UISettings { - PROXY_BASE_URL: string | null; - PROXY_LOGOUT_URL: string | null; - DEFAULT_TEAM_DISABLED: boolean; - SSO_ENABLED: boolean; + PROXY_BASE_URL: string | null + PROXY_LOGOUT_URL: string | null + DEFAULT_TEAM_DISABLED: boolean + SSO_ENABLED: boolean } const Createuser: React.FC = ({ @@ -52,97 +52,91 @@ const Createuser: React.FC = ({ onUserCreated, isEmbedded = false, }) => { - const queryClient = useQueryClient(); - const [uiSettings, setUISettings] = useState(null); - const [form] = Form.useForm(); - const [isModalVisible, setIsModalVisible] = useState(false); - const [apiuser, setApiuser] = useState(false); - const [userModels, setUserModels] = useState([]); - const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = - useState(false); - const [invitationLinkData, setInvitationLinkData] = - useState(null); - const [baseUrl, setBaseUrl] = useState(null); + const queryClient = useQueryClient() + const [uiSettings, setUISettings] = useState(null) + const [form] = Form.useForm() + const [isModalVisible, setIsModalVisible] = useState(false) + const [apiuser, setApiuser] = useState(false) + const [userModels, setUserModels] = useState([]) + const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false) + const [invitationLinkData, setInvitationLinkData] = useState(null) + const [baseUrl, setBaseUrl] = useState(null) // get all models useEffect(() => { const fetchData = async () => { try { - const userRole = "any"; // You may need to get the user role dynamically - const modelDataResponse = await modelAvailableCall( - accessToken, - userID, - userRole, - ); + const userRole = "any" // You may need to get the user role dynamically + const modelDataResponse = await modelAvailableCall(accessToken, userID, userRole) // Assuming modelDataResponse.data contains an array of model objects with a 'model_name' property - const availableModels = []; + const availableModels = [] for (let i = 0; i < modelDataResponse.data.length; i++) { - const model = modelDataResponse.data[i]; - availableModels.push(model.id); + const model = modelDataResponse.data[i] + availableModels.push(model.id) } - console.log("Model data response:", modelDataResponse.data); - console.log("Available models:", availableModels); + console.log("Model data response:", modelDataResponse.data) + console.log("Available models:", availableModels) // Assuming modelDataResponse.data contains an array of model names - setUserModels(availableModels); + setUserModels(availableModels) // get ui settings - const uiSettingsResponse = await getProxyUISettings(accessToken); - console.log("uiSettingsResponse:", uiSettingsResponse); + const uiSettingsResponse = await getProxyUISettings(accessToken) + console.log("uiSettingsResponse:", uiSettingsResponse) - setUISettings(uiSettingsResponse); + setUISettings(uiSettingsResponse) } catch (error) { - console.error("Error fetching model data:", error); + console.error("Error fetching model data:", error) } - }; + } - setBaseUrl(getProxyBaseUrl()); + setBaseUrl(getProxyBaseUrl()) - fetchData(); // Call the function to fetch model data when the component mounts - }, []); // Empty dependency array to run only once + fetchData() // Call the function to fetch model data when the component mounts + }, []) // Empty dependency array to run only once const handleOk = () => { - setIsModalVisible(false); - form.resetFields(); - }; + setIsModalVisible(false) + form.resetFields() + } const handleCancel = () => { - setIsModalVisible(false); - setApiuser(false); - form.resetFields(); - }; + setIsModalVisible(false) + setApiuser(false) + form.resetFields() + } - const handleCreate = async (formValues: { user_id: string, models?: string[], user_role: string }) => { + const handleCreate = async (formValues: { user_id: string; models?: string[]; user_role: string }) => { try { - message.info("Making API Call"); + message.info("Making API Call") if (!isEmbedded) { - setIsModalVisible(true); + setIsModalVisible(true) } if ((!formValues.models || formValues.models.length === 0) && formValues.user_role !== "proxy_admin") { console.log("formValues.user_role", formValues.user_role) // If models is empty or undefined, set it to "no-default-models" - formValues.models = ["no-default-models"]; + formValues.models = ["no-default-models"] } - console.log("formValues in create user:", formValues); - const response = await userCreateCall(accessToken, null, formValues); - await queryClient.invalidateQueries({ queryKey: ['userList'] }) - console.log("user create Response:", response); - setApiuser(true); - const user_id = response.data?.user_id || response.user_id; + console.log("formValues in create user:", formValues) + const response = await userCreateCall(accessToken, null, formValues) + await queryClient.invalidateQueries({ queryKey: ["userList"] }) + console.log("user create Response:", response) + setApiuser(true) + const user_id = response.data?.user_id || response.user_id // Call the callback if provided (for embedded mode) if (onUserCreated && isEmbedded) { - onUserCreated(user_id); - form.resetFields(); - return; // Skip the invitation flow when embedded + onUserCreated(user_id) + form.resetFields() + return // Skip the invitation flow when embedded } // only do invite link flow if sso is not enabled if (!uiSettings?.SSO_ENABLED) { invitationCreateCall(accessToken, user_id).then((data) => { - data.has_user_setup_sso = false; - setInvitationLinkData(data); - setIsInvitationLinkModalVisible(true); - }); + data.has_user_setup_sso = false + setInvitationLinkData(data) + setIsInvitationLinkModalVisible(true) + }) } else { // create an InvitationLink Object for this user for the SSO flow // for SSO the invite link is the proxy base url since the User just needs to login @@ -157,52 +151,41 @@ const Createuser: React.FC = ({ updated_at: new Date(), updated_by: userID, has_user_setup_sso: true, - }; - setInvitationLinkData(invitationLink); - setIsInvitationLinkModalVisible(true); + } + setInvitationLinkData(invitationLink) + setIsInvitationLinkModalVisible(true) } - message.success("API user Created"); - form.resetFields(); - localStorage.removeItem("userData" + userID); + message.success("API user Created") + form.resetFields() + localStorage.removeItem("userData" + userID) } catch (error: any) { - const errorMessage = error.response?.data?.detail || error?.message || "Error creating the user"; - message.error(errorMessage); - console.error("Error creating the user:", error); + const errorMessage = error.response?.data?.detail || error?.message || "Error creating the user" + message.error(errorMessage) + console.error("Error creating the user:", error) } - }; + } // Modify the return statement to handle embedded mode if (isEmbedded) { return ( -
+ {possibleUIRoles && - Object.entries(possibleUIRoles).map( - ([role, { ui_label, description }]) => ( - -
- {ui_label}{" "} -

- {description} -

-
-
- ), - )} + Object.entries(possibleUIRoles).map(([role, { ui_label, description }]) => ( + +
+ {ui_label}{" "} +

+ {description} +

+
+
+ ))}
@@ -224,25 +207,21 @@ const Createuser: React.FC = ({ - +
- ); + ) } // Original return for standalone mode return (
- setIsModalVisible(true)}> + setIsModalVisible(true)}> + Invite User - + = ({ onCancel={handleCancel} > Create a User who can own keys -
+ - - Global Proxy Role{' '} - - - - - } - name="user_role"> + + Global Proxy Role{" "} + + + + + } + name="user_role" + > {possibleUIRoles && - Object.entries(possibleUIRoles).map( - ([role, { ui_label, description }]) => ( - -
- {ui_label}{" "} -

- {description} -

-
-
- ), - )} + Object.entries(possibleUIRoles).map(([role, { ui_label, description }]) => ( + +
+ {ui_label}{" "} +

+ {description} +

+
+
+ ))}
- - + + Yes No - + {guardrailData.litellm_params?.guardrail === "presidio" && ( <> - PII Protection -
- {guardrailSettings && ( - - )} -
+ PII Protection +
+ {guardrailSettings && ( + + )} +
)} - + Provider Settings - + {/* Provider-specific fields */} - guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail - ) || null} - accessToken={accessToken} + guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail, + ) || null + } + accessToken={accessToken} providerParams={guardrailProviderSpecificParams} value={guardrailData.litellm_params} /> - + {/* Optional parameters */} - {guardrailProviderSpecificParams && ( + {guardrailProviderSpecificParams && (() => { const currentProvider = Object.keys(guardrail_provider_map).find( - key => guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail - ); - if (!currentProvider) return null; - - const providerKey = guardrail_provider_map[currentProvider]?.toLowerCase(); - const providerFields = guardrailProviderSpecificParams[providerKey]; - - if (!providerFields || !providerFields.optional_params) return null; - + (key) => guardrail_provider_map[key] === guardrailData.litellm_params?.guardrail, + ) + if (!currentProvider) return null + + const providerKey = guardrail_provider_map[currentProvider]?.toLowerCase() + const providerFields = guardrailProviderSpecificParams[providerKey] + + if (!providerFields || !providerFields.optional_params) return null + return ( - ); - })() - )} - + ) + })()} + Advanced Settings - +
- - - Save Changes - + + Save Changes
) : ( @@ -551,18 +532,20 @@ const GuardrailInfoView: React.FC = ({ {guardrailData.litellm_params?.default_on ? "Yes" : "No"}
- - {guardrailData.litellm_params?.pii_entities_config && Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && ( -
- PII Protection -
- - {Object.keys(guardrailData.litellm_params.pii_entities_config).length} PII entities configured - + + {guardrailData.litellm_params?.pii_entities_config && + Object.keys(guardrailData.litellm_params.pii_entities_config).length > 0 && ( +
+ PII Protection +
+ + {Object.keys(guardrailData.litellm_params.pii_entities_config).length} PII entities + configured + +
-
- )} - + )} +
Created At
{formatDate(guardrailData.created_at)}
@@ -579,7 +562,7 @@ const GuardrailInfoView: React.FC = ({
- ); -}; + ) +} -export default GuardrailInfoView; \ No newline at end of file +export default GuardrailInfoView diff --git a/ui/litellm-dashboard/src/components/guardrails/guardrail_table.tsx b/ui/litellm-dashboard/src/components/guardrails/guardrail_table.tsx index cf6ccade35d..fad26e033bf 100644 --- a/ui/litellm-dashboard/src/components/guardrails/guardrail_table.tsx +++ b/ui/litellm-dashboard/src/components/guardrails/guardrail_table.tsx @@ -1,22 +1,8 @@ -import React, { useState } from "react"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeaderCell, - TableRow, - Icon, - Button, -} from "@tremor/react"; -import { - TrashIcon, - SwitchVerticalIcon, - ChevronUpIcon, - ChevronDownIcon, -} from "@heroicons/react/outline"; -import { Tooltip } from "antd"; -import { Badge } from "@tremor/react"; +import React, { useState } from "react" +import { Table, TableBody, TableCell, TableHead, TableHeaderCell, TableRow, Icon, Button } from "@tremor/react" +import { TrashIcon, SwitchVerticalIcon, ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/outline" +import { Tooltip } from "antd" +import { Badge } from "@tremor/react" import { ColumnDef, flexRender, @@ -24,34 +10,33 @@ import { getSortedRowModel, SortingState, useReactTable, -} from "@tanstack/react-table"; -import { getGuardrailLogoAndName, guardrail_provider_map } from "./guardrail_info_helpers"; -import EditGuardrailForm from "./edit_guardrail_form"; -import GuardrailInfoView from "./guardrail_info"; +} from "@tanstack/react-table" +import { getGuardrailLogoAndName, guardrail_provider_map } from "./guardrail_info_helpers" +import EditGuardrailForm from "./edit_guardrail_form" interface GuardrailItem { - guardrail_id?: string; - guardrail_name: string | null; + guardrail_id?: string + guardrail_name: string | null litellm_params: { - guardrail: string; - mode: string; - default_on: boolean; - pii_entities_config?: {[key: string]: string}; - [key: string]: any; - }; - guardrail_info: Record | null; - created_at?: string; - updated_at?: string; + guardrail: string + mode: string + default_on: boolean + pii_entities_config?: { [key: string]: string } + [key: string]: any + } + guardrail_info: Record | null + created_at?: string + updated_at?: string } interface GuardrailTableProps { - guardrailsList: GuardrailItem[]; - isLoading: boolean; - onDeleteClick: (guardrailId: string, guardrailName: string) => void; - accessToken: string | null; - onGuardrailUpdated: () => void; - isAdmin?: boolean; - onShowGuardrailInfo?: (isVisible: boolean) => void; + guardrailsList: GuardrailItem[] + isLoading: boolean + onDeleteClick: (guardrailId: string, guardrailName: string) => void + accessToken: string | null + onGuardrailUpdated: () => void + isAdmin?: boolean + onGuardrailClick: (id: string) => void } const GuardrailTable: React.FC = ({ @@ -61,52 +46,29 @@ const GuardrailTable: React.FC = ({ accessToken, onGuardrailUpdated, isAdmin = false, - onShowGuardrailInfo, + onGuardrailClick, }) => { - const [sorting, setSorting] = useState([ - { id: "created_at", desc: true } - ]); - const [editModalVisible, setEditModalVisible] = useState(false); - const [selectedGuardrail, setSelectedGuardrail] = useState(null); - const [showGuardrailInfo, setShowGuardrailInfo] = useState(false); - const [selectedGuardrailId, setSelectedGuardrailId] = useState(null); + const [sorting, setSorting] = useState([{ id: "created_at", desc: true }]) + const [editModalVisible, setEditModalVisible] = useState(false) + const [selectedGuardrail, setSelectedGuardrail] = useState(null) // Format date helper function const formatDate = (dateString?: string) => { - if (!dateString) return "-"; - const date = new Date(dateString); - return date.toLocaleString(); - }; + if (!dateString) return "-" + const date = new Date(dateString) + return date.toLocaleString() + } const handleEditClick = (guardrail: GuardrailItem) => { - setSelectedGuardrail(guardrail); - setEditModalVisible(true); - }; + setSelectedGuardrail(guardrail) + setEditModalVisible(true) + } const handleEditSuccess = () => { - setEditModalVisible(false); - setSelectedGuardrail(null); - onGuardrailUpdated(); - }; - - const handleGuardrailIdClick = (guardrailId: string) => { - setSelectedGuardrailId(guardrailId); - setShowGuardrailInfo(true); - onShowGuardrailInfo?.(true); - }; - - const handleGuardrailInfoClose = () => { - setShowGuardrailInfo(false); - setSelectedGuardrailId(null); - onShowGuardrailInfo?.(false); - }; - - const handleGuardrailDeleted = () => { - setShowGuardrailInfo(false); - setSelectedGuardrailId(null); - onShowGuardrailInfo?.(false); - onGuardrailUpdated(); - }; + setEditModalVisible(false) + setSelectedGuardrail(null) + onGuardrailUpdated() + } const columns: ColumnDef[] = [ { @@ -114,11 +76,11 @@ const GuardrailTable: React.FC = ({ accessorKey: "guardrail_id", cell: (info: any) => ( - @@ -129,115 +91,108 @@ const GuardrailTable: React.FC = ({ header: "Name", accessorKey: "guardrail_name", cell: ({ row }) => { - const guardrail = row.original; + const guardrail = row.original return ( - - {guardrail.guardrail_name || "-"} - + {guardrail.guardrail_name || "-"} - ); + ) }, }, { header: "Provider", accessorKey: "litellm_params.guardrail", cell: ({ row }) => { - const guardrail = row.original; - const { logo, displayName } = getGuardrailLogoAndName(guardrail.litellm_params.guardrail); + const guardrail = row.original + const { logo, displayName } = getGuardrailLogoAndName(guardrail.litellm_params.guardrail) return (
{logo && ( - {`${displayName} { // Hide broken image - (e.target as HTMLImageElement).style.display = 'none'; + ;(e.target as HTMLImageElement).style.display = "none" }} /> )} {displayName}
- ); + ) }, }, { header: "Mode", accessorKey: "litellm_params.mode", cell: ({ row }) => { - const guardrail = row.original; - return ( - - {guardrail.litellm_params.mode} - - ); + const guardrail = row.original + return {guardrail.litellm_params.mode} }, }, { header: "Default On", accessorKey: "litellm_params.default_on", cell: ({ row }) => { - const guardrail = row.original; + const guardrail = row.original return ( - {guardrail.litellm_params?.default_on ? "Default On" : "Default Off"} - ); + ) }, }, { header: "Created At", accessorKey: "created_at", cell: ({ row }) => { - const guardrail = row.original; + const guardrail = row.original return ( - - {formatDate(guardrail.created_at)} - + {formatDate(guardrail.created_at)} - ); + ) }, }, { header: "Updated At", accessorKey: "updated_at", cell: ({ row }) => { - const guardrail = row.original; + const guardrail = row.original return ( - - {formatDate(guardrail.updated_at)} - + {formatDate(guardrail.updated_at)} - ); + ) }, }, { id: "actions", header: "", cell: ({ row }) => { - const guardrail = row.original; + const guardrail = row.original return (
guardrail.guardrail_id && onDeleteClick(guardrail.guardrail_id, guardrail.guardrail_name || 'Unnamed Guardrail')} + onClick={() => + guardrail.guardrail_id && + onDeleteClick(guardrail.guardrail_id, guardrail.guardrail_name || "Unnamed Guardrail") + } className="cursor-pointer hover:text-red-500" tooltip="Delete guardrail" />
- ); + ) }, }, - ]; + ] const table = useReactTable({ data: guardrailsList, @@ -249,19 +204,7 @@ const GuardrailTable: React.FC = ({ getCoreRowModel: getCoreRowModel(), getSortedRowModel: getSortedRowModel(), enableSorting: true, - }); - - // If showing guardrail info, render the GuardrailInfoView - if (showGuardrailInfo && selectedGuardrailId) { - return ( - - ); - } + }) return (
@@ -274,27 +217,20 @@ const GuardrailTable: React.FC = ({
- {header.isPlaceholder ? null : ( - flexRender( - header.column.columnDef.header, - header.getContext() - ) - )} + {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
- {header.id !== 'actions' && ( + {header.id !== "actions" && (
{header.column.getIsSorted() ? ( { asc: , - desc: + desc: , }[header.column.getIsSorted() as string] ) : ( @@ -323,9 +259,9 @@ const GuardrailTable: React.FC = ({ {flexRender(cell.column.columnDef.cell, cell.getContext())} @@ -353,21 +289,22 @@ const GuardrailTable: React.FC = ({ onClose={() => setEditModalVisible(false)} accessToken={accessToken} onSuccess={handleEditSuccess} - guardrailId={selectedGuardrail.guardrail_id || ''} + guardrailId={selectedGuardrail.guardrail_id || ""} initialValues={{ - guardrail_name: selectedGuardrail.guardrail_name || '', - provider: Object.keys(guardrail_provider_map).find( - key => guardrail_provider_map[key] === selectedGuardrail?.litellm_params.guardrail - ) || '', + guardrail_name: selectedGuardrail.guardrail_name || "", + provider: + Object.keys(guardrail_provider_map).find( + (key) => guardrail_provider_map[key] === selectedGuardrail?.litellm_params.guardrail, + ) || "", mode: selectedGuardrail.litellm_params.mode, default_on: selectedGuardrail.litellm_params.default_on, pii_entities_config: selectedGuardrail.litellm_params.pii_entities_config, - ...selectedGuardrail.guardrail_info + ...selectedGuardrail.guardrail_info, }} /> )}
- ); -}; + ) +} -export default GuardrailTable; \ No newline at end of file +export default GuardrailTable diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx index e998b1a0eea..616a62cd1c8 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx +++ b/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx @@ -1,88 +1,83 @@ -import React, { useState } from "react"; -import { - Modal, - Tooltip, - Form, - Select, - message, - Button as AntdButton, - Input, -} from "antd"; -import { InfoCircleOutlined } from "@ant-design/icons"; -import { Button, TextInput } from "@tremor/react"; -import { createMCPServer } from "../networking"; -import { MCPServer, MCPServerCostInfo } from "./types"; -import MCPServerCostConfig from "./mcp_server_cost_config"; -import MCPConnectionStatus from "./mcp_connection_status"; -import StdioConfiguration from "./StdioConfiguration"; -import { isAdminRole } from "@/utils/roles"; +import React, { useState } from "react" +import { Modal, Tooltip, Form, Select, message, Button as AntdButton, Input } from "antd" +import { InfoCircleOutlined } from "@ant-design/icons" +import { Button, TextInput } from "@tremor/react" +import { createMCPServer } from "../networking" +import { MCPServer, MCPServerCostInfo } from "./types" +import MCPServerCostConfig from "./mcp_server_cost_config" +import MCPConnectionStatus from "./mcp_connection_status" +import StdioConfiguration from "./StdioConfiguration" +import { isAdminRole } from "@/utils/roles" - -const asset_logos_folder = '../ui/assets/logos/'; -export const mcpLogoImg = `${asset_logos_folder}mcp_logo.png`; +const asset_logos_folder = "../ui/assets/logos/" +export const mcpLogoImg = `${asset_logos_folder}mcp_logo.png` interface CreateMCPServerProps { - userRole: string; - accessToken: string | null; - onCreateSuccess: (newMcpServer: MCPServer) => void; + userRole: string + accessToken: string | null + onCreateSuccess: (newMcpServer: MCPServer) => void + isModalVisible: boolean + setModalVisible: (visible: boolean) => void } const CreateMCPServer: React.FC = ({ userRole, accessToken, onCreateSuccess, + isModalVisible, + setModalVisible, }) => { - const [form] = Form.useForm(); - const [isLoading, setIsLoading] = useState(false); - const [costConfig, setCostConfig] = useState({}); - const [mcpAccessGroups, setMcpAccessGroups] = useState([]); - const [formValues, setFormValues] = useState>({}); - const [tools, setTools] = useState([]); - const [transportType, setTransportType] = useState('sse'); + const [form] = Form.useForm() + const [isLoading, setIsLoading] = useState(false) + const [costConfig, setCostConfig] = useState({}) + const [mcpAccessGroups, setMcpAccessGroups] = useState([]) + const [formValues, setFormValues] = useState>({}) + const [tools, setTools] = useState([]) + const [transportType, setTransportType] = useState("sse") const handleCreate = async (formValues: Record) => { - setIsLoading(true); + setIsLoading(true) try { // Transform access groups into objects with name property - + const accessGroups = formValues.mcp_access_groups // Process stdio configuration if present - let stdioFields = {}; - if (formValues.stdio_config && transportType === 'stdio') { + let stdioFields = {} + if (formValues.stdio_config && transportType === "stdio") { try { - const stdioConfig = JSON.parse(formValues.stdio_config); - + const stdioConfig = JSON.parse(formValues.stdio_config) + // Handle both formats: // 1. Full mcpServers structure: {"mcpServers": {"server-name": {...}}} // 2. Direct config: {"command": "...", "args": [...], "env": {...}} - - let actualConfig = stdioConfig; - + + let actualConfig = stdioConfig + // If it's the full mcpServers structure, extract the first server config - if (stdioConfig.mcpServers && typeof stdioConfig.mcpServers === 'object') { - const serverNames = Object.keys(stdioConfig.mcpServers); + if (stdioConfig.mcpServers && typeof stdioConfig.mcpServers === "object") { + const serverNames = Object.keys(stdioConfig.mcpServers) if (serverNames.length > 0) { - const firstServerName = serverNames[0]; - actualConfig = stdioConfig.mcpServers[firstServerName]; - + const firstServerName = serverNames[0] + actualConfig = stdioConfig.mcpServers[firstServerName] + // If no alias is provided, use the server name from the JSON if (!formValues.alias) { - formValues.alias = firstServerName.replace(/-/g, '_'); // Replace hyphens with underscores + formValues.alias = firstServerName.replace(/-/g, "_") // Replace hyphens with underscores } } } - + stdioFields = { command: actualConfig.command, args: actualConfig.args, - env: actualConfig.env - }; - - console.log('Parsed stdio config:', stdioFields); + env: actualConfig.env, + } + + console.log("Parsed stdio config:", stdioFields) } catch (error) { - message.error("Invalid JSON in stdio configuration"); - return; + message.error("Invalid JSON in stdio configuration") + return } } @@ -97,305 +92,252 @@ const CreateMCPServer: React.FC = ({ description: formValues.description, mcp_server_cost_info: Object.keys(costConfig).length > 0 ? costConfig : null, }, - mcp_access_groups: accessGroups - }; + mcp_access_groups: accessGroups, + } - console.log(`Payload: ${JSON.stringify(payload)}`); + console.log(`Payload: ${JSON.stringify(payload)}`) if (accessToken != null) { - const response = await createMCPServer( - accessToken, - payload - ); + const response = await createMCPServer(accessToken, payload) - message.success("MCP Server created successfully"); - form.resetFields(); - setCostConfig({}); - setTools([]); - setModalVisible(false); - onCreateSuccess(response); + message.success("MCP Server created successfully") + form.resetFields() + setCostConfig({}) + setTools([]) + setModalVisible(false) + onCreateSuccess(response) } } catch (error) { - message.error("Error creating MCP Server: " + error, 20); + message.error("Error creating MCP Server: " + error, 20) } finally { - setIsLoading(false); + setIsLoading(false) } - }; + } // state - const [isModalVisible, setModalVisible] = useState(false); - const handleCancel = () => { - form.resetFields(); - setCostConfig({}); - setTools([]); - setModalVisible(false); - }; + form.resetFields() + setCostConfig({}) + setTools([]) + setModalVisible(false) + } const handleTransportChange = (value: string) => { - setTransportType(value); + setTransportType(value) // Clear fields that are not relevant for the selected transport - if (value === 'stdio') { - form.setFieldsValue({ url: undefined, auth_type: undefined }); + if (value === "stdio") { + form.setFieldsValue({ url: undefined, auth_type: undefined }) } else { - form.setFieldsValue({ command: undefined, args: undefined, env: undefined }); + form.setFieldsValue({ command: undefined, args: undefined, env: undefined }) } - }; + } // rendering if (!isAdminRole(userRole)) { - return null; + return null } return ( -
- - - - MCP Logo -

Add New MCP Server

-
- } - open={isModalVisible} - width={1000} - onCancel={handleCancel} - footer={null} - className="top-8" - styles={{ - body: { padding: '24px' }, - header: { padding: '24px 24px 0 24px', border: 'none' }, - }} - > -
-
setFormValues(allValues)} - layout="vertical" - className="space-y-6" - > -
- - MCP Server Name - - - - - } - name="alias" - rules={[ - { required: false, message: "Please enter a server name" }, - { - validator: (_, value) => - value && value.includes('-') - ? Promise.reject("Server name cannot contain '-' (hyphen). Please use '_' (underscore) instead.") - : Promise.resolve(), - }, - ]} - > - - - - Description} - name="description" - rules={[ - { - required: false, - message: "Please enter a server description", - }, - ]} - > - - - - - Transport Type - - } - name="transport" - rules={[{ required: true, message: "Please select a transport type" }]} - > - - - - {/* URL field - only show for HTTP and SSE */} - {transportType !== 'stdio' && ( - - MCP Server URL - - } - name="url" - rules={[ - { required: true, message: "Please enter a server URL" }, - { type: 'url', message: "Please enter a valid URL" } - ]} - > - - - )} - - {/* Authentication - only show for HTTP and SSE */} - {transportType !== 'stdio' && ( - - Authentication - - } - name="auth_type" - rules={[{ required: true, message: "Please select an auth type" }]} - > - - - )} - - {/* Stdio Configuration - only show for stdio transport */} - - - - MCP Version - - - - - } - name="spec_version" - rules={[ - { required: true, message: "Please select a spec version" }, - ]} - > - - - - - MCP Access Groups - - - - - } - name="mcp_access_groups" - className="mb-4" - > - + HTTP + Server-Sent Events (SSE) + Standard Input/Output (stdio) + + -export default CreateMCPServer; \ No newline at end of file + {/* URL field - only show for HTTP and SSE */} + {transportType !== "stdio" && ( + MCP Server URL} + name="url" + rules={[ + { required: true, message: "Please enter a server URL" }, + { type: "url", message: "Please enter a valid URL" }, + ]} + > + + + )} + + {/* Authentication - only show for HTTP and SSE */} + {transportType !== "stdio" && ( + Authentication} + name="auth_type" + rules={[{ required: true, message: "Please select an auth type" }]} + > + + + )} + + {/* Stdio Configuration - only show for stdio transport */} + + + + MCP Version + + + + + } + name="spec_version" + rules={[{ required: true, message: "Please select a spec version" }]} + > + + + + + MCP Access Groups + + + + + } + name="mcp_access_groups" + className="mb-4" + > + + + + {premiumUser ? ( +
+ - - - Select Customer Name - - - + + Select Customer Name + + + ); + })} + - - Select Team - - - -
- ): ( -
- {/* ... existing non-premium user content ... */} - - Select Team - - - -
- ) - } -
+ Select Team + + +
+ ) : ( +
+ {/* ... existing non-premium user content ... */} + Select Team + + +
+ )} +
); const customTooltip = (props: any) => { @@ -1009,7 +955,6 @@ const ModelDashboard: React.FC = ({ ); }; - const handleOk = () => { form .validateFields() @@ -1039,8 +984,8 @@ const ModelDashboard: React.FC = ({ if (selectedTeamId) { return (
- setSelectedTeamId(null)} accessToken={accessToken} is_team_admin={userRole === "Admin"} @@ -1054,674 +999,814 @@ const ModelDashboard: React.FC = ({ } return ( -
- {selectedModelId ? ( - { - setSelectedModelId(null); - setEditModel(false); - }} - modelData={modelData.data.find((model: any) => model.model_info.id === selectedModelId)} - accessToken={accessToken} - userID={userID} - userRole={userRole} - setEditModalVisible={setEditModalVisible} - setSelectedModel={setSelectedModel} - onModelUpdate={(updatedModel) => { - // Update the model in the modelData.data array - const updatedModelData = { - ...modelData, - data: modelData.data.map((model: any) => - model.model_info.id === updatedModel.model_info.id ? updatedModel : model - ) - }; - setModelData(updatedModelData); - // Trigger a refresh to update UI - handleRefreshClick(); - }} - modelAccessGroups={availableModelAccessGroups} - /> - ) : ( - - - -
- {all_admin_roles.includes(userRole) ? All Models : Your Models} - Add Model - {all_admin_roles.includes(userRole) && LLM Credentials} - {all_admin_roles.includes(userRole) && Pass-Through Endpoints} - {all_admin_roles.includes(userRole) && - Health Status - } - {all_admin_roles.includes(userRole) && Model Analytics} - {all_admin_roles.includes(userRole) && Model Retry Settings} - -
+
+ +
+ {all_admin_roles.includes(userRole || "") && ( + + )} + {selectedModelId ? ( + { + setSelectedModelId(null); + setEditModel(false); + }} + modelData={modelData.data.find( + (model: any) => model.model_info.id === selectedModelId + )} + accessToken={accessToken} + userID={userID} + userRole={userRole} + setEditModalVisible={setEditModalVisible} + setSelectedModel={setSelectedModel} + onModelUpdate={(updatedModel) => { + // Update the model in the modelData.data array + const updatedModelData = { + ...modelData, + data: modelData.data.map((model: any) => + model.model_info.id === updatedModel.model_info.id + ? updatedModel + : model + ), + }; + setModelData(updatedModelData); + // Trigger a refresh to update UI + handleRefreshClick(); + }} + modelAccessGroups={availableModelAccessGroups} + /> + ) : ( + + +
+ {all_admin_roles.includes(userRole) ? ( + All Models + ) : ( + Your Models + )} + Add Model + {all_admin_roles.includes(userRole) && ( + LLM Credentials + )} + {all_admin_roles.includes(userRole) && ( + Pass-Through Endpoints + )} + {all_admin_roles.includes(userRole) && ( + Health Status + )} + {all_admin_roles.includes(userRole) && ( + Model Analytics + )} + {all_admin_roles.includes(userRole) && ( + Model Retry Settings + )} +
-
- {lastRefreshed && Last Refreshed: {lastRefreshed}} - -
-
- - - -
-
-
- Model Management - {!all_admin_roles.includes(userRole) ? ( - - Add models for teams you are an admin for. - - ) : ( - - Add and manage models for the proxy - - )} -
-
- -
-
-
- {/* Current Team Selector - Prominent */} -
-
-
- Current Team: - -
- { - modelViewMode === "current_team" && ( - -
- -
- {currentTeam === "personal" ? ( - - To access these models: Create a Virtual Key without selecting a team on the {" "} - - Virtual Keys page - - - ) : ( - - To access these models: Create a Virtual Key and select Team as "{currentTeam}" on the {" "} - - Virtual Keys page - - - )} -
-
- ) - } -
- - - {/* Model View Mode Toggle - Also prominent */} -
- View: - -
- -
- - - {/* Other Filters */} -
-
- {/* Model Name Filter */} -
- Filter by Public Model Name: - -
- -
- Filter by Model Access Group: - -
-
- -
- - {/* Results Count */} -
- - Showing {modelData && modelData.data.length > 0 ? - modelData.data.filter((model: any) => { - const modelNameMatch = selectedModelGroup === "all" || model.model_name === selectedModelGroup || !selectedModelGroup; - const accessGroupMatch = selectedModelAccessGroupFilter === "all" || model.model_info["access_groups"]?.includes(selectedModelAccessGroupFilter) || !selectedModelAccessGroupFilter; - let teamAccessMatch = true; - if (modelViewMode === 'current_team') { - if (currentTeam === 'personal') { - teamAccessMatch = model.model_info?.direct_access === true; - } else { - teamAccessMatch = model.model_info?.access_via_team_ids?.includes(currentTeam) === true; - } - } - - return modelNameMatch && accessGroupMatch && teamAccessMatch; - }).length : 0} results - +
+ {lastRefreshed && ( + Last Refreshed: {lastRefreshed} + )} + +
+ + + + +
+
+
+ Model Management + {!all_admin_roles.includes(userRole) ? ( + + Add models for teams you are an admin for. + + ) : ( + + Add and manage models for the proxy + + )}
-
- { - // Model name filter - const modelNameMatch = selectedModelGroup === "all" || model.model_name === selectedModelGroup || !selectedModelGroup; - - // Model access group filter - const accessGroupMatch = selectedModelAccessGroupFilter === "all" || model.model_info["access_groups"]?.includes(selectedModelAccessGroupFilter) || !selectedModelAccessGroupFilter; - - // Team access filter based on current team and view mode - let teamAccessMatch = true; - if (modelViewMode === 'current_team') { - if (currentTeam === 'personal') { - // Show only models with direct access - teamAccessMatch = model.model_info?.direct_access === true; - } else { - // Show only models accessible by the current team - teamAccessMatch = model.model_info?.access_via_team_ids?.includes(currentTeam) === true; +
+
+
+ {/* Current Team Selector - Prominent */} +
+
+
+ + Current Team: + + +
+ {modelViewMode === "current_team" && ( +
+ +
+ {currentTeam === "personal" ? ( + + To access these models: Create a + Virtual Key without selecting a team + on the{" "} + + Virtual Keys page + + + ) : ( + + To access these models: Create a + Virtual Key and select Team as " + {currentTeam}" on the{" "} + + Virtual Keys page + + + )} +
+
+ )} +
+ + {/* Model View Mode Toggle - Also prominent */} +
+ + View: + + +
+
+ + {/* Other Filters */} +
+
+ {/* Model Name Filter */} +
+ Filter by Public Model Name: + +
+ +
+ Filter by Model Access Group: + +
+
+
+ + {/* Results Count */} +
+ + Showing{" "} + {modelData && modelData.data.length > 0 + ? modelData.data.filter((model: any) => { + const modelNameMatch = + selectedModelGroup === "all" || + model.model_name === + selectedModelGroup || + !selectedModelGroup; + const accessGroupMatch = + selectedModelAccessGroupFilter === + "all" || + model.model_info[ + "access_groups" + ]?.includes( + selectedModelAccessGroupFilter + ) || + !selectedModelAccessGroupFilter; + let teamAccessMatch = true; + if (modelViewMode === "current_team") { + if (currentTeam === "personal") { + teamAccessMatch = + model.model_info?.direct_access === + true; + } else { + teamAccessMatch = + model.model_info?.access_via_team_ids?.includes( + currentTeam + ) === true; + } + } + + return ( + modelNameMatch && + accessGroupMatch && + teamAccessMatch + ); + }).length + : 0}{" "} + results + +
+
+
+ + { + // Model name filter + const modelNameMatch = + selectedModelGroup === "all" || + model.model_name === selectedModelGroup || + !selectedModelGroup; + + // Model access group filter + const accessGroupMatch = + selectedModelAccessGroupFilter === "all" || + model.model_info["access_groups"]?.includes( + selectedModelAccessGroupFilter + ) || + !selectedModelAccessGroupFilter; + + // Team access filter based on current team and view mode + let teamAccessMatch = true; + if (modelViewMode === "current_team") { + if (currentTeam === "personal") { + // Show only models with direct access + teamAccessMatch = + model.model_info?.direct_access === true; + } else { + // Show only models accessible by the current team + teamAccessMatch = + model.model_info?.access_via_team_ids?.includes( + currentTeam + ) === true; + } } - } - // For 'all' mode, show all models (teamAccessMatch remains true) - - return modelNameMatch && accessGroupMatch && teamAccessMatch; - } - )} - isLoading={false} - table={tableRef} - /> -
-
- - - - - - - - - - - - - - - - -
- { - setDateValue(value); - updateModelMetrics( - selectedModelGroup, - value.from, - value.to - ); - }} + // For 'all' mode, show all models (teamAccessMatch remains true) + + return ( + modelNameMatch && + accessGroupMatch && + teamAccessMatch + ); + })} + isLoading={false} + table={tableRef} + /> + + + + + + - - - Select Model Group - - {group} - - ))} - - - - - - - - - - - - - - - - - Avg. Latency per Token - Time to first token - - - -

(seconds/token)

- - average Latency for successfull requests divided by - the total tokens - - {modelMetrics && modelMetricsCategories && ( - - )} -
- - ( + + updateModelMetrics( + group, + dateValue.from, + dateValue.to + ) } - customTooltip={customTooltip} - premiumUser={premiumUser} - /> - -
-
-
- - - -
- - - Deployment - Success Responses - - Slow Responses

Success Responses taking 600+s

-
-
-
- - {slowResponsesData.map((metric, idx) => ( - - {metric.api_base} - {metric.total_count} - {metric.slow_count} - + > + {group} + ))} - -
- - - - - - - All Exceptions for {selectedModelGroup} - - - - - - - - - - All Up Rate Limit Errors (429) for {selectedModelGroup} - - - Num Rate Limit Errors { (globalExceptionData.sum_num_rate_limit_exceptions)} - console.log(v)} - /> + - - - - - + + + + - - + + + + + + Avg. Latency per Token + Time to first token + + + +

+ {" "} + (seconds/token) +

+ + average Latency for successfull requests divided + by the total tokens + + {modelMetrics && modelMetricsCategories && ( + + )} +
+ + + +
+
+
+ + + + + + + Deployment + + Success Responses + + + Slow Responses{" "} +

Success Responses taking 600+s

+
+
+
+ + {slowResponsesData.map((metric, idx) => ( + + {metric.api_base} + {metric.total_count} + {metric.slow_count} + + ))} + +
+
+ +
+ + + All Exceptions for {selectedModelGroup} - { - premiumUser ? ( - <> - {globalExceptionPerDeployment.map((globalActivity, index) => ( - - {globalActivity.api_base ? globalActivity.api_base : "Unknown API Base"} + + + + + + + + All Up Rate Limit Errors (429) for {selectedModelGroup} + - Num Rate Limit Errors (429) {(globalActivity.sum_num_rate_limit_exceptions)} + + Num Rate Limit Errors{" "} + {globalExceptionData.sum_num_rate_limit_exceptions} + console.log(v)} /> - + - ))} - - ) : - <> - {globalExceptionPerDeployment && globalExceptionPerDeployment.length > 0 && - globalExceptionPerDeployment.slice(0, 1).map((globalActivity, index) => ( - - ✨ Rate Limit Errors by Deployment -

Upgrade to see exceptions for all deployments

- - - {globalActivity.api_base} - - - - Num Rate Limit Errors {(globalActivity.sum_num_rate_limit_exceptions)} - - console.log(v)} - /> - - - - - -
- ))} - - } -
- - - -
- Filter by Public Model Name - -
- - Retry Policy for {selectedModelGroup} - - How many retries should be attempted based on the Exception - - {retry_policy_map && ( - - - {Object.entries(retry_policy_map).map( - ([exceptionType, retryPolicyKey], idx) => { - let retryCount = - modelGroupRetryPolicy?.[selectedModelGroup!]?.[ - retryPolicyKey - ]; - if (retryCount == null) { - retryCount = defaultRetry; - } - - return ( - - - + + Num Rate Limit Errors (429){" "} + { + globalActivity.sum_num_rate_limit_exceptions } - ); - }} - /> - - - ); - } + + console.log(v)} + /> + + + + ) + )} + + ) : ( + <> + {globalExceptionPerDeployment && + globalExceptionPerDeployment.length > 0 && + globalExceptionPerDeployment + .slice(0, 1) + .map((globalActivity, index) => ( + + + ✨ Rate Limit Errors by Deployment + +

+ Upgrade to see exceptions for all deployments +

+ + + {globalActivity.api_base} + +
+ + Num Rate Limit Errors{" "} + { + globalActivity.sum_num_rate_limit_exceptions + } + + console.log(v)} + /> + + + + + ))} + )} - -
- {exceptionType} - - { - setModelGroupRetryPolicy( - (prevModelGroupRetryPolicy) => { - const prevRetryPolicy = - prevModelGroupRetryPolicy?.[ - selectedModelGroup! - ] ?? {}; - return { - ...(prevModelGroupRetryPolicy ?? {}), - [selectedModelGroup!]: { - ...prevRetryPolicy, - [retryPolicyKey!]: value, - }, - } as RetryPolicyObject; + {premiumUser ? ( + <> + {globalExceptionPerDeployment.map( + (globalActivity, index) => ( + + + {globalActivity.api_base + ? globalActivity.api_base + : "Unknown API Base"} + + +
- )} - -
- - - - )} +
+ + +
+ Filter by Public Model Name -
+ +
+ + Retry Policy for {selectedModelGroup} + + How many retries should be attempted based on the Exception + + {retry_policy_map && ( + + + {Object.entries(retry_policy_map).map( + ([exceptionType, retryPolicyKey], idx) => { + let retryCount = + modelGroupRetryPolicy?.[selectedModelGroup!]?.[ + retryPolicyKey + ]; + if (retryCount == null) { + retryCount = defaultRetry; + } + + return ( + + + + + ); + } + )} + +
+ {exceptionType} + + { + setModelGroupRetryPolicy( + (prevModelGroupRetryPolicy) => { + const prevRetryPolicy = + prevModelGroupRetryPolicy?.[ + selectedModelGroup! + ] ?? {}; + return { + ...(prevModelGroupRetryPolicy ?? + {}), + [selectedModelGroup!]: { + ...prevRetryPolicy, + [retryPolicyKey!]: value, + }, + } as RetryPolicyObject; + } + ); + }} + /> +
+ )} + + + + + )} + + +
); }; diff --git a/ui/litellm-dashboard/src/components/model_info_view.tsx b/ui/litellm-dashboard/src/components/model_info_view.tsx index 12ee6854a89..cf82d7df0de 100644 --- a/ui/litellm-dashboard/src/components/model_info_view.tsx +++ b/ui/litellm-dashboard/src/components/model_info_view.tsx @@ -15,7 +15,16 @@ import { } from "@tremor/react"; import NumericalInput from "./shared/numerical_input"; import { ArrowLeftIcon, TrashIcon, KeyIcon } from "@heroicons/react/outline"; -import { modelDeleteCall, modelUpdateCall, CredentialItem, credentialGetCall, credentialCreateCall, modelInfoCall, modelInfoV1Call, modelPatchUpdateCall } from "./networking"; +import { + modelDeleteCall, + modelUpdateCall, + CredentialItem, + credentialGetCall, + credentialCreateCall, + modelInfoCall, + modelInfoV1Call, + modelPatchUpdateCall, +} from "./networking"; import { Button, Form, Input, InputNumber, message, Select, Modal } from "antd"; import EditModelModal from "./edit_model/edit_model_modal"; import { handleEditModelSubmit } from "./edit_model/edit_model_modal"; @@ -39,10 +48,10 @@ interface ModelInfoViewProps { modelAccessGroups: string[] | null; } -export default function ModelInfoView({ - modelId, - onClose, - modelData, +export default function ModelInfoView({ + modelId, + onClose, + modelData, accessToken, userID, userRole, @@ -50,7 +59,7 @@ export default function ModelInfoView({ setEditModalVisible, setSelectedModel, onModelUpdate, - modelAccessGroups + modelAccessGroups, }: ModelInfoViewProps) { const [form] = Form.useForm(); const [localModelData, setLocalModelData] = useState(null); @@ -59,30 +68,40 @@ export default function ModelInfoView({ const [isDirty, setIsDirty] = useState(false); const [isSaving, setIsSaving] = useState(false); const [isEditing, setIsEditing] = useState(false); - const [existingCredential, setExistingCredential] = useState(null); + const [existingCredential, setExistingCredential] = + useState(null); const [showCacheControl, setShowCacheControl] = useState(false); - const canEditModel = userRole === "Admin" || modelData.model_info.created_by === userID; + const canEditModel = + userRole === "Admin" || modelData.model_info.created_by === userID; const isAdmin = userRole === "Admin"; - const usingExistingCredential = modelData.litellm_params?.litellm_credential_name != null && modelData.litellm_params?.litellm_credential_name != undefined; + const usingExistingCredential = + modelData.litellm_params?.litellm_credential_name != null && + modelData.litellm_params?.litellm_credential_name != undefined; console.log("usingExistingCredential, ", usingExistingCredential); - console.log("modelData.litellm_params.litellm_credential_name, ", modelData.litellm_params.litellm_credential_name); - + console.log( + "modelData.litellm_params.litellm_credential_name, ", + modelData.litellm_params.litellm_credential_name + ); useEffect(() => { const getExistingCredential = async () => { console.log("accessToken, ", accessToken); if (!accessToken) return; if (usingExistingCredential) return; - let existingCredentialResponse = await credentialGetCall(accessToken, null, modelId); + let existingCredentialResponse = await credentialGetCall( + accessToken, + null, + modelId + ); console.log("existingCredentialResponse, ", existingCredentialResponse); setExistingCredential({ credential_name: existingCredentialResponse["credential_name"], - credential_values: existingCredentialResponse["credential_values"], - credential_info: existingCredentialResponse["credential_info"] + credential_values: existingCredentialResponse["credential_values"], + credential_info: existingCredentialResponse["credential_info"], }); - } + }; const getModelInfo = async () => { if (!accessToken) return; @@ -90,12 +109,12 @@ export default function ModelInfoView({ console.log("modelInfoResponse, ", modelInfoResponse); let specificModelData = modelInfoResponse.data[0]; setLocalModelData(specificModelData); - + // Check if cache control is enabled if (specificModelData?.litellm_params?.cache_control_injection_points) { setShowCacheControl(true); } - } + }; getExistingCredential(); getModelInfo(); }, [accessToken, modelId]); @@ -107,14 +126,17 @@ export default function ModelInfoView({ credential_name: values.credential_name, model_id: modelId, credential_info: { - "custom_llm_provider": localModelData.litellm_params?.custom_llm_provider, - } - } + custom_llm_provider: localModelData.litellm_params?.custom_llm_provider, + }, + }; message.info("Storing credential.."); - let credentialResponse = await credentialCreateCall(accessToken, credentialItem); + let credentialResponse = await credentialCreateCall( + accessToken, + credentialItem + ); console.log("credentialResponse, ", credentialResponse); message.success("Credential stored successfully"); - } + }; const handleModelUpdate = async (values: any) => { try { @@ -122,7 +144,7 @@ export default function ModelInfoView({ setIsSaving(true); console.log("values.model_name, ", values.model_name); - + let updatedLitellmParams = { ...localModelData.litellm_params, model: values.litellm_model_name, @@ -137,10 +159,14 @@ export default function ModelInfoView({ input_cost_per_token: values.input_cost / 1_000_000, output_cost_per_token: values.output_cost / 1_000_000, }; - + // Handle cache control settings - if (values.cache_control && values.cache_control_injection_points?.length > 0) { - updatedLitellmParams.cache_control_injection_points = values.cache_control_injection_points; + if ( + values.cache_control && + values.cache_control_injection_points?.length > 0 + ) { + updatedLitellmParams.cache_control_injection_points = + values.cache_control_injection_points; } else { delete updatedLitellmParams.cache_control_injection_points; } @@ -148,35 +174,37 @@ export default function ModelInfoView({ // Parse the model_info from the form values let updatedModelInfo; try { - updatedModelInfo = values.model_info ? JSON.parse(values.model_info) : modelData.model_info; + updatedModelInfo = values.model_info + ? JSON.parse(values.model_info) + : modelData.model_info; // Update access_groups from the form if (values.model_access_group) { updatedModelInfo = { ...updatedModelInfo, - access_groups: values.model_access_group + access_groups: values.model_access_group, }; } } catch (e) { message.error("Invalid JSON in Model Info"); return; } - + const updateData = { model_name: values.model_name, litellm_params: updatedLitellmParams, - model_info: updatedModelInfo + model_info: updatedModelInfo, }; await modelPatchUpdateCall(accessToken, updateData, modelId); - + const updatedModelData = { ...localModelData, model_name: values.model_name, litellm_model_name: values.litellm_model_name, litellm_params: updatedLitellmParams, - model_info: updatedModelInfo + model_info: updatedModelInfo, }; - + setLocalModelData(updatedModelData); if (onModelUpdate) { @@ -197,13 +225,14 @@ export default function ModelInfoView({ if (!modelData) { return (
- + Model not found
); @@ -214,14 +243,14 @@ export default function ModelInfoView({ if (!accessToken) return; await modelDeleteCall(accessToken, modelId); message.success("Model deleted successfully"); - + if (onModelUpdate) { - onModelUpdate({ - deleted: true, - model_info: { id: modelId } + onModelUpdate({ + deleted: true, + model_info: { id: modelId }, }); } - + onClose(); } catch (error) { console.error("Error deleting the model:", error); @@ -229,20 +258,22 @@ export default function ModelInfoView({ } }; - return (
- + Public Model Name: {getDisplayModelName(modelData)} - {modelData.model_info.id} + + {modelData.model_info.id} +
{isAdmin && ( @@ -277,7 +308,12 @@ export default function ModelInfoView({ {/* Overview Grid */} - + Provider
@@ -291,9 +327,11 @@ export default function ModelInfoView({ const target = e.target as HTMLImageElement; const parent = target.parentElement; if (parent) { - const fallbackDiv = document.createElement('div'); - fallbackDiv.className = 'w-4 h-4 rounded-full bg-gray-200 flex items-center justify-center text-xs'; - fallbackDiv.textContent = modelData.provider?.charAt(0) || '-'; + const fallbackDiv = document.createElement("div"); + fallbackDiv.className = + "w-4 h-4 rounded-full bg-gray-200 flex items-center justify-center text-xs"; + fallbackDiv.textContent = + modelData.provider?.charAt(0) || "-"; parent.replaceChild(fallbackDiv, target); } }} @@ -320,20 +358,43 @@ export default function ModelInfoView({ {/* Audit info shown as a subtle banner below the overview */}
- - + + - Created At {modelData.model_info.created_at - ? new Date(modelData.model_info.created_at).toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: 'numeric' + Created At{" "} + {modelData.model_info.created_at + ? new Date( + modelData.model_info.created_at + ).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", }) : "Not Set"}
- - + + Created By {modelData.model_info.created_by || "Not Set"}
@@ -358,303 +419,397 @@ export default function ModelInfoView({ form={form} onFinish={handleModelUpdate} initialValues={{ - model_name: localModelData.model_name, - litellm_model_name: localModelData.litellm_model_name, - api_base: localModelData.litellm_params.api_base, - custom_llm_provider: localModelData.litellm_params.custom_llm_provider, - organization: localModelData.litellm_params.organization, - tpm: localModelData.litellm_params.tpm, - rpm: localModelData.litellm_params.rpm, - max_retries: localModelData.litellm_params.max_retries, - timeout: localModelData.litellm_params.timeout, - stream_timeout: localModelData.litellm_params.stream_timeout, - input_cost: localModelData.litellm_params.input_cost_per_token ? - (localModelData.litellm_params.input_cost_per_token * 1_000_000) : localModelData.model_info?.input_cost_per_token * 1_000_000 || null, - output_cost: localModelData.litellm_params?.output_cost_per_token ? - (localModelData.litellm_params.output_cost_per_token * 1_000_000) : localModelData.model_info?.output_cost_per_token * 1_000_000 || null, - cache_control: localModelData.litellm_params?.cache_control_injection_points ? true : false, - cache_control_injection_points: localModelData.litellm_params?.cache_control_injection_points || [], - model_access_group: Array.isArray(localModelData.model_info?.access_groups) ? localModelData.model_info.access_groups : [], - }} - layout="vertical" - onValuesChange={() => setIsDirty(true)} - > -
+ model_name: localModelData.model_name, + litellm_model_name: localModelData.litellm_model_name, + api_base: localModelData.litellm_params.api_base, + custom_llm_provider: + localModelData.litellm_params.custom_llm_provider, + organization: localModelData.litellm_params.organization, + tpm: localModelData.litellm_params.tpm, + rpm: localModelData.litellm_params.rpm, + max_retries: localModelData.litellm_params.max_retries, + timeout: localModelData.litellm_params.timeout, + stream_timeout: + localModelData.litellm_params.stream_timeout, + input_cost: localModelData.litellm_params + .input_cost_per_token + ? localModelData.litellm_params.input_cost_per_token * + 1_000_000 + : localModelData.model_info?.input_cost_per_token * + 1_000_000 || null, + output_cost: localModelData.litellm_params + ?.output_cost_per_token + ? localModelData.litellm_params.output_cost_per_token * + 1_000_000 + : localModelData.model_info?.output_cost_per_token * + 1_000_000 || null, + cache_control: localModelData.litellm_params + ?.cache_control_injection_points + ? true + : false, + cache_control_injection_points: + localModelData.litellm_params + ?.cache_control_injection_points || [], + model_access_group: Array.isArray( + localModelData.model_info?.access_groups + ) + ? localModelData.model_info.access_groups + : [], + }} + layout="vertical" + onValuesChange={() => setIsDirty(true)} + >
-
- Model Name - {isEditing ? ( - - - - ) : ( -
{localModelData.model_name}
- )} -
- -
- LiteLLM Model Name - {isEditing ? ( - - - - ) : ( -
{localModelData.litellm_model_name}
- )} -
- -
- Input Cost (per 1M tokens) - {isEditing ? ( - - - - ) : ( -
- {localModelData?.litellm_params?.input_cost_per_token - ? (localModelData.litellm_params?.input_cost_per_token * 1_000_000).toFixed(4) - : localModelData?.model_info?.input_cost_per_token ? (localModelData.model_info.input_cost_per_token * 1_000_000).toFixed(4) : null} -
- )} -
- -
- Output Cost (per 1M tokens) - {isEditing ? ( - - - - ) : ( -
- {localModelData?.litellm_params?.output_cost_per_token - ? (localModelData.litellm_params.output_cost_per_token * 1_000_000).toFixed(4) - : localModelData?.model_info?.output_cost_per_token ? (localModelData.model_info.output_cost_per_token * 1_000_000).toFixed(4) : null} -
- )} -
- -
- API Base - {isEditing ? ( - - - - ) : ( -
- {localModelData.litellm_params?.api_base || "Not Set"} -
- )} -
- -
- Custom LLM Provider - {isEditing ? ( - - - - ) : ( -
- {localModelData.litellm_params?.custom_llm_provider || "Not Set"} -
- )} -
- -
- Organization - {isEditing ? ( - - - - ) : ( -
- {localModelData.litellm_params?.organization || "Not Set"} -
- )} -
- -
- TPM (Tokens per Minute) - {isEditing ? ( - - - - ) : ( -
- {localModelData.litellm_params?.tpm || "Not Set"} -
- )} -
- -
- RPM (Requests per Minute) - {isEditing ? ( - - - - ) : ( -
- {localModelData.litellm_params?.rpm || "Not Set"} -
- )} -
- -
- Max Retries - {isEditing ? ( - - - - ) : ( -
- {localModelData.litellm_params?.max_retries || "Not Set"} -
- )} -
- -
- Timeout (seconds) - {isEditing ? ( - - - - ) : ( -
- {localModelData.litellm_params?.timeout || "Not Set"} -
- )} -
- -
- Stream Timeout (seconds) - {isEditing ? ( - - - - ) : ( -
- {localModelData.litellm_params?.stream_timeout || "Not Set"} -
- )} -
- -
- Model Access Groups - {isEditing ? ( - - ({ + value: group, + label: group, + }))} + /> + + ) : ( +
+ {localModelData.model_info?.access_groups ? ( + Array.isArray( + localModelData.model_info.access_groups + ) ? ( + localModelData.model_info.access_groups.length > + 0 ? ( +
+ {localModelData.model_info.access_groups.map( + (group: string, index: number) => ( + + {group} + + ) + )}
- ))} + ) : ( + "No groups assigned" + ) + ) : ( + localModelData.model_info.access_groups + ) + ) : ( + "Not Set" + )} +
+ )} +
+ + {/* Cache Control Section */} + {isEditing ? ( + + setShowCacheControl(checked) + } + /> + ) : ( +
+ Cache Control +
+ {localModelData.litellm_params + ?.cache_control_injection_points ? ( +
+

Enabled

+
+ {localModelData.litellm_params.cache_control_injection_points.map( + (point: any, i: number) => ( +
+ Location: {point.location}, + {point.role && ( + Role: {point.role} + )} + {point.index !== undefined && ( + Index: {point.index} + )} +
+ ) + )} +
-
- ) : ( - "Disabled" - )} + ) : ( + "Disabled" + )} +
+ )} + +
+ Model Info + {isEditing ? ( + + + + ) : ( +
+
+                              {JSON.stringify(
+                                localModelData.model_info,
+                                null,
+                                2
+                              )}
+                            
+
+ )} +
+
+ Team ID +
+ {modelData.model_info.team_id || "Not Set"} +
+
+
+ + {isEditing && ( +
+ { + form.resetFields(); + setIsDirty(false); + setIsEditing(false); + }} + > + Cancel + + form.submit()} + loading={isSaving} + > + Save Changes +
)} - -
- Model Info - {isEditing ? ( - - - - ) : ( -
-
-                            {JSON.stringify(localModelData.model_info, null, 2)}
-                          
-
- )} -
-
- Team ID -
- {modelData.model_info.team_id || "Not Set"} -
-
- - {isEditing && ( -
- { - form.resetFields(); - setIsDirty(false); - setIsEditing(false); - }} - > - Cancel - - form.submit()} - loading={isSaving} - > - Save Changes - -
- )} -
) : ( Loading... @@ -676,11 +831,19 @@ export default function ModelInfoView({ {isDeleteModalOpen && (
- ); -} \ No newline at end of file +} diff --git a/ui/litellm-dashboard/src/components/organization/organization_view.tsx b/ui/litellm-dashboard/src/components/organization/organization_view.tsx index 5efbd2ba91e..d9ce9005ae2 100644 --- a/ui/litellm-dashboard/src/components/organization/organization_view.tsx +++ b/ui/litellm-dashboard/src/components/organization/organization_view.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect } from "react" import { Card, Title, @@ -18,29 +18,37 @@ import { TableBody, TableCell, Button as TremorButton, - Icon -} from "@tremor/react"; -import NumericalInput from "../shared/numerical_input"; -import { Button, Form, Input, Select, message, Tooltip } from "antd"; -import { InfoCircleOutlined } from '@ant-design/icons'; -import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline"; -import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key"; -import { Member, Organization, organizationInfoCall, organizationMemberAddCall, organizationMemberUpdateCall, organizationMemberDeleteCall, organizationUpdateCall } from "../networking"; -import UserSearchModal from "../common_components/user_search_modal"; -import MemberModal from "../team/edit_membership"; -import ObjectPermissionsView from "../object_permissions_view"; -import VectorStoreSelector from "../vector_store_management/VectorStoreSelector"; -import MCPServerSelector from "../mcp_server_management/MCPServerSelector"; -import { formatNumberWithCommas } from "@/utils/dataUtils"; + Icon, +} from "@tremor/react" +import NumericalInput from "../shared/numerical_input" +import { Button, Form, Input, Select, message, Tooltip } from "antd" +import { InfoCircleOutlined } from "@ant-design/icons" +import { ArrowLeftIcon, PencilAltIcon, TrashIcon } from "@heroicons/react/outline" +import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key" +import { + Member, + Organization, + organizationInfoCall, + organizationMemberAddCall, + organizationMemberUpdateCall, + organizationMemberDeleteCall, + organizationUpdateCall, +} from "../networking" +import UserSearchModal from "../common_components/user_search_modal" +import MemberModal from "../team/edit_membership" +import ObjectPermissionsView from "../object_permissions_view" +import VectorStoreSelector from "../vector_store_management/VectorStoreSelector" +import MCPServerSelector from "../mcp_server_management/MCPServerSelector" +import { formatNumberWithCommas } from "@/utils/dataUtils" interface OrganizationInfoProps { - organizationId: string; - onClose: () => void; - accessToken: string | null; - is_org_admin: boolean; - is_proxy_admin: boolean; - userModels: string[]; - editOrg: boolean; + organizationId: string + onClose: () => void + accessToken: string | null + is_org_admin: boolean + is_proxy_admin: boolean + userModels: string[] + editOrg: boolean } const OrganizationInfoView: React.FC = ({ @@ -50,40 +58,40 @@ const OrganizationInfoView: React.FC = ({ is_org_admin, is_proxy_admin, userModels, - editOrg + editOrg, }) => { - const [orgData, setOrgData] = useState(null); - const [loading, setLoading] = useState(true); - const [form] = Form.useForm(); - const [isEditing, setIsEditing] = useState(false); - const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false); - const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false); - const [selectedEditMember, setSelectedEditMember] = useState(null); + const [orgData, setOrgData] = useState(null) + const [loading, setLoading] = useState(true) + const [form] = Form.useForm() + const [isEditing, setIsEditing] = useState(false) + const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false) + const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false) + const [selectedEditMember, setSelectedEditMember] = useState(null) - const canEditOrg = is_org_admin || is_proxy_admin; + const canEditOrg = is_org_admin || is_proxy_admin const fetchOrgInfo = async () => { try { - setLoading(true); - if (!accessToken) return; - const response = await organizationInfoCall(accessToken, organizationId); - setOrgData(response); + setLoading(true) + if (!accessToken) return + const response = await organizationInfoCall(accessToken, organizationId) + setOrgData(response) } catch (error) { - message.error("Failed to load organization information"); - console.error("Error fetching organization info:", error); + message.error("Failed to load organization information") + console.error("Error fetching organization info:", error) } finally { - setLoading(false); + setLoading(false) } - }; + } useEffect(() => { - fetchOrgInfo(); - }, [organizationId, accessToken]); + fetchOrgInfo() + }, [organizationId, accessToken]) const handleMemberAdd = async (values: any) => { try { if (accessToken == null) { - return; + return } const member: Member = { @@ -91,21 +99,21 @@ const OrganizationInfoView: React.FC = ({ user_id: values.user_id, role: values.role, } - const response = await organizationMemberAddCall(accessToken, organizationId, member); + const response = await organizationMemberAddCall(accessToken, organizationId, member) - message.success("Organization member added successfully"); - setIsAddMemberModalVisible(false); - form.resetFields(); - fetchOrgInfo(); + message.success("Organization member added successfully") + setIsAddMemberModalVisible(false) + form.resetFields() + fetchOrgInfo() } catch (error) { - message.error("Failed to add organization member"); - console.error("Error adding organization member:", error); + message.error("Failed to add organization member") + console.error("Error adding organization member:", error) } - }; + } const handleMemberUpdate = async (values: any) => { try { - if (!accessToken) return; + if (!accessToken) return const member: Member = { user_email: values.user_email, @@ -113,35 +121,35 @@ const OrganizationInfoView: React.FC = ({ role: values.role, } - const response = await organizationMemberUpdateCall(accessToken, organizationId, member); - message.success("Organization member updated successfully"); - setIsEditMemberModalVisible(false); - form.resetFields(); - fetchOrgInfo(); + const response = await organizationMemberUpdateCall(accessToken, organizationId, member) + message.success("Organization member updated successfully") + setIsEditMemberModalVisible(false) + form.resetFields() + fetchOrgInfo() } catch (error) { - message.error("Failed to update organization member"); - console.error("Error updating organization member:", error); + message.error("Failed to update organization member") + console.error("Error updating organization member:", error) } - }; + } const handleMemberDelete = async (values: any) => { try { - if (!accessToken) return; + if (!accessToken) return - await organizationMemberDeleteCall(accessToken, organizationId, values.user_id); - message.success("Organization member deleted successfully"); - setIsEditMemberModalVisible(false); - form.resetFields(); - fetchOrgInfo(); + await organizationMemberDeleteCall(accessToken, organizationId, values.user_id) + message.success("Organization member deleted successfully") + setIsEditMemberModalVisible(false) + form.resetFields() + fetchOrgInfo() } catch (error) { - message.error("Failed to delete organization member"); - console.error("Error deleting organization member:", error); + message.error("Failed to delete organization member") + console.error("Error deleting organization member:", error) } - }; + } const handleOrgUpdate = async (values: any) => { try { - if (!accessToken) return; + if (!accessToken) return const updateData: any = { organization_id: organizationId, @@ -154,50 +162,55 @@ const OrganizationInfoView: React.FC = ({ budget_duration: values.budget_duration, }, metadata: values.metadata ? JSON.parse(values.metadata) : null, - }; + } // Handle object_permission updates if (values.vector_stores !== undefined || values.mcp_servers_and_groups !== undefined) { updateData.object_permission = { ...orgData?.object_permission, - vector_stores: values.vector_stores || [] - }; - + vector_stores: values.vector_stores || [], + } + if (values.mcp_servers_and_groups !== undefined) { - const { servers, accessGroups } = values.mcp_servers_and_groups || { servers: [], accessGroups: [] }; + const { servers, accessGroups } = values.mcp_servers_and_groups || { + servers: [], + accessGroups: [], + } if (servers && servers.length > 0) { - updateData.object_permission.mcp_servers = servers; + updateData.object_permission.mcp_servers = servers } if (accessGroups && accessGroups.length > 0) { - updateData.object_permission.mcp_access_groups = accessGroups; + updateData.object_permission.mcp_access_groups = accessGroups } } } - - const response = await organizationUpdateCall(accessToken, updateData); - message.success("Organization settings updated successfully"); - setIsEditing(false); - fetchOrgInfo(); + const response = await organizationUpdateCall(accessToken, updateData) + + message.success("Organization settings updated successfully") + setIsEditing(false) + fetchOrgInfo() } catch (error) { - message.error("Failed to update organization settings"); - console.error("Error updating organization:", error); + message.error("Failed to update organization settings") + console.error("Error updating organization:", error) } - }; + } if (loading) { - return
Loading...
; + return
Loading...
} if (!orgData) { - return
Organization not found
; + return
Organization not found
} return (
- + + Back to Organizations + {orgData.organization_alias} {orgData.organization_id}
@@ -213,138 +226,145 @@ const OrganizationInfoView: React.FC = ({ {/* Overview Panel */} - - + + Organization Details
- Created: {new Date(orgData.created_at).toLocaleDateString()} - Updated: {new Date(orgData.updated_at).toLocaleDateString()} - Created By: {orgData.created_by} + Created: {new Date(orgData.created_at).toLocaleDateString()} + Updated: {new Date(orgData.updated_at).toLocaleDateString()} + Created By: {orgData.created_by}
-
+
- + Budget Status
- ${formatNumberWithCommas(orgData.spend, 4)} - of {orgData.litellm_budget_table.max_budget === null ? "Unlimited" : `$${formatNumberWithCommas(orgData.litellm_budget_table.max_budget, 4)}`} - {orgData.litellm_budget_table.budget_duration && ( + ${formatNumberWithCommas(orgData.spend, 4)} + + of{" "} + {orgData.litellm_budget_table.max_budget === null + ? "Unlimited" + : `$${formatNumberWithCommas(orgData.litellm_budget_table.max_budget, 4)}`} + + {orgData.litellm_budget_table.budget_duration && ( Reset: {orgData.litellm_budget_table.budget_duration} - )} + )}
-
+
- + Rate Limits
- TPM: {orgData.litellm_budget_table.tpm_limit || 'Unlimited'} - RPM: {orgData.litellm_budget_table.rpm_limit || 'Unlimited'} - {orgData.litellm_budget_table.max_parallel_requests && ( + TPM: {orgData.litellm_budget_table.tpm_limit || "Unlimited"} + RPM: {orgData.litellm_budget_table.rpm_limit || "Unlimited"} + {orgData.litellm_budget_table.max_parallel_requests && ( Max Parallel Requests: {orgData.litellm_budget_table.max_parallel_requests} - )} + )}
-
+
- + Models
- {orgData.models.length === 0 ? ( - All proxy models - ) : ( - orgData.models.map((model, index) => ( - - {model} - - )) - )} + {orgData.models.length === 0 ? ( + All proxy models + ) : ( + orgData.models.map((model, index) => ( + + {model} + + )) + )}
-
- + + Teams
- {orgData.teams?.map((team, index) => ( + {orgData.teams?.map((team, index) => ( - {team.team_id} + {team.team_id} - ))} + ))}
-
+
- +
{/* Budget Panel */}
- + - + - User ID - Role - Spend - Created At - + User ID + Role + Spend + Created At + - + - + {orgData.members?.map((member, index) => ( - + - {member.user_id} + {member.user_id} - {member.user_role} + {member.user_role} - ${formatNumberWithCommas(member.spend, 4)} + ${formatNumberWithCommas(member.spend, 4)} - {new Date(member.created_at).toLocaleString()} + {new Date(member.created_at).toLocaleString()} - {canEditOrg && ( + {canEditOrg && ( <> - { - setSelectedEditMember({ - "role": member.user_role, - "user_email": member.user_email, - "user_id": member.user_id - }); - setIsEditMemberModalVisible(true); + setSelectedEditMember({ + role: member.user_role, + user_email: member.user_email, + user_id: member.user_id, + }) + setIsEditMemberModalVisible(true) }} - /> - + { - handleMemberDelete(member); + handleMemberDelete(member) }} - /> + /> - )} + )} - + ))} - +
-
- {canEditOrg && ( - { - setIsAddMemberModalVisible(true); - }}> - Add Member +
+ {canEditOrg && ( + { + setIsAddMemberModalVisible(true) + }} + > + Add Member - )} + )}
@@ -353,12 +373,8 @@ const OrganizationInfoView: React.FC = ({
Organization Settings - {(canEditOrg && !isEditing) && ( - setIsEditing(true)} - > - Edit Settings - + {canEditOrg && !isEditing && ( + setIsEditing(true)}>Edit Settings )}
@@ -377,24 +393,26 @@ const OrganizationInfoView: React.FC = ({ vector_stores: orgData.object_permission?.vector_stores || [], mcp_servers_and_groups: { servers: orgData.object_permission?.mcp_servers || [], - accessGroups: orgData.object_permission?.mcp_access_groups || [] - } + accessGroups: orgData.object_permission?.mcp_access_groups || [], + }, }} layout="vertical" > - + - All Proxy Models @@ -428,8 +446,8 @@ const OrganizationInfoView: React.FC = ({ form.setFieldValue('vector_stores', values)} - value={form.getFieldValue('vector_stores')} + onChange={(values) => form.setFieldValue("vector_stores", values)} + value={form.getFieldValue("vector_stores")} accessToken={accessToken || ""} placeholder="Select vector stores" /> @@ -437,25 +455,21 @@ const OrganizationInfoView: React.FC = ({ form.setFieldValue('mcp_servers_and_groups', values)} - value={form.getFieldValue('mcp_servers_and_groups')} + onChange={(values) => form.setFieldValue("mcp_servers_and_groups", values)} + value={form.getFieldValue("mcp_servers_and_groups")} accessToken={accessToken || ""} placeholder="Select MCP servers and access groups" /> - +
- - - Save Changes - + + Save Changes
@@ -485,17 +499,22 @@ const OrganizationInfoView: React.FC = ({
Rate Limits -
TPM: {orgData.litellm_budget_table.tpm_limit || 'Unlimited'}
-
RPM: {orgData.litellm_budget_table.rpm_limit || 'Unlimited'}
+
TPM: {orgData.litellm_budget_table.tpm_limit || "Unlimited"}
+
RPM: {orgData.litellm_budget_table.rpm_limit || "Unlimited"}
Budget -
Max: {orgData.litellm_budget_table.max_budget !== null ? `$${formatNumberWithCommas(orgData.litellm_budget_table.max_budget, 4)}` : 'No Limit'}
-
Reset: {orgData.litellm_budget_table.budget_duration || 'Never'}
+
+ Max:{" "} + {orgData.litellm_budget_table.max_budget !== null + ? `$${formatNumberWithCommas(orgData.litellm_budget_table.max_budget, 4)}` + : "No Limit"} +
+
Reset: {orgData.litellm_budget_table.budget_duration || "Never"}
- = ({ accessToken={accessToken} title="Add Organization Member" roles={[ - { label: "org_admin", value: "org_admin", description: "Can add and remove members, and change their roles." }, - { label: "internal_user", value: "internal_user", description: "Can view/create keys for themselves within organization." }, - { label: "internal_user_viewer", value: "internal_user_viewer", description: "Can only view their keys within organization." } + { + label: "org_admin", + value: "org_admin", + description: "Can add and remove members, and change their roles.", + }, + { + label: "internal_user", + value: "internal_user", + description: "Can view/create keys for themselves within organization.", + }, + { + label: "internal_user_viewer", + value: "internal_user_viewer", + description: "Can only view their keys within organization.", + }, ]} defaultRole="internal_user" /> @@ -532,12 +563,12 @@ const OrganizationInfoView: React.FC = ({ roleOptions: [ { label: "Org Admin", value: "org_admin" }, { label: "Internal User", value: "internal_user" }, - { label: "Internal User Viewer", value: "internal_user_viewer" } - ] + { label: "Internal User Viewer", value: "internal_user_viewer" }, + ], }} />
- ); -}; + ) +} -export default OrganizationInfoView; \ No newline at end of file +export default OrganizationInfoView diff --git a/ui/litellm-dashboard/src/components/organizations.tsx b/ui/litellm-dashboard/src/components/organizations.tsx index a0bacf12059..cb727d4dd64 100644 --- a/ui/litellm-dashboard/src/components/organizations.tsx +++ b/ui/litellm-dashboard/src/components/organizations.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect } from "react" import { Table, TableHead, @@ -18,38 +18,41 @@ import { Tab, TabPanels, TabPanel, -} from "@tremor/react"; -import NumericalInput from "./shared/numerical_input"; -import { Input } from "antd"; -import { Modal, Form, Tooltip, Select as Select2 } from "antd"; -import { InfoCircleOutlined } from '@ant-design/icons'; -import { PencilAltIcon, TrashIcon, RefreshIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline"; -import { TextInput } from "@tremor/react"; -import { getModelDisplayName } from './key_team_helpers/fetch_available_models_team_key'; -import { message } from 'antd'; -import OrganizationInfoView from './organization/organization_view'; -import { Organization, organizationListCall, organizationCreateCall, organizationDeleteCall } from './networking'; -import VectorStoreSelector from "./vector_store_management/VectorStoreSelector"; -import MCPServerSelector from "./mcp_server_management/MCPServerSelector"; -import { formatNumberWithCommas } from "../utils/dataUtils"; +} from "@tremor/react" +import NumericalInput from "./shared/numerical_input" +import { Input } from "antd" +import { Modal, Form, Tooltip, Select as Select2 } from "antd" +import { InfoCircleOutlined } from "@ant-design/icons" +import { PencilAltIcon, TrashIcon, RefreshIcon, ChevronDownIcon, ChevronRightIcon } from "@heroicons/react/outline" +import { TextInput } from "@tremor/react" +import { getModelDisplayName } from "./key_team_helpers/fetch_available_models_team_key" +import { message } from "antd" +import OrganizationInfoView from "./organization/organization_view" +import { Organization, organizationListCall, organizationCreateCall, organizationDeleteCall } from "./networking" +import VectorStoreSelector from "./vector_store_management/VectorStoreSelector" +import MCPServerSelector from "./mcp_server_management/MCPServerSelector" +import { formatNumberWithCommas } from "../utils/dataUtils" interface OrganizationsTableProps { - organizations: Organization[]; - userRole: string; - userModels: string[]; - accessToken: string | null; - lastRefreshed?: string; - handleRefreshClick?: () => void; - currentOrg?: any; - guardrailsList?: string[]; - setOrganizations: (organizations: Organization[]) => void; - premiumUser: boolean; + organizations: Organization[] + userRole: string + userModels: string[] + accessToken: string | null + lastRefreshed?: string + handleRefreshClick?: () => void + currentOrg?: any + guardrailsList?: string[] + setOrganizations: (organizations: Organization[]) => void + premiumUser: boolean } -export const fetchOrganizations = async (accessToken: string, setOrganizations: (organizations: Organization[]) => void) => { - const organizations = await organizationListCall(accessToken); - setOrganizations(organizations); -}; +export const fetchOrganizations = async ( + accessToken: string, + setOrganizations: (organizations: Organization[]) => void, +) => { + const organizations = await organizationListCall(accessToken) + setOrganizations(organizations) +} const OrganizationsTable: React.FC = ({ organizations, @@ -61,115 +64,106 @@ const OrganizationsTable: React.FC = ({ currentOrg, guardrailsList = [], setOrganizations, - premiumUser + premiumUser, }) => { - const [selectedOrgId, setSelectedOrgId] = useState(null); - const [editOrg, setEditOrg] = useState(false); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [orgToDelete, setOrgToDelete] = useState(null); - const [isOrgModalVisible, setIsOrgModalVisible] = useState(false); - const [form] = Form.useForm(); - const [expandedAccordions, setExpandedAccordions] = useState>({}); + const [selectedOrgId, setSelectedOrgId] = useState(null) + const [editOrg, setEditOrg] = useState(false) + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) + const [orgToDelete, setOrgToDelete] = useState(null) + const [isOrgModalVisible, setIsOrgModalVisible] = useState(false) + const [form] = Form.useForm() + const [expandedAccordions, setExpandedAccordions] = useState>({}) useEffect(() => { if (accessToken) { - fetchOrganizations(accessToken, setOrganizations); + fetchOrganizations(accessToken, setOrganizations) } - }, [accessToken]); + }, [accessToken]) const handleDelete = (orgId: string | null) => { - if (!orgId) return; - - setOrgToDelete(orgId); - setIsDeleteModalOpen(true); - }; + if (!orgId) return + + setOrgToDelete(orgId) + setIsDeleteModalOpen(true) + } const confirmDelete = async () => { - if (!orgToDelete || !accessToken) return; + if (!orgToDelete || !accessToken) return try { - await organizationDeleteCall(accessToken, orgToDelete); - message.success('Organization deleted successfully'); + await organizationDeleteCall(accessToken, orgToDelete) + message.success("Organization deleted successfully") - setIsDeleteModalOpen(false); - setOrgToDelete(null); + setIsDeleteModalOpen(false) + setOrgToDelete(null) // Refresh organizations list - fetchOrganizations(accessToken, setOrganizations); + fetchOrganizations(accessToken, setOrganizations) } catch (error) { - console.error('Error deleting organization:', error); + console.error("Error deleting organization:", error) } - }; + } const cancelDelete = () => { - setIsDeleteModalOpen(false); - setOrgToDelete(null); - }; + setIsDeleteModalOpen(false) + setOrgToDelete(null) + } const handleCreate = async (values: any) => { try { - if (!accessToken) return; + if (!accessToken) return - console.log(`values in organizations new create call: ${JSON.stringify(values)}`); + console.log(`values in organizations new create call: ${JSON.stringify(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 && + (values.allowed_mcp_servers_and_groups.servers?.length > 0 || + values.allowed_mcp_servers_and_groups.accessGroups?.length > 0)) ) { - values.object_permission = {}; + values.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; + values.object_permission.vector_stores = values.allowed_vector_store_ids + delete values.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; + values.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; + values.object_permission.mcp_access_groups = values.allowed_mcp_servers_and_groups.accessGroups } - delete values.allowed_mcp_servers_and_groups; + delete values.allowed_mcp_servers_and_groups } } - await organizationCreateCall(accessToken, values); - setIsOrgModalVisible(false); - form.resetFields(); + await organizationCreateCall(accessToken, values) + setIsOrgModalVisible(false) + form.resetFields() // Refresh organizations list - fetchOrganizations(accessToken, setOrganizations); + fetchOrganizations(accessToken, setOrganizations) } catch (error) { - console.error('Error creating organization:', error); + console.error("Error creating organization:", error) } - }; + } const handleCancel = () => { - setIsOrgModalVisible(false); - form.resetFields(); - }; + setIsOrgModalVisible(false) + form.resetFields() + } if (!premiumUser) { return (
- This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key here. + + This is a LiteLLM Enterprise feature, and requires a valid key to use. Get a trial key{" "} + + here + + . +
- ); - } - - if (selectedOrgId) { - return ( - { - setSelectedOrgId(null); - setEditOrg(false); - }} - accessToken={accessToken} - is_org_admin={true} // You'll need to implement proper org admin check - is_proxy_admin={userRole === "Admin"} - userModels={userModels} - editOrg={editOrg} - /> - ); + ) } return ( @@ -177,436 +171,331 @@ const OrganizationsTable: React.FC = ({ {(userRole === "Admin" || userRole === "Org Admin") && ( - <> - - -
- - - - - - - All Proxy Models - - {userModels && userModels.length > 0 && userModels.map((model) => ( - - {getModelDisplayName(model)} - - ))} - - - - - - - - - daily - weekly - monthly - - - - - - - - - - - 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)" - /> - - - - 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)" - /> - - - - - - -
- -
-
-
- + )} + {selectedOrgId ? ( + { + setSelectedOrgId(null) + setEditOrg(false) + }} + accessToken={accessToken} + is_org_admin={true} // You'll need to implement proper org admin check + is_proxy_admin={userRole === "Admin"} + userModels={userModels} + editOrg={editOrg} + /> + ) : ( + + +
+ Your Organizations +
+
+ {lastRefreshed && Last Refreshed: {lastRefreshed}} + +
+
+ + + Click on “Organization ID” to view organization details. + + + + + + + Organization ID + Organization Name + Created + Spend (USD) + Budget (USD) + Models + TPM / RPM Limits + Info + Actions + + - - -
- Your Organizations -
-
- {lastRefreshed && Last Refreshed: {lastRefreshed}} - -
-
- - - - Click on “Organization ID” to view organization details. - - -
- -
- - - 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) => ( - - -
- - - -
-
- {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.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'} + + {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 - )} - {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)} - - - ) - ))} -
+ ) : ( + <> +
+ {org.models.length > 3 && ( +
+ { + setExpandedAccordions((prev) => ({ + ...prev, + [org.organization_id || ""]: + !prev[org.organization_id || ""], + })) + }} + /> +
+ )} +
+ {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(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)} - icon={TrashIcon} - size="sm" - /> - - )} - - - )) - : null} - -
-
- - {(userRole === "Admin" || userRole === "Org Admin") && ( - - -
- - - - - - - All Proxy Models - - {userModels && userModels.length > 0 && userModels.map((model) => ( - - {getModelDisplayName(model)} - - ))} - - - - - - - - - daily - weekly - monthly - - - - - - - - - - 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)" - /> - - - 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)" - /> - - - - - - -
- -
-
-
- - )} -
-
-
-
+ ) : 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)} + icon={TrashIcon} + size="sm" + /> + + )} + + + )) + : null} + + + + + + + + + )} + +
+ + + + + + + All Proxy Models + + {userModels && + userModels.length > 0 && + userModels.map((model) => ( + + {getModelDisplayName(model)} + + ))} + + + + + + + + + daily + weekly + monthly + + + + + + + + + + + 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)" + /> + + + + 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)" + /> + + + + + + +
+ +
+
+
{isDeleteModalOpen ? (
@@ -623,13 +512,9 @@ const OrganizationsTable: React.FC = ({
-

- Delete Organization -

+

Delete Organization

-

- Are you sure you want to delete this organization? -

+

Are you sure you want to delete this organization?

@@ -643,10 +528,11 @@ const OrganizationsTable: React.FC = ({
- ) : <>} - + ) : ( + <> + )}
- ); -}; + ) +} -export default OrganizationsTable; \ No newline at end of file +export default OrganizationsTable diff --git a/ui/litellm-dashboard/src/components/team/team_info.tsx b/ui/litellm-dashboard/src/components/team/team_info.tsx index 8abcc3925cd..c2b4a402f75 100644 --- a/ui/litellm-dashboard/src/components/team/team_info.tsx +++ b/ui/litellm-dashboard/src/components/team/team_info.tsx @@ -18,17 +18,27 @@ import { TableHeaderCell, TableBody, Table, - Icon + Icon, } from "@tremor/react"; import TeamMembersComponent from "./team_member_view"; import MemberPermissions from "./member_permissions"; -import { teamInfoCall, teamMemberDeleteCall, teamMemberAddCall, teamMemberUpdateCall, Member, teamUpdateCall } from "@/components/networking"; -import { Button, Form, Input, Select, message, Tooltip } from "antd"; -import { InfoCircleOutlined } from '@ant-design/icons'; import { - Select as Select2, -} from "antd"; -import { PencilAltIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline"; + teamInfoCall, + teamMemberDeleteCall, + teamMemberAddCall, + teamMemberUpdateCall, + Member, + teamUpdateCall, +} from "@/components/networking"; +import { Button, Form, Input, Select, message, Tooltip } from "antd"; +import { InfoCircleOutlined } from "@ant-design/icons"; +import { Select as Select2 } from "antd"; +import { + ArrowLeftIcon, + PencilAltIcon, + PlusIcon, + TrashIcon, +} from "@heroicons/react/outline"; import MemberModal from "./edit_membership"; import UserSearchModal from "@/components/common_components/user_search_modal"; import { getModelDisplayName } from "../key_team_helpers/fetch_available_models_team_key"; @@ -110,23 +120,26 @@ export interface TeamInfoProps { premiumUser?: boolean; } -const TeamInfoView: React.FC = ({ - teamId, - onClose, - accessToken, - is_team_admin, +const TeamInfoView: React.FC = ({ + teamId, + onClose, + accessToken, + is_team_admin, is_proxy_admin, userModels, editTeam, premiumUser = false, - onUpdate + onUpdate, }) => { const [teamData, setTeamData] = useState(null); const [loading, setLoading] = useState(true); const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false); const [form] = Form.useForm(); - const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false); - const [selectedEditMember, setSelectedEditMember] = useState(null); + const [isEditMemberModalVisible, setIsEditMemberModalVisible] = + useState(false); + const [selectedEditMember, setSelectedEditMember] = useState( + null + ); const [isEditing, setIsEditing] = useState(false); const [mcpAccessGroups, setMcpAccessGroups] = useState([]); const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false); @@ -168,41 +181,43 @@ const TeamInfoView: React.FC = ({ const handleMemberCreate = async (values: any) => { try { if (accessToken == null) return; - + const member: Member = { user_email: values.user_email, user_id: values.user_id, role: values.role, }; - + await teamMemberAddCall(accessToken, teamId, member); - + message.success("Team member added successfully"); setIsAddMemberModalVisible(false); form.resetFields(); - + // Fetch updated team info const updatedTeamData = await teamInfoCall(accessToken, teamId); setTeamData(updatedTeamData); - + // Notify parent component of the update onUpdate(updatedTeamData); } catch (error: any) { let errMsg = "Failed to add team member"; - - if (error?.raw?.detail?.error?.includes("Assigning team admins is a premium feature")) { - errMsg = "Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this."; + + if ( + error?.raw?.detail?.error?.includes( + "Assigning team admins is a premium feature" + ) + ) { + errMsg = + "Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this."; } else if (error?.message) { errMsg = error.message; } - + message.error(errMsg); console.error("Error adding team member:", error); } }; - - - const handleMemberUpdate = async (values: any) => { try { @@ -214,24 +229,29 @@ const TeamInfoView: React.FC = ({ user_email: values.user_email, user_id: values.user_id, role: values.role, - } + }; message.destroy(); // Remove all existing toasts await teamMemberUpdateCall(accessToken, teamId, member); message.success("Team member updated successfully"); setIsEditMemberModalVisible(false); - + // Fetch updated team info const updatedTeamData = await teamInfoCall(accessToken, teamId); setTeamData(updatedTeamData); - + // Notify parent component of the update onUpdate(updatedTeamData); } catch (error: any) { let errMsg = "Failed to update team member"; - if (error?.raw?.detail?.includes("Assigning team admins is a premium feature")) { - errMsg = "Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this."; + if ( + error?.raw?.detail?.includes( + "Assigning team admins is a premium feature" + ) + ) { + errMsg = + "Assigning admins is an enterprise-only feature. Please upgrade your LiteLLM plan to enable this."; } else if (error?.message) { errMsg = error.message; } @@ -243,7 +263,6 @@ const TeamInfoView: React.FC = ({ console.error("Error updating team member:", error); } }; - const handleMemberDelete = async (member: Member) => { try { @@ -254,11 +273,11 @@ const TeamInfoView: React.FC = ({ await teamMemberDeleteCall(accessToken, teamId, member); message.success("Team member removed successfully"); - + // Fetch updated team info const updatedTeamData = await teamInfoCall(accessToken, teamId); setTeamData(updatedTeamData); - + // Notify parent component of the update onUpdate(updatedTeamData); } catch (error) { @@ -290,7 +309,7 @@ const TeamInfoView: React.FC = ({ metadata: { ...parsedMetadata, guardrails: values.guardrails || [], - logging: values.logging_settings || [] + logging: values.logging_settings || [], }, organization_id: values.organization_id, }; @@ -304,8 +323,14 @@ const TeamInfoView: React.FC = ({ } // Handle object_permission updates - const { servers, accessGroups } = values.mcp_servers_and_groups || { servers: [], accessGroups: [] }; - if ((servers && servers.length > 0) || (accessGroups && accessGroups.length > 0)) { + const { servers, accessGroups } = values.mcp_servers_and_groups || { + servers: [], + accessGroups: [], + }; + if ( + (servers && servers.length > 0) || + (accessGroups && accessGroups.length > 0) + ) { updateData.object_permission = {}; if (servers && servers.length > 0) { updateData.object_permission.mcp_servers = servers; @@ -315,9 +340,9 @@ const TeamInfoView: React.FC = ({ } } delete values.mcp_servers_and_groups; - + const response = await teamUpdateCall(accessToken, updateData); - + message.success("Team settings updated successfully"); setIsEditing(false); fetchTeamInfo(); @@ -340,7 +365,14 @@ const TeamInfoView: React.FC = ({
- + + Back to Teams + {info.team_alias} {info.team_id}
@@ -350,11 +382,13 @@ const TeamInfoView: React.FC = ({ {[ Overview, - ...(canEditTeam ? [ - Members, - Member Permissions, - Settings - ] : []) + ...(canEditTeam + ? [ + Members, + Member Permissions, + Settings, + ] + : []), ]} @@ -366,13 +400,26 @@ const TeamInfoView: React.FC = ({ Budget Status
${formatNumberWithCommas(info.spend, 4)} - of {info.max_budget === null ? "Unlimited" : `$${formatNumberWithCommas(info.max_budget, 4)}`} + + of{" "} + {info.max_budget === null + ? "Unlimited" + : `$${formatNumberWithCommas(info.max_budget, 4)}`} + {info.budget_duration && ( - Reset: {info.budget_duration} + + Reset: {info.budget_duration} + )} -
+
{info.team_member_budget_table && ( - Team Member Budget: ${formatNumberWithCommas(info.team_member_budget_table.max_budget, 4)} + + Team Member Budget: $ + {formatNumberWithCommas( + info.team_member_budget_table.max_budget, + 4 + )} + )}
@@ -380,10 +427,12 @@ const TeamInfoView: React.FC = ({ Rate Limits
- TPM: {info.tpm_limit || 'Unlimited'} - RPM: {info.rpm_limit || 'Unlimited'} + TPM: {info.tpm_limit || "Unlimited"} + RPM: {info.rpm_limit || "Unlimited"} {info.max_parallel_requests && ( - Max Parallel Requests: {info.max_parallel_requests} + + Max Parallel Requests: {info.max_parallel_requests} + )}
@@ -403,13 +452,13 @@ const TeamInfoView: React.FC = ({
- - @@ -431,7 +480,7 @@ const TeamInfoView: React.FC = ({ {/* Member Permissions Panel */} {canEditTeam && ( - = ({
Team Settings - {(canEditTeam && !isEditing) && ( - setIsEditing(true)} - > + {canEditTeam && !isEditing && ( + setIsEditing(true)}> Edit Settings )} @@ -467,31 +514,39 @@ const TeamInfoView: React.FC = ({ budget_duration: info.budget_duration, guardrails: info.metadata?.guardrails || [], metadata: info.metadata - ? JSON.stringify((({ logging, ...rest }) => rest)(info.metadata), null, 2) + ? JSON.stringify( + (({ logging, ...rest }) => rest)(info.metadata), + null, + 2 + ) : "", logging_settings: info.metadata?.logging || [], organization_id: info.organization_id, vector_stores: info.object_permission?.vector_stores || [], mcp_servers: info.object_permission?.mcp_servers || [], - mcp_access_groups: info.object_permission?.mcp_servers || [], - mcp_servers_and_groups: info.object_permission?.mcp_servers || [] + mcp_access_groups: + info.object_permission?.mcp_servers || [], + mcp_servers_and_groups: + info.object_permission?.mcp_servers || [], }} layout="vertical" > - + - + - + All Proxy Models {Array.from(new Set(userModels)).map((model, idx) => ( @@ -503,14 +558,30 @@ const TeamInfoView: React.FC = ({ - + - - + + - + - - + - + - Guardrails{' '} + Guardrails{" "} - e.stopPropagation()} > - + @@ -562,45 +638,54 @@ const TeamInfoView: React.FC = ({ form.setFieldValue('vector_stores', values)} - value={form.getFieldValue('vector_stores')} + onChange={(values) => + form.setFieldValue("vector_stores", values) + } + value={form.getFieldValue("vector_stores")} accessToken={accessToken || ""} placeholder="Select vector stores" /> - + 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)" /> - + form.setFieldValue('logging_settings', values)} + value={form.getFieldValue("logging_settings")} + onChange={(values) => + form.setFieldValue("logging_settings", values) + } /> - - +
- - - Save Changes - + Save Changes
@@ -630,23 +715,34 @@ const TeamInfoView: React.FC = ({
Rate Limits -
TPM: {info.tpm_limit || 'Unlimited'}
-
RPM: {info.rpm_limit || 'Unlimited'}
+
TPM: {info.tpm_limit || "Unlimited"}
+
RPM: {info.rpm_limit || "Unlimited"}
Team Budget -
Max Budget: {info.max_budget !== null ? `$${formatNumberWithCommas(info.max_budget, 4)}` : 'No Limit'}
-
Budget Reset: {info.budget_duration || 'Never'}
+
+ Max Budget:{" "} + {info.max_budget !== null + ? `$${formatNumberWithCommas(info.max_budget, 4)}` + : "No Limit"} +
+
Budget Reset: {info.budget_duration || "Never"}
- Team Member Settings{' '} + Team Member Settings{" "} - + -
Max Budget: {info.team_member_budget_table?.max_budget || 'No Limit'}
-
Key Duration: {info.metadata?.team_member_key_duration || 'No Limit'}
+
+ Max Budget:{" "} + {info.team_member_budget_table?.max_budget || "No Limit"} +
+
+ Key Duration:{" "} + {info.metadata?.team_member_key_duration || "No Limit"} +
Organization ID @@ -654,19 +750,19 @@ const TeamInfoView: React.FC = ({
Status - - {info.blocked ? 'Blocked' : 'Active'} + + {info.blocked ? "Blocked" : "Active"}
- - = ({ showUserId: true, roleOptions: [ { label: "Admin", value: "admin" }, - { label: "User", value: "user" } - ] + { label: "User", value: "user" }, + ], }} /> @@ -705,4 +801,4 @@ const TeamInfoView: React.FC = ({ ); }; -export default TeamInfoView; \ No newline at end of file +export default TeamInfoView; diff --git a/ui/litellm-dashboard/src/components/teams.tsx b/ui/litellm-dashboard/src/components/teams.tsx index b7db95a39cc..ef6ecd42e1d 100644 --- a/ui/litellm-dashboard/src/components/teams.tsx +++ b/ui/litellm-dashboard/src/components/teams.tsx @@ -1,7 +1,14 @@ import React, { useState, useEffect } from "react"; import Link from "next/link"; import { Typography } from "antd"; -import { teamDeleteCall, teamUpdateCall, teamInfoCall, Organization, DEFAULT_ORGANIZATION, fetchMCPAccessGroups } from "./networking"; +import { + teamDeleteCall, + teamUpdateCall, + teamInfoCall, + Organization, + DEFAULT_ORGANIZATION, + fetchMCPAccessGroups, +} from "./networking"; import TeamMemberModal from "@/components/team/edit_membership"; import { fetchTeams } from "./common_components/fetch_teams"; import { @@ -12,7 +19,7 @@ import { StatusOnlineIcon, TrashIcon, ChevronDownIcon, - ChevronRightIcon + ChevronRightIcon, } from "@heroicons/react/outline"; import { Button as Button2, @@ -21,12 +28,16 @@ import { Input, Select as Select2, message, - Tooltip + Tooltip, } from "antd"; import NumericalInput from "./shared/numerical_input"; -import { fetchAvailableModelsForTeamOrKey, getModelDisplayName, unfurlWildcardModelsInList } from "./key_team_helpers/fetch_available_models_team_key"; +import { + fetchAvailableModelsForTeamOrKey, + getModelDisplayName, + unfurlWildcardModelsInList, +} from "./key_team_helpers/fetch_available_models_team_key"; import { Select, SelectItem } from "@tremor/react"; -import { InfoCircleOutlined } from '@ant-design/icons'; +import { InfoCircleOutlined } from "@ant-design/icons"; import { getGuardrailsList } from "./networking"; import TeamInfoView, { TeamData } from "@/components/team/team_info"; import TeamSSOSettings from "@/components/TeamSSOSettings"; @@ -53,7 +64,7 @@ import { TabList, TabPanel, TabPanels, - Tab + Tab, } from "@tremor/react"; import { CogIcon } from "@heroicons/react/outline"; import AvailableTeamsPanel from "@/components/team/available_teams"; @@ -80,7 +91,7 @@ interface FilterState { team_alias: string; organization_id: string; sort_by: string; - sort_order: 'asc' | 'desc'; + sort_order: "asc" | "desc"; } interface EditTeamModalProps { @@ -96,7 +107,7 @@ import { teamMemberUpdateCall, Member, modelAvailableCall, - v2TeamListCall + v2TeamListCall, } from "./networking"; import { updateExistingKeys } from "@/utils/dataUtils"; @@ -109,7 +120,10 @@ interface PerTeamInfo { team_info: TeamInfo; } -const getOrganizationModels = (organization: Organization | null, userModels: string[]) => { +const getOrganizationModels = ( + organization: Organization | null, + userModels: string[] +) => { let tempModelsToPick = []; if (organization) { @@ -126,7 +140,7 @@ const getOrganizationModels = (organization: Organization | null, userModels: st } return unfurlWildcardModelsInList(tempModelsToPick, userModels); -} +}; const Teams: React.FC = ({ teams, @@ -136,27 +150,28 @@ const Teams: React.FC = ({ userID, userRole, organizations, - premiumUser = false + premiumUser = false, }) => { const [lastRefreshed, setLastRefreshed] = useState(""); const [currentOrg, setCurrentOrg] = useState(null); - const [currentOrgForCreateTeam, setCurrentOrgForCreateTeam] = useState(null); + const [currentOrgForCreateTeam, setCurrentOrgForCreateTeam] = + useState(null); const [showFilters, setShowFilters] = useState(false); const [filters, setFilters] = useState({ team_id: "", team_alias: "", organization_id: "", sort_by: "created_at", - sort_order: "desc" + sort_order: "desc", }); useEffect(() => { - console.log(`inside useeffect - ${lastRefreshed}`) + console.log(`inside useeffect - ${lastRefreshed}`); if (accessToken) { // Call your function here - fetchTeams(accessToken, userID, userRole, currentOrg, setTeams) + fetchTeams(accessToken, userID, userRole, currentOrg, setTeams); } - handleRefreshClick() + handleRefreshClick(); }, [lastRefreshed]); const [form] = Form.useForm(); @@ -165,24 +180,27 @@ const Teams: React.FC = ({ const [value, setValue] = useState(""); const [editModalVisible, setEditModalVisible] = useState(false); - const [selectedTeam, setSelectedTeam] = useState( - null - ); + const [selectedTeam, setSelectedTeam] = useState(null); const [selectedTeamId, setSelectedTeamId] = useState(null); const [editTeam, setEditTeam] = useState(false); const [isTeamModalVisible, setIsTeamModalVisible] = useState(false); const [isAddMemberModalVisible, setIsAddMemberModalVisible] = useState(false); - const [isEditMemberModalVisible, setIsEditMemberModalVisible] = useState(false); + const [isEditMemberModalVisible, setIsEditMemberModalVisible] = + useState(false); const [userModels, setUserModels] = useState([]); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [teamToDelete, setTeamToDelete] = useState(null); const [modelsToPick, setModelsToPick] = useState([]); - const [perTeamInfo, setPerTeamInfo] = useState>({}); + const [perTeamInfo, setPerTeamInfo] = useState>( + {} + ); // Add this state near the other useState declarations const [guardrailsList, setGuardrailsList] = useState([]); - const [expandedAccordions, setExpandedAccordions] = useState>({}); + const [expandedAccordions, setExpandedAccordions] = useState< + Record + >({}); const [loggingSettings, setLoggingSettings] = useState([]); const [mcpAccessGroups, setMcpAccessGroups] = useState([]); const [mcpAccessGroupsLoaded, setMcpAccessGroupsLoaded] = useState(false); @@ -192,7 +210,7 @@ const Teams: React.FC = ({ const models = getOrganizationModels(currentOrgForCreateTeam, userModels); console.log(`models: ${models}`); setModelsToPick(models); - form.setFieldValue('models', []); + form.setFieldValue("models", []); }, [currentOrgForCreateTeam, userModels]); // Add this useEffect to fetch guardrails @@ -235,17 +253,20 @@ const Teams: React.FC = ({ useEffect(() => { const fetchTeamInfo = () => { if (!teams) return; - - const newPerTeamInfo = teams.reduce((acc, team) => { - acc[team.team_id] = { - keys: team.keys || [], - team_info: { - members_with_roles: team.members_with_roles || [] - } - }; - return acc; - }, {} as Record); - + + const newPerTeamInfo = teams.reduce( + (acc, team) => { + acc[team.team_id] = { + keys: team.keys || [], + team_info: { + members_with_roles: team.members_with_roles || [], + }, + }; + return acc; + }, + {} as Record + ); + setPerTeamInfo(newPerTeamInfo); }; @@ -290,7 +311,7 @@ const Teams: React.FC = ({ try { await teamDeleteCall(accessToken, teamToDelete); // Successfully completed the deletion. Update the state to trigger a rerender. - fetchTeams(accessToken, userID, userRole, currentOrg, setTeams) + fetchTeams(accessToken, userID, userRole, currentOrg, setTeams); } catch (error) { console.error("Error deleting the team:", error); // Handle any error situations, such as displaying an error message to the user. @@ -313,7 +334,11 @@ const Teams: React.FC = ({ if (userID === null || userRole === null || accessToken === null) { return; } - const models = await fetchAvailableModelsForTeamOrKey(userID, userRole, accessToken); + const models = await fetchAvailableModelsForTeamOrKey( + userID, + userRole, + accessToken + ); if (models) { setUserModels(models); } @@ -331,14 +356,14 @@ const Teams: React.FC = ({ if (accessToken != null) { const newTeamAlias = formValues?.team_alias; const existingTeamAliases = teams?.map((t) => t.team_alias) ?? []; - let organizationId = formValues?.organization_id || currentOrg?.organization_id; - if (organizationId === "" || typeof organizationId !== 'string') { + let organizationId = + formValues?.organization_id || currentOrg?.organization_id; + if (organizationId === "" || typeof organizationId !== "string") { formValues.organization_id = null; } else { formValues.organization_id = organizationId.trim(); } - // Remove guardrails from top level since it's now in metadata if (existingTeamAliases.includes(newTeamAlias)) { throw new Error( @@ -347,7 +372,7 @@ const Teams: React.FC = ({ } message.info("Creating Team"); - + // Handle logging settings in metadata if (loggingSettings.length > 0) { let metadata = {}; @@ -355,31 +380,42 @@ const Teams: React.FC = ({ try { metadata = JSON.parse(formValues.metadata); } catch (e) { - console.warn("Invalid JSON in metadata field, starting with empty object"); + console.warn( + "Invalid JSON in metadata field, starting with empty object" + ); } } - + // Add logging settings to metadata metadata = { ...metadata, - logging: loggingSettings.filter(config => config.callback_name) // Only include configs with callback_name + logging: loggingSettings.filter((config) => config.callback_name), // Only include configs with callback_name }; - + formValues.metadata = JSON.stringify(metadata); } - + // Transform allowed_vector_store_ids and allowed_mcp_servers_and_groups into object_permission if ( - (formValues.allowed_vector_store_ids && formValues.allowed_vector_store_ids.length > 0) || - (formValues.allowed_mcp_servers_and_groups && (formValues.allowed_mcp_servers_and_groups.servers?.length > 0 || formValues.allowed_mcp_servers_and_groups.accessGroups?.length > 0)) + (formValues.allowed_vector_store_ids && + formValues.allowed_vector_store_ids.length > 0) || + (formValues.allowed_mcp_servers_and_groups && + (formValues.allowed_mcp_servers_and_groups.servers?.length > 0 || + formValues.allowed_mcp_servers_and_groups.accessGroups?.length > + 0)) ) { formValues.object_permission = {}; - if (formValues.allowed_vector_store_ids && formValues.allowed_vector_store_ids.length > 0) { - formValues.object_permission.vector_stores = formValues.allowed_vector_store_ids; + if ( + formValues.allowed_vector_store_ids && + formValues.allowed_vector_store_ids.length > 0 + ) { + formValues.object_permission.vector_stores = + formValues.allowed_vector_store_ids; delete formValues.allowed_vector_store_ids; } if (formValues.allowed_mcp_servers_and_groups) { - const { servers, accessGroups } = formValues.allowed_mcp_servers_and_groups; + const { servers, accessGroups } = + formValues.allowed_mcp_servers_and_groups; if (servers && servers.length > 0) { formValues.object_permission.mcp_servers = servers; } @@ -391,11 +427,15 @@ const Teams: React.FC = ({ } // Transform allowed_mcp_access_groups into object_permission - if (formValues.allowed_mcp_access_groups && formValues.allowed_mcp_access_groups.length > 0) { + if ( + formValues.allowed_mcp_access_groups && + formValues.allowed_mcp_access_groups.length > 0 + ) { if (!formValues.object_permission) { formValues.object_permission = {}; } - formValues.object_permission.mcp_access_groups = formValues.allowed_mcp_access_groups; + formValues.object_permission.mcp_access_groups = + formValues.allowed_mcp_access_groups; delete formValues.allowed_mcp_access_groups; } const response: any = await teamCreateCall(accessToken, formValues); @@ -427,9 +467,7 @@ const Teams: React.FC = ({ } } return false; - } - - + }; const handleRefreshClick = () => { // Update the 'lastRefreshed' state to the current date and time @@ -448,21 +486,23 @@ const Teams: React.FC = ({ null, newFilters.team_id || null, newFilters.team_alias || null - ).then((response) => { - if (response && response.teams) { - setTeams(response.teams); - } - }).catch((error) => { - console.error("Error fetching teams:", error); - }); + ) + .then((response) => { + if (response && response.teams) { + setTeams(response.teams); + } + }) + .catch((error) => { + console.error("Error fetching teams:", error); + }); } }; - const handleSortChange = (sortBy: string, sortOrder: 'asc' | 'desc') => { + const handleSortChange = (sortBy: string, sortOrder: "asc" | "desc") => { const newFilters = { ...filters, sort_by: sortBy, - sort_order: sortOrder + sort_order: sortOrder, }; setFilters(newFilters); // Call teamListCall with the new sort parameters @@ -473,13 +513,15 @@ const Teams: React.FC = ({ null, filters.team_id || null, filters.team_alias || null - ).then((response) => { - if (response && response.teams) { - setTeams(response.teams); - } - }).catch((error) => { - console.error("Error fetching teams:", error); - }); + ) + .then((response) => { + if (response && response.teams) { + setTeams(response.teams); + } + }) + .catch((error) => { + console.error("Error fetching teams:", error); + }); } }; @@ -489,17 +531,19 @@ const Teams: React.FC = ({ team_alias: "", organization_id: "", sort_by: "created_at", - sort_order: "desc" + sort_order: "desc", }); // Reset teams list if (accessToken) { - v2TeamListCall(accessToken, null, userID || null, null, null).then((response) => { - if (response && response.teams) { - setTeams(response.teams); - } - }).catch((error) => { - console.error("Error fetching teams:", error); - }); + v2TeamListCall(accessToken, null, userID || null, null, null) + .then((response) => { + if (response && response.teams) { + setTeams(response.teams); + } + }) + .catch((error) => { + console.error("Error fetching teams:", error); + }); } }; @@ -507,54 +551,58 @@ const Teams: React.FC = ({
- { - (userRole == "Admin" || userRole == "Org Admin") && !selectedTeamId? - : null - } + {(userRole == "Admin" || userRole == "Org Admin") && ( + + )} {selectedTeamId ? ( - { - setTeams(teams => { + { + setTeams((teams) => { if (teams == null) { return teams; } - - return teams.map(team => { - if (data.team_id === team.team_id) { - return updateExistingKeys(team, data) - } - - return team - }) - }) - }} - onClose={() => { - setSelectedTeamId(null); - setEditTeam(false); - }} - accessToken={accessToken} - is_team_admin={is_team_admin(teams?.find((team) => team.team_id === selectedTeamId))} - is_proxy_admin={userRole == "Admin"} - userModels={userModels} - editTeam={editTeam} - /> + return teams.map((team) => { + if (data.team_id === team.team_id) { + return updateExistingKeys(team, data); + } + + return team; + }); + }); + }} + onClose={() => { + setSelectedTeamId(null); + setEditTeam(false); + }} + accessToken={accessToken} + is_team_admin={is_team_admin( + teams?.find((team) => team.team_id === selectedTeamId) + )} + is_proxy_admin={userRole == "Admin"} + userModels={userModels} + editTeam={editTeam} + /> ) : ( - -
- Your Teams - Available Teams - {isAdminRole(userRole || "") && Default Team Settings} + +
+ Your Teams + Available Teams + {isAdminRole(userRole || "") && ( + Default Team Settings + )}
- {lastRefreshed && Last Refreshed: {lastRefreshed}} + {lastRefreshed && ( + Last Refreshed: {lastRefreshed} + )} = ({ onClick={handleRefreshClick} />
-
- - - - Click on “Team ID” to view team details and manage team members. - - - - -
-
- {/* Search and Filter Controls */} -
- {/* Team Alias Search */} -
- handleFilterChange('team_alias', e.target.value)} - /> - - - -
+ + + + + Click on “Team ID” to view team details{" "} + and manage team members. + + + + +
+
+ {/* Search and Filter Controls */} +
+ {/* Team Alias Search */} +
+ + handleFilterChange( + "team_alias", + e.target.value + ) + } + /> + + + +
- {/* Filter Button */} - + {/* Filter Button */} + - {/* Reset Filters Button */} - -
+ {/* Reset Filters Button */} + +
- {/* Additional Filters */} - {showFilters && ( -
- {/* Team ID Search */} -
- handleFilterChange('team_id', e.target.value)} - /> - - - -
+ {/* Additional Filters */} + {showFilters && ( +
+ {/* Team ID Search */} +
+ + handleFilterChange( + "team_id", + e.target.value + ) + } + /> + + + +
- {/* Organization Dropdown */} -
- + {/* Organization Dropdown */} +
+ +
+
+ )}
- )} -
-
- - - - Team Name - Team ID - Created - Spend (USD) - Budget (USD) - Models - Organization - Info - - +
+ + + Team Name + Team ID + Created + Spend (USD) + Budget (USD) + Models + Organization + Info + + - - {teams && teams.length > 0 - ? teams - .filter((team) => { - if (!currentOrg) return true; - return team.organization_id === currentOrg.organization_id; - }) - .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) - .map((team: any) => ( - - - {team["team_alias"]} - - -
- - - -
-
- - {team.created_at ? new Date(team.created_at).toLocaleDateString() : "N/A"} - - - {formatNumberWithCommas(team["spend"], 4)} - - - {team["max_budget"] !== null && team["max_budget"] !== undefined ? team["max_budget"] : "No limit"} - - 3 ? "px-0" : ""} - > -
- {Array.isArray(team.models) ? ( -
- {team.models.length === 0 ? ( - - All Proxy Models - - ) : ( - <> -
- {team.models.length > 3 && ( -
- { - setExpandedAccordions(prev => ({ - ...prev, - [team.team_id]: !prev[team.team_id] - })); - }} - /> + + {teams && teams.length > 0 + ? teams + .filter((team) => { + if (!currentOrg) return true; + return ( + team.organization_id === + currentOrg.organization_id + ); + }) + .sort( + (a, b) => + new Date(b.created_at).getTime() - + new Date(a.created_at).getTime() + ) + .map((team: any) => ( + + + {team["team_alias"]} + + +
+ + +
- )} -
- {team.models.slice(0, 3).map((model: string, index: number) => ( - model === "all-proxy-models" ? ( - - All Proxy Models - - ) : ( - - - {model.length > 30 - ? `${getModelDisplayName(model).slice(0, 30)}...` - : getModelDisplayName(model)} - - - ) - ))} - {team.models.length > 3 && !expandedAccordions[team.team_id] && ( - - +{team.models.length - 3} {team.models.length - 3 === 1 ? 'more model' : 'more models'} - + + + {team.created_at + ? new Date( + team.created_at + ).toLocaleDateString() + : "N/A"} + + + {formatNumberWithCommas( + team["spend"], + 4 )} - {expandedAccordions[team.team_id] && ( -
- {team.models.slice(3).map((model: string, index: number) => ( - model === "all-proxy-models" ? ( - - All Proxy Models - - ) : ( - - - {model.length > 30 - ? `${getModelDisplayName(model).slice(0, 30)}...` - : getModelDisplayName(model)} - - - ) - ))} -
- )} +
+ + {team["max_budget"] !== null && + team["max_budget"] !== undefined + ? team["max_budget"] + : "No limit"} + + 3 ? "px-0" : "" + } + > +
+ {Array.isArray(team.models) ? ( +
+ {team.models.length === 0 ? ( + + All Proxy Models + + ) : ( + <> +
+ {team.models.length > 3 && ( +
+ { + setExpandedAccordions( + (prev) => ({ + ...prev, + [team.team_id]: + !prev[ + team.team_id + ], + }) + ); + }} + /> +
+ )} +
+ {team.models + .slice(0, 3) + .map( + ( + model: string, + index: number + ) => + model === + "all-proxy-models" ? ( + + + All Proxy + Models + + + ) : ( + + + {model.length > + 30 + ? `${getModelDisplayName(model).slice(0, 30)}...` + : getModelDisplayName( + model + )} + + + ) + )} + {team.models.length > 3 && + !expandedAccordions[ + team.team_id + ] && ( + + + + + {team.models + .length - + 3}{" "} + {team.models + .length - + 3 === + 1 + ? "more model" + : "more models"} + + + )} + {expandedAccordions[ + team.team_id + ] && ( +
+ {team.models + .slice(3) + .map( + ( + model: string, + index: number + ) => + model === + "all-proxy-models" ? ( + + + All Proxy + Models + + + ) : ( + + + {model.length > + 30 + ? `${getModelDisplayName(model).slice(0, 30)}...` + : getModelDisplayName( + model + )} + + + ) + )} +
+ )} +
+
+ + )} +
+ ) : null} +
+
+ + + {team.organization_id} + + + + {perTeamInfo && + team.team_id && + perTeamInfo[team.team_id] && + perTeamInfo[team.team_id].keys && + perTeamInfo[team.team_id].keys + .length}{" "} + Keys + + + {perTeamInfo && + team.team_id && + perTeamInfo[team.team_id] && + perTeamInfo[team.team_id] + .team_info && + perTeamInfo[team.team_id].team_info + .members_with_roles && + perTeamInfo[team.team_id].team_info + .members_with_roles.length}{" "} + Members + + + + {userRole == "Admin" ? ( + <> + { + setSelectedTeamId(team.team_id); + setEditTeam(true); + }} + /> + + handleDelete(team.team_id) + } + icon={TrashIcon} + size="sm" + /> + + ) : null} + + + )) + : null} + +
+ {isDeleteModalOpen && ( +
+
+ + + {/* Modal Panel */} + + + {/* Confirmation Modal Content */} +
+
+
+
+

+ Delete Team +

+
+

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

- - - )} -
- ) : null} -
- - - - {team.organization_id} - - - - {perTeamInfo && - team.team_id && - perTeamInfo[team.team_id] && - perTeamInfo[team.team_id].keys && - perTeamInfo[team.team_id].keys.length}{" "} - Keys - - - {perTeamInfo && - team.team_id && - perTeamInfo[team.team_id] && - perTeamInfo[team.team_id].team_info && - perTeamInfo[team.team_id].team_info.members_with_roles && - perTeamInfo[team.team_id].team_info.members_with_roles.length}{" "} - Members - - - - {userRole == "Admin" ? ( - <> - { - setSelectedTeamId(team.team_id); - setEditTeam(true); - }} - /> - handleDelete(team.team_id)} - icon={TrashIcon} - size="sm" - /> - - ) : null} - - - )) - : null} - - - {isDeleteModalOpen && ( -
-
- - - {/* Modal Panel */} - - - {/* Confirmation Modal Content */} -
-
-
-
-

- Delete Team -

-
-

- Are you sure you want to delete this team ? -

+
+
+
+ +
-
- - -
-
-
-
- )} - - - {userRole == "Admin" || userRole == "Org Admin"? ( - - -
+ + + + + + + {isAdminRole(userRole || "") && ( + + + + )} + + + )} + {(userRole == "Admin" || userRole == "Org Admin") && ( + + + <> + - <> - + + + Organization{" "} + + Organizations can have multiple teams. Learn more + about{" "} + e.stopPropagation()} + > + user management hierarchy + + + } + > + + + + } + name="organization_id" + initialValue={ + currentOrg ? currentOrg.organization_id : null + } + className="mt-8" + > + { + form.setFieldValue("organization_id", value); + setCurrentOrgForCreateTeam( + organizations?.find( + (org) => org.organization_id === value + ) || null + ); + }} + filterOption={(input, option) => { + if (!option) return false; + const optionValue = option.children?.toString() || ""; + return optionValue + .toLowerCase() + .includes(input.toLowerCase()); + }} + optionFilterProp="children" + > + {organizations?.map((org) => ( + + + {org.organization_alias} + {" "} + + ({org.organization_id}) + + + ))} + + + + Models{" "} + + + + + } + name="models" + > + + - + All Proxy Models + + {modelsToPick.map((model) => ( + + {getModelDisplayName(model)} + + ))} + + + + + + + + + daily + weekly + monthly + + + + + + + + + + { + if (!mcpAccessGroupsLoaded) { + fetchMcpAccessGroups(); + setMcpAccessGroupsLoaded(true); + } + }} + > + + Additional Settings + + + + { + e.target.value = e.target.value.trim(); + }} + /> + + + value ? Number(value) : undefined + } + tooltip="This is the individual budget for a user in the team." + > + + + + + 1 day + 1 week + 1 month + + + + - Organization{' '} - - Organizations can have multiple teams. Learn more about{' '} - e.stopPropagation()} - > - user management hierarchy - - - }> - + Guardrails{" "} + + e.stopPropagation()} + > + + } - name="organization_id" - initialValue={currentOrg ? currentOrg.organization_id : null} + name="guardrails" className="mt-8" + help="Select existing guardrails or enter new ones" > { - form.setFieldValue('organization_id', value); - setCurrentOrgForCreateTeam(organizations?.find((org) => org.organization_id === value) || null); - }} - filterOption={(input, option) => { - if (!option) return false; - const optionValue = option.children?.toString() || ''; - return optionValue.toLowerCase().includes(input.toLowerCase()); - }} - optionFilterProp="children" - > - {organizations?.map((org) => ( - - {org.organization_alias}{" "} - ({org.organization_id}) - - ))} - + mode="tags" + style={{ width: "100%" }} + placeholder="Select or enter guardrails" + options={guardrailsList.map((name) => ({ + value: name, + label: name, + }))} + /> - - Models{' '} - - + Allowed Vector Stores{" "} + + - } name="models"> - - - All Proxy Models - - {modelsToPick.map((model) => ( - - {getModelDisplayName(model)} - - ))} - - - - - - - - - daily - weekly - monthly - + + form.setFieldValue( + "allowed_vector_store_ids", + values + ) + } + value={form.getFieldValue("allowed_vector_store_ids")} + accessToken={accessToken || ""} + placeholder="Select vector stores (optional)" + premiumUser={premiumUser} + /> + Allowed MCP Servers{" "} + + + + + } + name="allowed_mcp_servers_and_groups" + className="mt-8" + help="Select MCP servers or access groups this team can access. " > - - - - + + form.setFieldValue( + "allowed_mcp_servers_and_groups", + val + ) + } + value={form.getFieldValue( + "allowed_mcp_servers_and_groups" + )} + accessToken={accessToken || ""} + placeholder="Select MCP servers or access groups (optional)" + premiumUser={premiumUser} + /> + + - { if (!mcpAccessGroupsLoaded) { fetchMcpAccessGroups(); setMcpAccessGroupsLoaded(true); } }}> - - Additional Settings - - - - { - e.target.value = e.target.value.trim(); - }} - /> - - value ? Number(value) : undefined} - tooltip="This is the individual budget for a user in the team." - > - - - - - 1 day - 1 week - 1 month - - - - - - - Guardrails{' '} - - e.stopPropagation()} - > - - - - - } - name="guardrails" - className="mt-8" - help="Select existing guardrails or enter new ones" - > - ({ value: name, label: name }))} - /> - - - Allowed Vector Stores{' '} - - - - - } - name="allowed_vector_store_ids" - className="mt-8" - help="Select vector stores this team 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)" - premiumUser={premiumUser} - /> - - - Allowed MCP Servers{' '} - - - - - } - name="allowed_mcp_servers_and_groups" - className="mt-8" - help="Select MCP servers or access groups this team can access. " - > - form.setFieldValue('allowed_mcp_servers_and_groups', val)} - value={form.getFieldValue('allowed_mcp_servers_and_groups')} - accessToken={accessToken || ''} - placeholder="Select MCP servers or access groups (optional)" - premiumUser={premiumUser} - /> - - - - - - - Logging Settings - - -
- -
-
-
- -
- Create Team -
- -
- - ) : null} - - - - - - {isAdminRole(userRole || "") && ( - - - - )} - - - )} + + + Logging Settings + + +
+ +
+
+
+ +
+ Create Team +
+ +
+ )}
diff --git a/ui/litellm-dashboard/src/components/user_dashboard.tsx b/ui/litellm-dashboard/src/components/user_dashboard.tsx index b32951f5dbc..c8055b813a8 100644 --- a/ui/litellm-dashboard/src/components/user_dashboard.tsx +++ b/ui/litellm-dashboard/src/components/user_dashboard.tsx @@ -1,5 +1,5 @@ -"use client"; -import React, { useState, useEffect } from "react"; +"use client" +import React, { useState, useEffect } from "react" import { userInfoCall, modelAvailableCall, @@ -9,67 +9,64 @@ import { organizationListCall, DEFAULT_ORGANIZATION, keyInfoCall, - getProxyBaseUrl -} 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 ViewUserSpend from "./view_user_spend"; -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"; -import { clearTokenCookies } from "@/utils/cookieUtils"; + getProxyBaseUrl, +} 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 ViewUserSpend from "./view_user_spend" +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" +import { clearTokenCookies } from "@/utils/cookieUtils" export interface ProxySettings { - PROXY_BASE_URL: string | null; - PROXY_LOGOUT_URL: string | null; - DEFAULT_TEAM_DISABLED: boolean; - SSO_ENABLED: boolean; - DISABLE_EXPENSIVE_DB_QUERIES: boolean; - NUM_SPEND_LOGS_ROWS: number; + PROXY_BASE_URL: string | null + PROXY_LOGOUT_URL: string | null + DEFAULT_TEAM_DISABLED: boolean + SSO_ENABLED: boolean + DISABLE_EXPENSIVE_DB_QUERIES: boolean + NUM_SPEND_LOGS_ROWS: number } - export type UserInfo = { - models: string[]; - max_budget?: number | null; - spend: number; + models: string[] + max_budget?: number | null + spend: number } function getCookie(name: string) { console.log("COOKIES", document.cookie) - 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 } interface UserDashboardProps { - userID: string | null; - userRole: string | null; - userEmail: string | null; - teams: Team[] | null; - keys: any[] | null; - setUserRole: React.Dispatch>; - setUserEmail: React.Dispatch>; - setTeams: React.Dispatch>; - setKeys: React.Dispatch>; - premiumUser: boolean; - organizations: Organization[] | null; - addKey: (data: any) => void; + userID: string | null + userRole: string | null + userEmail: string | null + teams: Team[] | null + keys: any[] | null + setUserRole: React.Dispatch> + setUserEmail: React.Dispatch> + setTeams: React.Dispatch> + setKeys: React.Dispatch> + premiumUser: boolean + organizations: Organization[] | null + addKey: (data: any) => void createClicked: boolean } type TeamInterface = { - models: any[]; - team_id: null; - team_alias: String; -}; + models: any[] + team_id: null + team_alias: String +} const UserDashboard: React.FC = ({ userID, @@ -84,63 +81,61 @@ const UserDashboard: React.FC = ({ premiumUser, organizations, addKey, - createClicked + createClicked, }) => { - const [userSpendData, setUserSpendData] = useState( - null - ); - const [currentOrg, setCurrentOrg] = useState(null); + const [userSpendData, setUserSpendData] = useState(null) + const [currentOrg, setCurrentOrg] = useState(null) // Assuming useSearchParams() hook exists and works in your setup - const searchParams = useSearchParams()!; + const searchParams = useSearchParams()! - const token = getCookie('token'); + const token = getCookie("token") - const invitation_id = searchParams.get("invitation_id"); + const invitation_id = searchParams.get("invitation_id") - const [accessToken, setAccessToken] = useState(null); - const [teamSpend, setTeamSpend] = useState(null); - const [userModels, setUserModels] = useState([]); - const [proxySettings, setProxySettings] = useState(null); + const [accessToken, setAccessToken] = useState(null) + const [teamSpend, setTeamSpend] = useState(null) + const [userModels, setUserModels] = useState([]) + const [proxySettings, setProxySettings] = useState(null) const defaultTeam: TeamInterface = { models: [], team_alias: "Default Team", team_id: null, - }; - const [selectedTeam, setSelectedTeam] = useState(null); - const [selectedKeyAlias, setSelectedKeyAlias] = useState(null); + } + const [selectedTeam, setSelectedTeam] = useState(null) + const [selectedKeyAlias, setSelectedKeyAlias] = useState(null) // check if window is not undefined if (typeof window !== "undefined") { window.addEventListener("beforeunload", function () { // Clear session storage - sessionStorage.clear(); - }); + sessionStorage.clear() + }) } function formatUserRole(userRole: string) { if (!userRole) { - return "Undefined Role"; + return "Undefined Role" } - console.log(`Received user role: ${userRole}`); + console.log(`Received user role: ${userRole}`) 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 "app_user": - return "App User"; + return "App User" case "internal_user": - return "Internal User"; + return "Internal User" case "internal_user_viewer": - return "Internal Viewer"; + return "Internal Viewer" default: - return "Unknown Role"; + return "Unknown Role" } } @@ -148,258 +143,217 @@ const UserDashboard: React.FC = ({ // Moved useEffect inside the component and used a condition to run fetch only if the params are available useEffect(() => { if (token) { - const decoded = jwtDecode(token) as { [key: string]: any }; + const decoded = jwtDecode(token) as { [key: string]: any } if (decoded) { // cast decoded to dictionary - console.log("Decoded token:", decoded); + console.log("Decoded token:", decoded) - console.log("Decoded key:", decoded.key); + console.log("Decoded key:", decoded.key) // set accessToken - setAccessToken(decoded.key); + setAccessToken(decoded.key) // check if userRole is defined if (decoded.user_role) { - const formattedUserRole = formatUserRole(decoded.user_role); - console.log("Decoded user_role:", formattedUserRole); - setUserRole(formattedUserRole); + const formattedUserRole = formatUserRole(decoded.user_role) + console.log("Decoded user_role:", formattedUserRole) + setUserRole(formattedUserRole) } else { - console.log("User role not defined"); + console.log("User role not defined") } if (decoded.user_email) { - setUserEmail(decoded.user_email); + setUserEmail(decoded.user_email) } else { - console.log(`User Email is not set ${decoded}`); + console.log(`User Email is not set ${decoded}`) } } } if (userID && accessToken && userRole && !keys && !userSpendData) { - const cachedUserModels = sessionStorage.getItem("userModels" + userID); + const cachedUserModels = sessionStorage.getItem("userModels" + userID) if (cachedUserModels) { - setUserModels(JSON.parse(cachedUserModels)); + setUserModels(JSON.parse(cachedUserModels)) } else { console.log(`currentOrg: ${JSON.stringify(currentOrg)}`) const fetchData = async () => { try { - const proxy_settings: ProxySettings = await getProxyUISettings(accessToken); - setProxySettings(proxy_settings); + const proxy_settings: ProxySettings = await getProxyUISettings(accessToken) + setProxySettings(proxy_settings) - const response = await userInfoCall( - accessToken, - userID, - userRole, - false, - null, - null - ); - + const response = await userInfoCall(accessToken, userID, userRole, false, null, null) - setUserSpendData(response["user_info"]); + setUserSpendData(response["user_info"]) console.log(`userSpendData: ${JSON.stringify(userSpendData)}`) - // set keys for admin and users if (!response?.teams[0].keys) { - setKeys(response["keys"]); + setKeys(response["keys"]) } else { setKeys( response["keys"].concat( response.teams .filter((team: any) => userRole === "Admin" || team.user_id === userID) - .flatMap((team: any) => team.keys) - ) - ); - + .flatMap((team: any) => team.keys), + ), + ) } - sessionStorage.setItem( - "userData" + userID, - JSON.stringify(response["keys"]) - ); - sessionStorage.setItem( - "userSpendData" + userID, - JSON.stringify(response["user_info"]) - ); + sessionStorage.setItem("userData" + userID, JSON.stringify(response["keys"])) + sessionStorage.setItem("userSpendData" + userID, JSON.stringify(response["user_info"])) - const model_available = await modelAvailableCall( - accessToken, - userID, - userRole - ); + const model_available = await modelAvailableCall(accessToken, userID, userRole) // loop through model_info["data"] and create an array of element.model_name - 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); + 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) - console.log("userModels:", userModels); + console.log("userModels:", userModels) - sessionStorage.setItem( - "userModels" + userID, - JSON.stringify(available_model_names) - ); + sessionStorage.setItem("userModels" + userID, JSON.stringify(available_model_names)) } catch (error: any) { - console.error("There was an error fetching the data", error); + console.error("There was an error fetching the data", error) if (error.message.includes("Invalid proxy server token passed")) { - gotoLogin(); + gotoLogin() } // Optionally, update your UI to reflect the error state here as well } - }; - fetchData(); - fetchTeams(accessToken, userID, userRole, currentOrg, setTeams); + } + fetchData() + fetchTeams(accessToken, userID, userRole, currentOrg, setTeams) } } - }, [userID, token, accessToken, keys, userRole]); - + }, [userID, token, accessToken, keys, userRole]) useEffect(() => { // check key health - if it's invalid, redirect to login if (accessToken) { const fetchKeyInfo = async () => { try { - const keyInfo = await keyInfoCall(accessToken, [accessToken]); - console.log("keyInfo: ", keyInfo); + const keyInfo = await keyInfoCall(accessToken, [accessToken]) + console.log("keyInfo: ", keyInfo) } catch (error: any) { if (error.message.includes("Invalid proxy server token passed")) { - gotoLogin(); + gotoLogin() } } } - fetchKeyInfo(); + fetchKeyInfo() } - }, [accessToken]); + }, [accessToken]) useEffect(() => { - console.log(`currentOrg: ${JSON.stringify(currentOrg)}, accessToken: ${accessToken}, userID: ${userID}, userRole: ${userRole}`) + console.log( + `currentOrg: ${JSON.stringify(currentOrg)}, accessToken: ${accessToken}, userID: ${userID}, userRole: ${userRole}`, + ) if (accessToken) { console.log(`fetching teams`) - fetchTeams(accessToken, userID, userRole, currentOrg, setTeams); + fetchTeams(accessToken, userID, userRole, currentOrg, setTeams) } - }, [currentOrg]); + }, [currentOrg]) useEffect(() => { // This code will run every time selectedTeam changes - if ( - keys !== null && - selectedTeam !== null && - selectedTeam !== undefined && - selectedTeam.team_id !== null - ) { - let sum = 0; + if (keys !== null && selectedTeam !== null && selectedTeam !== undefined && selectedTeam.team_id !== null) { + let sum = 0 console.log(`keys: ${JSON.stringify(keys)}`) for (const key of keys) { - if ( - selectedTeam.hasOwnProperty("team_id") && - key.team_id !== null && - key.team_id === selectedTeam.team_id - ) { - sum += key.spend; + if (selectedTeam.hasOwnProperty("team_id") && key.team_id !== null && key.team_id === selectedTeam.team_id) { + sum += key.spend } } console.log(`sum: ${sum}`) - setTeamSpend(sum); + setTeamSpend(sum) } else if (keys !== null) { // sum the keys which don't have team-id set (default team) - let sum = 0; + let sum = 0 for (const key of keys) { - sum += key.spend; + sum += key.spend } - setTeamSpend(sum); + setTeamSpend(sum) } - }, [selectedTeam]); - + }, [selectedTeam]) if (invitation_id != null) { - return ( - - ) + return } function gotoLogin() { // Clear token cookies using the utility function - clearTokenCookies(); + clearTokenCookies() - const baseUrl = getProxyBaseUrl(); + const baseUrl = getProxyBaseUrl() - console.log("proxyBaseUrl:", baseUrl); - - const url = baseUrl - ? `${baseUrl}/sso/key/generate` - : `/sso/key/generate`; + console.log("proxyBaseUrl:", baseUrl) - console.log("Full URL:", url); - window.location.href = url; + const url = baseUrl ? `${baseUrl}/sso/key/generate` : `/sso/key/generate` - return null; + console.log("Full URL:", url) + window.location.href = url + + return null } if (token == null) { - // user is not logged in as yet - console.log("All cookies before redirect:", document.cookie); - + // user is not logged in as yet + console.log("All cookies before redirect:", document.cookie) + // Clear token cookies using the utility function - gotoLogin(); - return null; + gotoLogin() + return null } else { // Check if token is expired try { - const decoded = jwtDecode(token) as { [key: string]: any }; - console.log("Decoded token:", decoded); - const expTime = decoded.exp; - const currentTime = Math.floor(Date.now() / 1000); - + const decoded = jwtDecode(token) as { [key: string]: any } + console.log("Decoded token:", decoded) + const expTime = decoded.exp + const currentTime = Math.floor(Date.now() / 1000) + if (expTime && currentTime >= expTime) { - console.log("Token expired, redirecting to login"); - - gotoLogin(); - - return null; + console.log("Token expired, redirecting to login") + + gotoLogin() + + return null } } catch (error) { - console.error("Error decoding token:", error); + console.error("Error decoding token:", error) // If there's an error decoding the token, consider it invalid - clearTokenCookies(); - - gotoLogin(); - - return null; + clearTokenCookies() + + gotoLogin() + + return null } - + if (accessToken == null) { - return null; + return null } } if (userID == null) { - return ( -

User ID is not set

- ); + return

User ID is not set

} - if (userRole == null) { - setUserRole("App Owner"); + setUserRole("App Owner") } if (userRole && userRole == "Admin Viewer") { - const { Title, Paragraph } = Typography; + const { Title, Paragraph } = Typography return (
Access Denied Ask your proxy admin for access to create keys
- ); + ) } - console.log("inside user dashboard, selected team", selectedTeam); - console.log("All cookies after redirect:", document.cookie); + console.log("inside user dashboard, selected team", selectedTeam) + console.log("All cookies after redirect:", document.cookie) return (
- = ({
- ); -}; + ) +} -export default UserDashboard; +export default UserDashboard diff --git a/ui/litellm-dashboard/src/components/view_logs/SessionView.tsx b/ui/litellm-dashboard/src/components/view_logs/SessionView.tsx index b2b2c3138e7..bb655ba0e8f 100644 --- a/ui/litellm-dashboard/src/components/view_logs/SessionView.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/SessionView.tsx @@ -1,63 +1,61 @@ -import React, { useState } from 'react'; -import { LogEntry } from './columns'; -import { DataTable } from './table'; -import { columns } from './columns'; -import { Card, Title, Text, Metric, AreaChart } from '@tremor/react'; -import { RequestViewer } from './index'; -import { formatNumberWithCommas } from '@/utils/dataUtils'; +import React, { useState } from "react" +import { LogEntry } from "./columns" +import { DataTable } from "./table" +import { columns } from "./columns" +import { Card, Title, Text, Metric, AreaChart, Button as TremorButton } from "@tremor/react" +import { RequestViewer } from "./index" +import { formatNumberWithCommas } from "@/utils/dataUtils" +import { ArrowLeftIcon } from "@heroicons/react/outline" interface SessionViewProps { - sessionId: string; - logs: LogEntry[]; - onBack: () => void; + sessionId: string + logs: LogEntry[] + onBack: () => void } export const SessionView: React.FC = ({ sessionId, logs, onBack }) => { // Track which log row is expanded - const [expandedRequestId, setExpandedRequestId] = useState(null); + const [expandedRequestId, setExpandedRequestId] = useState(null) // Calculate session metrics - const totalCost = logs.reduce((sum, log) => sum + (log.spend || 0), 0); - const totalTokens = logs.reduce((sum, log) => sum + (log.total_tokens || 0), 0); - const startTime = logs.length > 0 ? new Date(logs[0].startTime) : new Date(); - const endTime = logs.length > 0 ? new Date(logs[logs.length - 1].endTime) : new Date(); - const durationMs = endTime.getTime() - startTime.getTime(); - const durationSec = (durationMs / 1000).toFixed(2); - + const totalCost = logs.reduce((sum, log) => sum + (log.spend || 0), 0) + const totalTokens = logs.reduce((sum, log) => sum + (log.total_tokens || 0), 0) + const startTime = logs.length > 0 ? new Date(logs[0].startTime) : new Date() + const endTime = logs.length > 0 ? new Date(logs[logs.length - 1].endTime) : new Date() + const durationMs = endTime.getTime() - startTime.getTime() + const durationSec = (durationMs / 1000).toFixed(2) + // Prepare data for the timeline chart - const timelineData = logs.map(log => ({ + const timelineData = logs.map((log) => ({ time: new Date(log.startTime).toISOString(), tokens: log.total_tokens || 0, cost: log.spend || 0, - })); + })) return (
{/* Header with back button */}
-
- -
+ + Back to All Logs +

Session Details

@@ -80,17 +78,17 @@ export const SessionView: React.FC = ({ sessionId, logs, onBac
{/* Request Timeline */} - Session Logs -
- true} - loadingMessage="Loading logs..." - noDataMessage="No logs found" - /> -
+ Session Logs +
+ true} + loadingMessage="Loading logs..." + noDataMessage="No logs found" + /> +
- ); -}; \ No newline at end of file + ) +} diff --git a/ui/litellm-dashboard/src/components/view_users.tsx b/ui/litellm-dashboard/src/components/view_users.tsx index ff9dab752eb..c9ace5d2045 100644 --- a/ui/litellm-dashboard/src/components/view_users.tsx +++ b/ui/litellm-dashboard/src/components/view_users.tsx @@ -1,15 +1,7 @@ -import React, { useState, useEffect, useCallback, useRef } from "react"; -import { - Tab, - TabGroup, - TabList, - TabPanels, - TabPanel, - Select, - SelectItem, -} from "@tremor/react"; +import React, { useState, useEffect, useCallback, useRef } from "react" +import { Tab, TabGroup, TabList, TabPanels, TabPanel, Select, SelectItem } from "@tremor/react" -import { message } from "antd"; +import { message } from "antd" import { userInfoCall, @@ -19,48 +11,47 @@ import { UserListResponse, invitationCreateCall, getProxyBaseUrl, -} from "./networking"; -import { Button } from "@tremor/react"; -import CreateUser from "./create_user_button"; -import EditUserModal from "./edit_user"; -import OnboardingModal from "./onboarding_link"; -import { InvitationLink } from "./onboarding_link"; +} from "./networking" +import { Button } from "@tremor/react" +import CreateUser from "./create_user_button" +import EditUserModal from "./edit_user" +import OnboardingModal from "./onboarding_link" +import { InvitationLink } from "./onboarding_link" -import { userDeleteCall } from "./networking"; -import { columns } from "./view_users/columns"; -import { UserDataTable } from "./view_users/table"; -import { UserInfo } from "./view_users/types"; -import SSOSettings from "./SSOSettings"; -import debounce from "lodash/debounce"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { updateExistingKeys } from "@/utils/dataUtils"; -import { useDebouncedState } from '@tanstack/react-pacer/debouncer' +import { userDeleteCall } from "./networking" +import { columns } from "./view_users/columns" +import { UserDataTable } from "./view_users/table" +import { UserInfo } from "./view_users/types" +import SSOSettings from "./SSOSettings" +import debounce from "lodash/debounce" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { updateExistingKeys } from "@/utils/dataUtils" +import { useDebouncedState } from "@tanstack/react-pacer/debouncer" interface ViewUserDashboardProps { - accessToken: string | null; - token: string | null; - keys: any[] | null; - userRole: string | null; - userID: string | null; - teams: any[] | null; - setKeys: React.Dispatch>; + accessToken: string | null + token: string | null + keys: any[] | null + userRole: string | null + userID: string | null + teams: any[] | null + setKeys: React.Dispatch> } interface FilterState { - email: string; - user_id: string; - user_role: string; - sso_user_id: string; - team: string; - model: string; - min_spend: number | null; - max_spend: number | null; - sort_by: string; - sort_order: 'asc' | 'desc'; + email: string + user_id: string + user_role: string + sso_user_id: string + team: string + model: string + min_spend: number | null + max_spend: number | null + sort_by: string + sort_order: "asc" | "desc" } - -const DEFAULT_PAGE_SIZE = 25; +const DEFAULT_PAGE_SIZE = 25 const initialFilters: FilterState = { email: "", @@ -72,36 +63,28 @@ const initialFilters: FilterState = { min_spend: null, max_spend: null, sort_by: "created_at", - sort_order: "desc" + sort_order: "desc", } -const ViewUserDashboard: React.FC = ({ - accessToken, - token, - userRole, - userID, - teams, -}) => { - const queryClient = useQueryClient(); - const [currentPage, setCurrentPage] = useState(1); - const [editModalVisible, setEditModalVisible] = useState(false); - const [selectedUser, setSelectedUser] = useState(null); - const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); - const [userToDelete, setUserToDelete] = useState(null); - const [activeTab, setActiveTab] = useState("users"); - const [filters, setFilters] = useState(initialFilters); +const ViewUserDashboard: React.FC = ({ accessToken, token, userRole, userID, teams }) => { + const queryClient = useQueryClient() + const [currentPage, setCurrentPage] = useState(1) + const [editModalVisible, setEditModalVisible] = useState(false) + const [selectedUser, setSelectedUser] = useState(null) + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) + const [userToDelete, setUserToDelete] = useState(null) + const [activeTab, setActiveTab] = useState("users") + const [filters, setFilters] = useState(initialFilters) const [debouncedFilters, setDebouncedFilters, debouncer] = useDebouncedState(filters, { wait: 300 }) - const [showFilters, setShowFilters] = useState(false); - const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = - useState(false); - const [invitationLinkData, setInvitationLinkData] = - useState(null); - const [baseUrl, setBaseUrl] = useState(null); + const [showFilters, setShowFilters] = useState(false) + const [isInvitationLinkModalVisible, setIsInvitationLinkModalVisible] = useState(false) + const [invitationLinkData, setInvitationLinkData] = useState(null) + const [baseUrl, setBaseUrl] = useState(null) const handleDelete = (userId: string) => { - setUserToDelete(userId); - setIsDeleteModalOpen(true); - }; + setUserToDelete(userId) + setIsDeleteModalOpen(true) + } useEffect(() => { return () => { @@ -110,106 +93,106 @@ const ViewUserDashboard: React.FC = ({ }, [debouncer]) useEffect(() => { - setBaseUrl(getProxyBaseUrl()); - }, []); + setBaseUrl(getProxyBaseUrl()) + }, []) const updateFilters = (update: Partial) => { setFilters((previousFilters) => { - const newFilters = {...previousFilters, ...update }; - setDebouncedFilters(newFilters); - return newFilters; + const newFilters = { ...previousFilters, ...update } + setDebouncedFilters(newFilters) + return newFilters }) - }; + } - const handleSortChange = (sortBy: string, sortOrder: 'asc' | 'desc') => { - updateFilters({ sort_by: sortBy, sort_order: sortOrder }); - }; + const handleSortChange = (sortBy: string, sortOrder: "asc" | "desc") => { + updateFilters({ sort_by: sortBy, sort_order: sortOrder }) + } const handleResetPassword = async (userId: string) => { if (!accessToken) { - message.error("Access token not found"); - return; + message.error("Access token not found") + return } try { - message.success("Generating password reset link..."); - const data = await invitationCreateCall(accessToken, userId); - setInvitationLinkData(data); - setIsInvitationLinkModalVisible(true); + message.success("Generating password reset link...") + const data = await invitationCreateCall(accessToken, userId) + setInvitationLinkData(data) + setIsInvitationLinkModalVisible(true) } catch (error) { - message.error("Failed to generate password reset link"); + message.error("Failed to generate password reset link") } - }; + } const confirmDelete = async () => { if (userToDelete && accessToken) { try { - await userDeleteCall(accessToken, [userToDelete]); + await userDeleteCall(accessToken, [userToDelete]) // Update the user list after deletion - queryClient.setQueriesData({ queryKey: ['userList'] }, (previousData) => { - if (previousData === undefined) return previousData; - const updatedUsers = previousData.users.filter(user => user.user_id !== userToDelete); - return { ...previousData, users: updatedUsers }; + queryClient.setQueriesData({ queryKey: ["userList"] }, (previousData) => { + if (previousData === undefined) return previousData + const updatedUsers = previousData.users.filter((user) => user.user_id !== userToDelete) + return { ...previousData, users: updatedUsers } }) - - message.success("User deleted successfully"); + + message.success("User deleted successfully") } catch (error) { - console.error("Error deleting user:", error); - message.error("Failed to delete user"); + console.error("Error deleting user:", error) + message.error("Failed to delete user") } } - setIsDeleteModalOpen(false); - setUserToDelete(null); - }; + setIsDeleteModalOpen(false) + setUserToDelete(null) + } const cancelDelete = () => { - setIsDeleteModalOpen(false); - setUserToDelete(null); - }; + setIsDeleteModalOpen(false) + setUserToDelete(null) + } const handleEditCancel = async () => { - setSelectedUser(null); - setEditModalVisible(false); - }; + setSelectedUser(null) + setEditModalVisible(false) + } const handleEditSubmit = async (editedUser: any) => { - console.log("inside handleEditSubmit:", editedUser); + console.log("inside handleEditSubmit:", editedUser) if (!accessToken || !token || !userRole || !userID) { - return; + return } try { - const response = await userUpdateUserCall(accessToken, editedUser, null); - queryClient.setQueriesData({ queryKey: ['userList'] }, (previousData) => { - if (previousData === undefined) return previousData; - const updatedUsers = previousData.users.map(user => { + const response = await userUpdateUserCall(accessToken, editedUser, null) + queryClient.setQueriesData({ queryKey: ["userList"] }, (previousData) => { + if (previousData === undefined) return previousData + const updatedUsers = previousData.users.map((user) => { if (user.user_id === response.data.user_id) { - return updateExistingKeys(user, response.data); + return updateExistingKeys(user, response.data) } - return user; - }); - - return { ...previousData, users: updatedUsers }; + return user + }) + + return { ...previousData, users: updatedUsers } }) - message.success(`User ${editedUser.user_id} updated successfully`); + message.success(`User ${editedUser.user_id} updated successfully`) } catch (error) { - console.error("There was an error updating the user", error); + console.error("There was an error updating the user", error) } - setSelectedUser(null); - setEditModalVisible(false); + setSelectedUser(null) + setEditModalVisible(false) // Close the modal - }; + } const handlePageChange = async (newPage: number) => { - setCurrentPage(newPage); - }; + setCurrentPage(newPage) + } const userListQuery = useQuery({ - queryKey: ['userList', { debouncedFilter: debouncedFilters, currentPage }], + queryKey: ["userList", { debouncedFilter: debouncedFilters, currentPage }], queryFn: async () => { - if (!accessToken) throw new Error('Access token required'); + if (!accessToken) throw new Error("Access token required") return await userListCall( accessToken, @@ -221,23 +204,23 @@ const ViewUserDashboard: React.FC = ({ debouncedFilters.team || null, debouncedFilters.sso_user_id || null, debouncedFilters.sort_by, - debouncedFilters.sort_order - ); + debouncedFilters.sort_order, + ) }, enabled: Boolean(accessToken && token && userRole && userID), - placeholderData: (previousData) => previousData - }); + placeholderData: (previousData) => previousData, + }) const userListResponse = userListQuery.data const userRolesQuery = useQuery>>({ - queryKey: ['userRoles'], + queryKey: ["userRoles"], initialData: () => ({}), queryFn: async () => { - if (!accessToken) throw new Error('Access token required'); - return await getPossibleUserRoles(accessToken); + if (!accessToken) throw new Error("Access token required") + return await getPossibleUserRoles(accessToken) }, enabled: Boolean(accessToken && token && userRole && userID), - }); + }) const possibleUIRoles = userRolesQuery.data if (userListQuery.isLoading) { @@ -245,40 +228,34 @@ const ViewUserDashboard: React.FC = ({ } if (!accessToken || !token || !userRole || !userID) { - return
Loading...
; + return
Loading...
} const tableColumns = columns( possibleUIRoles, (user) => { - setSelectedUser(user); - setEditModalVisible(true); + setSelectedUser(user) + setEditModalVisible(true) }, handleDelete, handleResetPassword, - () => {} // placeholder function, will be overridden in UserDataTable - ); + () => {}, // placeholder function, will be overridden in UserDataTable + ) return (
-

Users

- +
- + setActiveTab(index === 0 ? "users" : "settings")}> Users Default User Settings - +
@@ -312,15 +289,10 @@ const ViewUserDashboard: React.FC = ({ {/* Filter Button */}
- + {/* SSO ID Search */}
= ({ placeholder="Filter by SSO ID" className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500" value={filters.sso_user_id} - onChange={(e) => updateFilters({ sso_user_id : e.target.value })} + onChange={(e) => updateFilters({ sso_user_id: e.target.value })} />
@@ -437,23 +404,18 @@ const ViewUserDashboard: React.FC = ({ : 0}{" "} -{" "} {userListResponse && userListResponse.users - ? Math.min( - userListResponse.page * userListResponse.page_size, - userListResponse.total - ) + ? Math.min(userListResponse.page * userListResponse.page_size, userListResponse.total) : 0}{" "} of {userListResponse ? userListResponse.total : 0} results - + {/* Pagination Buttons */}
- + - + @@ -515,18 +482,12 @@ const ViewUserDashboard: React.FC = ({ {isDeleteModalOpen && (
-