diff --git a/ui/litellm-dashboard/.prettierignore b/ui/litellm-dashboard/.prettierignore new file mode 100644 index 00000000000..ab37c884be1 --- /dev/null +++ b/ui/litellm-dashboard/.prettierignore @@ -0,0 +1,11 @@ +node_modules +.next +.out +dist +build +.coverage +.vercel +.turbo +.next-static +*.min.js +coverage/ \ No newline at end of file diff --git a/ui/litellm-dashboard/.prettierrc b/ui/litellm-dashboard/.prettierrc new file mode 100644 index 00000000000..6ce0705cb78 --- /dev/null +++ b/ui/litellm-dashboard/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "printWidth": 120, + "trailingComma": "all" +} diff --git a/ui/litellm-dashboard/.prettierrc.json b/ui/litellm-dashboard/.prettierrc.json deleted file mode 100644 index 69cb9796325..00000000000 --- a/ui/litellm-dashboard/.prettierrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "semi": false, - "tabWidth": 2, - "printWidth": 120, - "trailingComma": "all", - "jsxBracketSameLine": false -} \ No newline at end of file diff --git a/ui/litellm-dashboard/next.config.mjs b/ui/litellm-dashboard/next.config.mjs index 540849269e5..f3083c5e802 100644 --- a/ui/litellm-dashboard/next.config.mjs +++ b/ui/litellm-dashboard/next.config.mjs @@ -1,12 +1,12 @@ /** @type {import('next').NextConfig} */ const nextConfig = { - output: 'export', - basePath: '', - assetPrefix: '/litellm-asset-prefix', // If a server_root_path is set, this will be overridden by runtime injection + output: "export", + basePath: "", + assetPrefix: "/litellm-asset-prefix", // If a server_root_path is set, this will be overridden by runtime injection }; nextConfig.experimental = { - missingSuspenseWithCSRBailout: false -} + missingSuspenseWithCSRBailout: false, +}; export default nextConfig; diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index 9ab39c57bfd..b4aa6fc6b76 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -17973,6 +17973,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index 30c35591d6d..bddc866d744 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -8,7 +8,9 @@ "start": "next start", "lint": "next lint", "test": "vitest", - "test:watch": "vitest -w" + "test:watch": "vitest -w", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "dependencies": { "@anthropic-ai/sdk": "^0.54.0", diff --git a/ui/litellm-dashboard/src/app/globals.css b/ui/litellm-dashboard/src/app/globals.css index 8b9aa9e6706..a702982678e 100644 --- a/ui/litellm-dashboard/src/app/globals.css +++ b/ui/litellm-dashboard/src/app/globals.css @@ -19,12 +19,7 @@ body { color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); + background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb)); } @layer utilities { diff --git a/ui/litellm-dashboard/src/app/model_hub/page.tsx b/ui/litellm-dashboard/src/app/model_hub/page.tsx index 76e9fc342b8..265fe485ec4 100644 --- a/ui/litellm-dashboard/src/app/model_hub/page.tsx +++ b/ui/litellm-dashboard/src/app/model_hub/page.tsx @@ -19,7 +19,5 @@ export default function PublicModelHub() { * populate navbar * */ - return ( - - ); + return ; } diff --git a/ui/litellm-dashboard/src/app/model_hub_table/page.tsx b/ui/litellm-dashboard/src/app/model_hub_table/page.tsx index 196279a7852..d10f11d61bf 100644 --- a/ui/litellm-dashboard/src/app/model_hub_table/page.tsx +++ b/ui/litellm-dashboard/src/app/model_hub_table/page.tsx @@ -19,7 +19,5 @@ export default function PublicModelHubTable() { * populate navbar * */ - return ( - - ); -} \ No newline at end of file + return ; +} diff --git a/ui/litellm-dashboard/src/app/onboarding/page.tsx b/ui/litellm-dashboard/src/app/onboarding/page.tsx index 4e2f8903188..f37d69f7989 100644 --- a/ui/litellm-dashboard/src/app/onboarding/page.tsx +++ b/ui/litellm-dashboard/src/app/onboarding/page.tsx @@ -1,16 +1,7 @@ "use client"; import React, { Suspense, useEffect, useState } from "react"; import { useSearchParams } from "next/navigation"; -import { - Card, - Title, - Text, - TextInput, - Callout, - Button, - Grid, - Col, -} from "@tremor/react"; +import { Card, Title, Text, TextInput, Callout, Button, Grid, Col } from "@tremor/react"; import { RiAlarmWarningLine, RiCheckboxCircleLine } from "@remixicon/react"; import { invitationClaimCall, @@ -18,7 +9,7 @@ import { getOnboardingCredentials, claimOnboardingToken, getUiConfig, - getProxyBaseUrl + getProxyBaseUrl, } from "@/components/networking"; import { jwtDecode } from "jwt-decode"; import { Form, Button as Button2, message } from "antd"; @@ -27,7 +18,7 @@ import { getCookie } from "@/utils/cookieUtils"; export default function Onboarding() { const [form] = Form.useForm(); const searchParams = useSearchParams()!; - const token = getCookie('token'); + const token = getCookie("token"); const inviteID = searchParams.get("invitation_id"); const action = searchParams.get("action"); const [accessToken, setAccessToken] = useState(null); @@ -39,14 +30,16 @@ export default function Onboarding() { const [getUiConfigLoading, setGetUiConfigLoading] = useState(true); useEffect(() => { - getUiConfig().then((data) => { // get the information for constructing the proxy base url, and then set the token and auth loading + getUiConfig().then((data) => { + // get the information for constructing the proxy base url, and then set the token and auth loading console.log("ui config in onboarding.tsx:", data); setGetUiConfigLoading(false); }); }, []); useEffect(() => { - if (!inviteID || getUiConfigLoading) { // wait for the ui config to be loaded + if (!inviteID || getUiConfigLoading) { + // wait for the ui config to be loaded return; } @@ -72,14 +65,7 @@ export default function Onboarding() { }, [inviteID, getUiConfigLoading]); const handleSubmit = (formValues: Record) => { - console.log( - "in handle submit. accessToken:", - accessToken, - "token:", - jwtToken, - "formValues:", - formValues - ); + console.log("in handle submit. accessToken:", accessToken, "token:", jwtToken, "formValues:", formValues); if (!accessToken || !jwtToken) { return; } @@ -89,12 +75,7 @@ export default function Onboarding() { if (!userID || !inviteID) { return; } - claimOnboardingToken( - accessToken, - inviteID, - userID, - formValues.password - ).then((data) => { + claimOnboardingToken(accessToken, inviteID, userID, formValues.password).then((data) => { let litellm_dashboard_ui = "/ui/"; litellm_dashboard_ui += "?login=success"; @@ -119,15 +100,14 @@ export default function Onboarding() { 🚅 LiteLLM {action === "reset_password" ? "Reset Password" : "Sign up"} - {action === "reset_password" ? "Reset your password to access Admin UI." : "Claim your user account to login to Admin UI."} + + {action === "reset_password" + ? "Reset your password to access Admin UI." + : "Claim your user account to login to Admin UI."} + {action !== "reset_password" && ( - + SSO is under the Enterprise Tier. @@ -142,28 +122,16 @@ export default function Onboarding() { )} -
+ <> - + diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 0503e5c6086..579e90e530c 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -1,85 +1,85 @@ -"use client" +"use client"; -import React, { Suspense, useEffect, useState } from "react" -import { useSearchParams } from "next/navigation" -import { jwtDecode } from "jwt-decode" -import { QueryClient, QueryClientProvider } from "@tanstack/react-query" -import { Team } from "@/components/key_team_helpers/key_list" -import Navbar from "@/components/navbar" -import { ThemeProvider } from "@/contexts/ThemeContext" -import UserDashboard from "@/components/user_dashboard" -import ModelDashboard from "@/components/templates/model_dashboard" -import ViewUserDashboard from "@/components/view_users" -import Teams from "@/components/teams" -import Organizations from "@/components/organizations" -import { fetchOrganizations } from "@/components/organizations" -import AdminPanel from "@/components/admins" -import Settings from "@/components/settings" -import GeneralSettings from "@/components/general_settings" -import PassThroughSettings from "@/components/pass_through_settings" -import BudgetPanel from "@/components/budgets/budget_panel" -import SpendLogsTable from "@/components/view_logs" -import ModelHubTable from "@/components/model_hub_table" -import NewUsagePage from "@/components/new_usage" -import APIRef from "@/components/api_ref" -import ChatUI from "@/components/chat_ui/ChatUI" -import Sidebar from "@/components/leftnav" -import Usage from "@/components/usage" -import CacheDashboard from "@/components/cache_dashboard" -import { getUiConfig, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking" -import { Organization } from "@/components/networking" -import GuardrailsPanel from "@/components/guardrails" -import PromptsPanel from "@/components/prompts" -import TransformRequestPanel from "@/components/transform_request" -import { fetchUserModels } from "@/components/organisms/create_key_button" -import { fetchTeams } from "@/components/common_components/fetch_teams" -import { MCPServers } from "@/components/mcp_tools" -import TagManagement from "@/components/tag_management" -import VectorStoreManagement from "@/components/vector_store_management" -import UIThemeSettings from "@/components/ui_theme_settings" -import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner" -import { cx } from "@/lib/cva.config" +import React, { Suspense, useEffect, useState } from "react"; +import { useSearchParams } from "next/navigation"; +import { jwtDecode } from "jwt-decode"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { Team } from "@/components/key_team_helpers/key_list"; +import Navbar from "@/components/navbar"; +import { ThemeProvider } from "@/contexts/ThemeContext"; +import UserDashboard from "@/components/user_dashboard"; +import ModelDashboard from "@/components/templates/model_dashboard"; +import ViewUserDashboard from "@/components/view_users"; +import Teams from "@/components/teams"; +import Organizations from "@/components/organizations"; +import { fetchOrganizations } from "@/components/organizations"; +import AdminPanel from "@/components/admins"; +import Settings from "@/components/settings"; +import GeneralSettings from "@/components/general_settings"; +import PassThroughSettings from "@/components/pass_through_settings"; +import BudgetPanel from "@/components/budgets/budget_panel"; +import SpendLogsTable from "@/components/view_logs"; +import ModelHubTable from "@/components/model_hub_table"; +import NewUsagePage from "@/components/new_usage"; +import APIRef from "@/components/api_ref"; +import ChatUI from "@/components/chat_ui/ChatUI"; +import Sidebar from "@/components/leftnav"; +import Usage from "@/components/usage"; +import CacheDashboard from "@/components/cache_dashboard"; +import { getUiConfig, proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking"; +import { Organization } from "@/components/networking"; +import GuardrailsPanel from "@/components/guardrails"; +import PromptsPanel from "@/components/prompts"; +import TransformRequestPanel from "@/components/transform_request"; +import { fetchUserModels } from "@/components/organisms/create_key_button"; +import { fetchTeams } from "@/components/common_components/fetch_teams"; +import { MCPServers } from "@/components/mcp_tools"; +import TagManagement from "@/components/tag_management"; +import VectorStoreManagement from "@/components/vector_store_management"; +import UIThemeSettings from "@/components/ui_theme_settings"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; +import { cx } from "@/lib/cva.config"; function getCookie(name: string) { - const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "=")) - return cookieValue ? cookieValue.split("=")[1] : null + const cookieValue = document.cookie.split("; ").find((row) => row.startsWith(name + "=")); + return cookieValue ? cookieValue.split("=")[1] : null; } function formatUserRole(userRole: string) { if (!userRole) { - return "Undefined Role" + return "Undefined Role"; } switch (userRole.toLowerCase()) { case "app_owner": - return "App Owner" + return "App Owner"; case "demo_app_owner": - return "App Owner" + return "App Owner"; case "app_admin": - return "Admin" + return "Admin"; case "proxy_admin": - return "Admin" + return "Admin"; case "proxy_admin_viewer": - return "Admin Viewer" + return "Admin Viewer"; case "org_admin": - return "Org Admin" + return "Org Admin"; case "internal_user": - return "Internal User" + return "Internal User"; case "internal_user_viewer": case "internal_viewer": // TODO:remove if deprecated - return "Internal Viewer" + return "Internal Viewer"; case "app_user": - return "App User" + return "App User"; default: - return "Unknown Role" + return "Unknown Role"; } } interface ProxySettings { - PROXY_BASE_URL: string - PROXY_LOGOUT_URL: string + PROXY_BASE_URL: string; + PROXY_LOGOUT_URL: string; } -const queryClient = new QueryClient() +const queryClient = new QueryClient(); function LoadingScreen() { return ( @@ -91,51 +91,51 @@ function LoadingScreen() { Loading... - ) + ); } export default function CreateKeyPage() { - const [userRole, setUserRole] = useState("") - const [premiumUser, setPremiumUser] = useState(false) - const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] = useState(false) - const [userEmail, setUserEmail] = useState(null) - const [teams, setTeams] = useState(null) - const [keys, setKeys] = useState([]) - const [organizations, setOrganizations] = useState([]) - const [userModels, setUserModels] = useState([]) + const [userRole, setUserRole] = useState(""); + const [premiumUser, setPremiumUser] = useState(false); + const [disabledPersonalKeyCreation, setDisabledPersonalKeyCreation] = useState(false); + const [userEmail, setUserEmail] = useState(null); + const [teams, setTeams] = useState(null); + const [keys, setKeys] = useState([]); + const [organizations, setOrganizations] = useState([]); + const [userModels, setUserModels] = useState([]); const [proxySettings, setProxySettings] = useState({ PROXY_BASE_URL: "", PROXY_LOGOUT_URL: "", - }) + }); - const [showSSOBanner, setShowSSOBanner] = useState(true) - const searchParams = useSearchParams()! - const [modelData, setModelData] = useState({ data: [] }) - const [token, setToken] = useState(null) - const [createClicked, setCreateClicked] = useState(false) - const [authLoading, setAuthLoading] = useState(true) - const [userID, setUserID] = useState(null) + const [showSSOBanner, setShowSSOBanner] = useState(true); + const searchParams = useSearchParams()!; + const [modelData, setModelData] = useState({ data: [] }); + const [token, setToken] = useState(null); + const [createClicked, setCreateClicked] = useState(false); + const [authLoading, setAuthLoading] = useState(true); + const [userID, setUserID] = useState(null); - const invitation_id = searchParams.get("invitation_id") + const invitation_id = searchParams.get("invitation_id"); // Get page from URL, default to 'api-keys' if not present const [page, setPage] = useState(() => { - return searchParams.get("page") || "api-keys" - }) + return searchParams.get("page") || "api-keys"; + }); // Custom setPage function that updates URL const updatePage = (newPage: string) => { // Update URL without full page reload - const newSearchParams = new URLSearchParams(searchParams) - newSearchParams.set("page", newPage) + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.set("page", newPage); // Use Next.js router to update URL - window.history.pushState(null, "", `?${newSearchParams.toString()}`) + window.history.pushState(null, "", `?${newSearchParams.toString()}`); - setPage(newPage) - } + setPage(newPage); + }; - const [accessToken, setAccessToken] = useState(null) + const [accessToken, setAccessToken] = useState(null); const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const toggleSidebar = () => { @@ -143,83 +143,83 @@ export default function CreateKeyPage() { }; const addKey = (data: any) => { - setKeys((prevData) => (prevData ? [...prevData, data] : [data])) - setCreateClicked(() => !createClicked) - } - const redirectToLogin = authLoading === false && token === null && invitation_id === null + setKeys((prevData) => (prevData ? [...prevData, data] : [data])); + setCreateClicked(() => !createClicked); + }; + const redirectToLogin = authLoading === false && token === null && invitation_id === null; useEffect(() => { - const token = getCookie("token") + const token = getCookie("token"); getUiConfig().then((data) => { // get the information for constructing the proxy base url, and then set the token and auth loading - setToken(token) - setAuthLoading(false) - }) - }, []) + setToken(token); + setAuthLoading(false); + }); + }, []); useEffect(() => { if (redirectToLogin) { - window.location.href = (proxyBaseUrl || "") + "/sso/key/generate" + window.location.href = (proxyBaseUrl || "") + "/sso/key/generate"; } - }, [redirectToLogin]) + }, [redirectToLogin]); useEffect(() => { if (!token) { - return + return; } - const decoded = jwtDecode(token) as { [key: string]: any } + const decoded = jwtDecode(token) as { [key: string]: any }; if (decoded) { // set accessToken - setAccessToken(decoded.key) + setAccessToken(decoded.key); - setDisabledPersonalKeyCreation(decoded.disabled_non_admin_personal_key_creation) + setDisabledPersonalKeyCreation(decoded.disabled_non_admin_personal_key_creation); // check if userRole is defined if (decoded.user_role) { - const formattedUserRole = formatUserRole(decoded.user_role) - setUserRole(formattedUserRole) + const formattedUserRole = formatUserRole(decoded.user_role); + setUserRole(formattedUserRole); if (formattedUserRole == "Admin Viewer") { - setPage("usage") + setPage("usage"); } } if (decoded.user_email) { - setUserEmail(decoded.user_email) + setUserEmail(decoded.user_email); } if (decoded.login_method) { - setShowSSOBanner(decoded.login_method == "username_password" ? true : false) + setShowSSOBanner(decoded.login_method == "username_password" ? true : false); } if (decoded.premium_user) { - setPremiumUser(decoded.premium_user) + setPremiumUser(decoded.premium_user); } if (decoded.auth_header_name) { - setGlobalLitellmHeaderName(decoded.auth_header_name) + setGlobalLitellmHeaderName(decoded.auth_header_name); } if (decoded.user_id) { - setUserID(decoded.user_id) + setUserID(decoded.user_id); } } - }, [token]) + }, [token]); useEffect(() => { if (accessToken && userID && userRole) { - fetchUserModels(userID, userRole, accessToken, setUserModels) + fetchUserModels(userID, userRole, accessToken, setUserModels); } if (accessToken && userID && userRole) { - fetchTeams(accessToken, userID, userRole, null, setTeams) + fetchTeams(accessToken, userID, userRole, null, setTeams); } if (accessToken) { - fetchOrganizations(accessToken, setOrganizations) + fetchOrganizations(accessToken, setOrganizations); } - }, [accessToken, userID, userRole]) + }, [accessToken, userID, userRole]); if (authLoading || redirectToLogin) { - return + return ; } return ( @@ -425,5 +425,5 @@ export default function CreateKeyPage() { - ) + ); } diff --git a/ui/litellm-dashboard/src/components/SCIM.tsx b/ui/litellm-dashboard/src/components/SCIM.tsx index fa083c43818..5ccfecaf197 100644 --- a/ui/litellm-dashboard/src/components/SCIM.tsx +++ b/ui/litellm-dashboard/src/components/SCIM.tsx @@ -1,24 +1,14 @@ import React, { useState, useEffect } from "react"; -import { - Card, - Title, - Text, - Grid, - Col, - Button as TremorButton, - Callout, - TextInput, - Divider, -} from "@tremor/react"; +import { Card, Title, Text, Grid, Col, Button as TremorButton, Callout, TextInput, Divider } from "@tremor/react"; import { message, Form } from "antd"; import { keyCreateCall } from "./networking"; import { CopyToClipboard } from "react-copy-to-clipboard"; -import { - LinkOutlined, - KeyOutlined, +import { + LinkOutlined, + KeyOutlined, CopyOutlined, ExclamationCircleOutlined, - PlusCircleOutlined + PlusCircleOutlined, } from "@ant-design/icons"; import { parseErrorMessage } from "./shared/errorUtils"; import NotificationsManager from "./molecules/notifications_manager"; @@ -34,38 +24,38 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti const [isCreatingToken, setIsCreatingToken] = useState(false); const [tokenData, setTokenData] = useState(null); const [baseUrl, setBaseUrl] = useState(""); - + useEffect(() => { let url = ""; - + if (proxySettings && proxySettings.PROXY_BASE_URL && proxySettings.PROXY_BASE_URL !== undefined) { url = proxySettings.PROXY_BASE_URL; - } else if (typeof window !== 'undefined') { + } else if (typeof window !== "undefined") { // Use the current origin as the base URL if no proxy URL is set url = window.location.origin; } - + setBaseUrl(url); }, [proxySettings]); - + const scimBaseUrl = `${baseUrl}/scim/v2`; - + const handleCreateSCIMToken = async (values: any) => { if (!accessToken || !userID) { NotificationsManager.fromBackend("You need to be logged in to create a SCIM token"); return; } - + try { setIsCreatingToken(true); - + const formData = { key_alias: values.key_alias || "SCIM Access Token", team_id: null, models: [], allowed_routes: ["/scim/*"], }; - + const response = await keyCreateCall(accessToken, userID, formData); setTokenData(response); NotificationsManager.success("SCIM token created successfully"); @@ -84,11 +74,12 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti SCIM Configuration - System for Cross-domain Identity Management (SCIM) allows you to automatically provision and manage users and groups in LiteLLM. + System for Cross-domain Identity Management (SCIM) allows you to automatically provision and manage users and + groups in LiteLLM. - + - +
{/* Step 1: SCIM URL */}
@@ -105,11 +96,7 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti Use this URL in your identity provider SCIM integration settings.
- + NotificationsManager.success("URL copied to clipboard")} @@ -133,18 +120,15 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti Authentication Token
- + - You need a SCIM token to authenticate with the SCIM API. Create one below and use it in your SCIM provider configuration. + You need a SCIM token to authenticate with the SCIM API. Create one below and use it in your SCIM provider + configuration. {!tokenData ? (
- + = ({ accessToken, userID, proxySetti
- setTokenData(null)} - > + setTokenData(null)}> Create Another Token @@ -208,4 +188,4 @@ const SCIMConfig: React.FC = ({ accessToken, userID, proxySetti ); }; -export default SCIMConfig; \ No newline at end of file +export default SCIMConfig; diff --git a/ui/litellm-dashboard/src/components/SSOModals.tsx b/ui/litellm-dashboard/src/components/SSOModals.tsx index 56ec9393fa4..44b09dc4db0 100644 --- a/ui/litellm-dashboard/src/components/SSOModals.tsx +++ b/ui/litellm-dashboard/src/components/SSOModals.tsx @@ -38,56 +38,64 @@ interface SSOProviderConfig { const ssoProviderConfigs: Record = { google: { envVarMap: { - google_client_id: 'GOOGLE_CLIENT_ID', - google_client_secret: 'GOOGLE_CLIENT_SECRET', + google_client_id: "GOOGLE_CLIENT_ID", + google_client_secret: "GOOGLE_CLIENT_SECRET", }, fields: [ - { label: 'GOOGLE CLIENT ID', name: 'google_client_id' }, - { label: 'GOOGLE CLIENT SECRET', name: 'google_client_secret' }, + { label: "GOOGLE CLIENT ID", name: "google_client_id" }, + { label: "GOOGLE CLIENT SECRET", name: "google_client_secret" }, ], }, microsoft: { envVarMap: { - microsoft_client_id: 'MICROSOFT_CLIENT_ID', - microsoft_client_secret: 'MICROSOFT_CLIENT_SECRET', - microsoft_tenant: 'MICROSOFT_TENANT', + microsoft_client_id: "MICROSOFT_CLIENT_ID", + microsoft_client_secret: "MICROSOFT_CLIENT_SECRET", + microsoft_tenant: "MICROSOFT_TENANT", }, fields: [ - { label: 'MICROSOFT CLIENT ID', name: 'microsoft_client_id' }, - { label: 'MICROSOFT CLIENT SECRET', name: 'microsoft_client_secret' }, - { label: 'MICROSOFT TENANT', name: 'microsoft_tenant' }, + { label: "MICROSOFT CLIENT ID", name: "microsoft_client_id" }, + { label: "MICROSOFT CLIENT SECRET", name: "microsoft_client_secret" }, + { label: "MICROSOFT TENANT", name: "microsoft_tenant" }, ], }, okta: { envVarMap: { - generic_client_id: 'GENERIC_CLIENT_ID', - generic_client_secret: 'GENERIC_CLIENT_SECRET', - generic_authorization_endpoint: 'GENERIC_AUTHORIZATION_ENDPOINT', - generic_token_endpoint: 'GENERIC_TOKEN_ENDPOINT', - generic_userinfo_endpoint: 'GENERIC_USERINFO_ENDPOINT', + generic_client_id: "GENERIC_CLIENT_ID", + generic_client_secret: "GENERIC_CLIENT_SECRET", + generic_authorization_endpoint: "GENERIC_AUTHORIZATION_ENDPOINT", + generic_token_endpoint: "GENERIC_TOKEN_ENDPOINT", + generic_userinfo_endpoint: "GENERIC_USERINFO_ENDPOINT", }, fields: [ - { label: 'GENERIC CLIENT ID', name: 'generic_client_id' }, - { label: 'GENERIC CLIENT SECRET', name: 'generic_client_secret' }, - { label: 'AUTHORIZATION ENDPOINT', name: 'generic_authorization_endpoint', placeholder: 'https://your-okta-domain/authorize' }, - { label: 'TOKEN ENDPOINT', name: 'generic_token_endpoint', placeholder: 'https://your-okta-domain/token' }, - { label: 'USERINFO ENDPOINT', name: 'generic_userinfo_endpoint', placeholder: 'https://your-okta-domain/userinfo' }, + { label: "GENERIC CLIENT ID", name: "generic_client_id" }, + { label: "GENERIC CLIENT SECRET", name: "generic_client_secret" }, + { + label: "AUTHORIZATION ENDPOINT", + name: "generic_authorization_endpoint", + placeholder: "https://your-okta-domain/authorize", + }, + { label: "TOKEN ENDPOINT", name: "generic_token_endpoint", placeholder: "https://your-okta-domain/token" }, + { + label: "USERINFO ENDPOINT", + name: "generic_userinfo_endpoint", + placeholder: "https://your-okta-domain/userinfo", + }, ], }, generic: { envVarMap: { - generic_client_id: 'GENERIC_CLIENT_ID', - generic_client_secret: 'GENERIC_CLIENT_SECRET', - generic_authorization_endpoint: 'GENERIC_AUTHORIZATION_ENDPOINT', - generic_token_endpoint: 'GENERIC_TOKEN_ENDPOINT', - generic_userinfo_endpoint: 'GENERIC_USERINFO_ENDPOINT', + generic_client_id: "GENERIC_CLIENT_ID", + generic_client_secret: "GENERIC_CLIENT_SECRET", + generic_authorization_endpoint: "GENERIC_AUTHORIZATION_ENDPOINT", + generic_token_endpoint: "GENERIC_TOKEN_ENDPOINT", + generic_userinfo_endpoint: "GENERIC_USERINFO_ENDPOINT", }, fields: [ - { label: 'GENERIC CLIENT ID', name: 'generic_client_id' }, - { label: 'GENERIC CLIENT SECRET', name: 'generic_client_secret' }, - { label: 'AUTHORIZATION ENDPOINT', name: 'generic_authorization_endpoint' }, - { label: 'TOKEN ENDPOINT', name: 'generic_token_endpoint' }, - { label: 'USERINFO ENDPOINT', name: 'generic_userinfo_endpoint' }, + { label: "GENERIC CLIENT ID", name: "generic_client_id" }, + { label: "GENERIC CLIENT SECRET", name: "generic_client_secret" }, + { label: "AUTHORIZATION ENDPOINT", name: "generic_authorization_endpoint" }, + { label: "TOKEN ENDPOINT", name: "generic_token_endpoint" }, + { label: "USERINFO ENDPOINT", name: "generic_userinfo_endpoint" }, ], }, }; @@ -116,20 +124,22 @@ const SSOModals: React.FC = ({ if (ssoData && ssoData.values) { console.log("SSO values:", ssoData.values); // Debug log console.log("user_email from API:", ssoData.values.user_email); // Debug log - + // Determine which SSO provider is configured let selectedProvider = null; if (ssoData.values.google_client_id) { - selectedProvider = 'google'; + selectedProvider = "google"; } else if (ssoData.values.microsoft_client_id) { - selectedProvider = 'microsoft'; + selectedProvider = "microsoft"; } else if (ssoData.values.generic_client_id) { // Check if it looks like Okta based on endpoints - if (ssoData.values.generic_authorization_endpoint?.includes('okta') || - ssoData.values.generic_authorization_endpoint?.includes('auth0')) { - selectedProvider = 'okta'; + if ( + ssoData.values.generic_authorization_endpoint?.includes("okta") || + ssoData.values.generic_authorization_endpoint?.includes("auth0") + ) { + selectedProvider = "okta"; } else { - selectedProvider = 'generic'; + selectedProvider = "generic"; } } @@ -142,7 +152,7 @@ const SSOModals: React.FC = ({ }; console.log("Setting form values:", formValues); // Debug log - + // Clear form first, then set values with a small delay to ensure proper initialization form.resetFields(); setTimeout(() => { @@ -169,7 +179,7 @@ const SSOModals: React.FC = ({ try { // Save SSO settings using the new API await updateSSOSettings(accessToken, formValues); - + // Continue with the original flow (show instructions) handleShowInstructions(formValues); } catch (error) { @@ -204,16 +214,16 @@ const SSOModals: React.FC = ({ }; await updateSSOSettings(accessToken, clearSettings); - + // Clear the form form.resetFields(); - + // Close the confirmation modal setIsClearConfirmModalVisible(false); - + // Close the main SSO modal and trigger refresh handleAddSSOOk(); - + NotificationsManager.success("SSO settings cleared successfully"); } catch (error) { console.error("Failed to clear SSO settings:", error); @@ -233,11 +243,7 @@ const SSOModals: React.FC = ({ name={field.name} rules={[{ required: true, message: `Please enter the ${field.label.toLowerCase()}` }]} > - {field.name.includes('client') ? ( - - ) : ( - - )} + {field.name.includes("client") ? : } )); }; @@ -268,8 +274,14 @@ const SSOModals: React.FC = ({ updateTeam(index, "user_role", value)} > @@ -199,13 +202,8 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles,
))} - - @@ -214,17 +212,13 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles, const renderEditableField = (key: string, property: any, value: any) => { const type = property.type; - + if (key === "teams") { - return ( -
- {renderTeamsEditor(editedValues[key] || [])} -
- ); + return
{renderTeamsEditor(editedValues[key] || [])}
; } else if (key === "user_role" && possibleUIRoles) { return ( handleTextInputChange(key, value)} className="mt-2" > {property.items.enum.map((option: string) => ( - + ))} ); @@ -276,7 +269,7 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles, return ( handleTextInputChange(key, value)} className="mt-2" > {property.enum.map((option: string) => ( - + ))} ); } else { return ( - handleTextInputChange(key, e.target.value)} placeholder={property.description || ""} className="mt-2" @@ -316,12 +311,12 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles, const renderValue = (key: string, value: any): JSX.Element => { if (value === null || value === undefined) return Not set; - + if (key === "teams" && Array.isArray(value)) { if (value.length === 0) return No teams assigned; - + const normalizedTeams = normalizeTeams(value); - + return (
{normalizedTeams.map((team, index) => ( @@ -334,8 +329,8 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles,
Max Budget:

- {team.max_budget_in_team !== undefined - ? `$${formatNumberWithCommas(team.max_budget_in_team, 4)}` + {team.max_budget_in_team !== undefined + ? `$${formatNumberWithCommas(team.max_budget_in_team, 4)}` : "No limit"}

@@ -349,7 +344,7 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles,
); } - + if (key === "user_role" && possibleUIRoles && possibleUIRoles[value]) { const { ui_label, description } = possibleUIRoles[value]; return ( @@ -359,18 +354,18 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles, ); } - + if (key === "budget_duration") { return {getBudgetDurationLabel(value)}; } - + if (typeof value === "boolean") { return {value ? "Enabled" : "Disabled"}; } - + if (key === "models" && Array.isArray(value)) { if (value.length === 0) return None; - + return (
{value.map((model, index) => ( @@ -381,11 +376,11 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles,
); } - + if (typeof value === "object") { if (Array.isArray(value)) { if (value.length === 0) return None; - + return (
{value.map((item, index) => ( @@ -396,14 +391,10 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles,
); } - - return ( -
-          {JSON.stringify(value, null, 2)}
-        
- ); + + return
{JSON.stringify(value, null, 2)}
; } - + return {String(value)}; }; @@ -426,30 +417,26 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles, // Dynamically render settings based on the schema const renderSettings = () => { const { values, field_schema } = settings; - + if (!field_schema || !field_schema.properties) { return No schema information available; } return Object.entries(field_schema.properties).map(([key, property]: [string, any]) => { const value = values[key]; - const displayName = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); - + const displayName = key.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()); + return (
{displayName} {property.description || "No description available"} - + {isEditing ? ( -
- {renderEditableField(key, property, value)} -
+
{renderEditableField(key, property, value)}
) : ( -
- {renderValue(key, value)} -
+
{renderValue(key, value)}
)}
); @@ -460,10 +447,11 @@ const SSOSettings: React.FC = ({ accessToken, possibleUIRoles,
Default User Settings - {!loading && settings && ( - isEditing ? ( + {!loading && + settings && + (isEditing ? (
- -
) : ( - - ) - )} + + ))}
- + {settings?.field_schema?.description && ( {settings.field_schema.description} )} - -
- {renderSettings()} -
+ +
{renderSettings()}
); }; -export default SSOSettings; \ No newline at end of file +export default SSOSettings; diff --git a/ui/litellm-dashboard/src/components/TeamSSOSettings.tsx b/ui/litellm-dashboard/src/components/TeamSSOSettings.tsx index 79f02d93f1a..4d20630e9f6 100644 --- a/ui/litellm-dashboard/src/components/TeamSSOSettings.tsx +++ b/ui/litellm-dashboard/src/components/TeamSSOSettings.tsx @@ -33,7 +33,7 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID, const data = await getDefaultTeamSettings(accessToken); setSettings(data); setEditedValues(data.values || {}); - + // Fetch available models if (accessToken) { try { @@ -59,11 +59,11 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID, const handleSaveSettings = async () => { if (!accessToken) return; - + setSaving(true); try { const updatedSettings = await updateDefaultTeamSettings(accessToken, editedValues); - setSettings({...settings, values: updatedSettings.settings}); + setSettings({ ...settings, values: updatedSettings.settings }); setIsEditing(false); NotificationsManager.success("Default team settings updated successfully"); } catch (error) { @@ -77,13 +77,13 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID, const handleTextInputChange = (key: string, value: any) => { setEditedValues((prev: Record) => ({ ...prev, - [key]: value + [key]: value, })); }; const renderEditableField = (key: string, property: any, value: any) => { const type = property.type; - + if (key === "budget_duration") { return ( = ({ accessToken, userID, } else if (type === "boolean") { return (
- handleTextInputChange(key, checked)} - /> + handleTextInputChange(key, checked)} />
); } else if (type === "array" && property.items?.enum) { return ( ); @@ -119,7 +118,7 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID, return ( handleTextInputChange(key, value)} className="mt-2" > {property.enum.map((option: string) => ( - + ))} ); } else { return ( - handleTextInputChange(key, e.target.value)} placeholder={property.description || ""} className="mt-2" @@ -158,18 +159,18 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID, const renderValue = (key: string, value: any): JSX.Element => { if (value === null || value === undefined) return Not set; - + if (key === "budget_duration") { return {getBudgetDurationLabel(value)}; } - + if (typeof value === "boolean") { return {value ? "Enabled" : "Disabled"}; } - + if (key === "models" && Array.isArray(value)) { if (value.length === 0) return None; - + return (
{value.map((model, index) => ( @@ -180,11 +181,11 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID,
); } - + if (typeof value === "object") { if (Array.isArray(value)) { if (value.length === 0) return None; - + return (
{value.map((item, index) => ( @@ -195,14 +196,10 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID,
); } - - return ( -
-          {JSON.stringify(value, null, 2)}
-        
- ); + + return
{JSON.stringify(value, null, 2)}
; } - + return {String(value)}; }; @@ -225,30 +222,26 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID, // Dynamically render settings based on the schema const renderSettings = () => { const { values, field_schema } = settings; - + if (!field_schema || !field_schema.properties) { return No schema information available; } return Object.entries(field_schema.properties).map(([key, property]: [string, any]) => { const value = values[key]; - const displayName = key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); - + const displayName = key.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase()); + return (
{displayName} {property.description || "No description available"} - + {isEditing ? ( -
- {renderEditableField(key, property, value)} -
+
{renderEditableField(key, property, value)}
) : ( -
- {renderValue(key, value)} -
+
{renderValue(key, value)}
)}
); @@ -259,10 +252,11 @@ const TeamSSOSettings: React.FC = ({ accessToken, userID,
Default Team Settings - {!loading && settings && ( - isEditing ? ( + {!loading && + settings && + (isEditing ? (
- -
) : ( - - ) - )} + + ))}
- - - These settings will be applied by default when creating new teams. - - + + These settings will be applied by default when creating new teams. + {settings?.field_schema?.description && ( {settings.field_schema.description} )} - -
- {renderSettings()} -
+ +
{renderSettings()}
); }; -export default TeamSSOSettings; \ No newline at end of file +export default TeamSSOSettings; diff --git a/ui/litellm-dashboard/src/components/UIAccessControlForm.tsx b/ui/litellm-dashboard/src/components/UIAccessControlForm.tsx index 5ef75d3835c..dacae9e9fcf 100644 --- a/ui/litellm-dashboard/src/components/UIAccessControlForm.tsx +++ b/ui/litellm-dashboard/src/components/UIAccessControlForm.tsx @@ -24,14 +24,14 @@ const UIAccessControlForm: React.FC = ({ accessToken, // Handle nested ui_access_mode structure const uiAccessMode = ssoData.values.ui_access_mode; let formValues = {}; - - if (uiAccessMode && typeof uiAccessMode === 'object') { + + if (uiAccessMode && typeof uiAccessMode === "object") { formValues = { ui_access_mode_type: uiAccessMode.type, restricted_sso_group: uiAccessMode.restricted_sso_group, sso_group_jwt_field: uiAccessMode.sso_group_jwt_field, }; - } else if (typeof uiAccessMode === 'string') { + } else if (typeof uiAccessMode === "string") { // Handle legacy flat structure formValues = { ui_access_mode_type: uiAccessMode, @@ -39,7 +39,7 @@ const UIAccessControlForm: React.FC = ({ accessToken, sso_group_jwt_field: ssoData.values.team_ids_jwt_field || ssoData.values.sso_group_jwt_field, }; } - + form.setFieldsValue(formValues); } } catch (error) { @@ -61,11 +61,11 @@ const UIAccessControlForm: React.FC = ({ accessToken, try { // Transform form data to match API expected structure let apiPayload; - - if (formValues.ui_access_mode_type === 'all_authenticated_users') { + + if (formValues.ui_access_mode_type === "all_authenticated_users") { // Set ui_access_mode to none when all_authenticated_users is selected apiPayload = { - ui_access_mode: "none" + ui_access_mode: "none", }; } else { apiPayload = { @@ -73,7 +73,7 @@ const UIAccessControlForm: React.FC = ({ accessToken, type: formValues.ui_access_mode_type, restricted_sso_group: formValues.restricted_sso_group, sso_group_jwt_field: formValues.sso_group_jwt_field, - } + }, }; } @@ -88,23 +88,15 @@ const UIAccessControlForm: React.FC = ({ accessToken, }; return ( -
-
- +
+
+ Configure who can access the UI interface and how group information is extracted from JWT tokens.
- - - + + + ({ value: name, label: name }))} + options={guardrailsList.map((name) => ({ value: name, label: name }))} /> - {customPricing && (
- +