From 43337912e1c7be3de83a5381614a776f424ae917 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Apr 2026 12:47:36 +0000 Subject: [PATCH] Migrate top-level pages to shadcn (Phase 1) - LoginPage.tsx: antd Form/Input/Select/Popover/Card/Alert/Space/Typography -> shadcn/Tailwind equivalents. Preserves SSO popover, worker selector, admin-disabled view, and credentials info banner. All 8 existing tests still pass. - OnboardingFormBody.tsx: antd Form.useForm -> react-hook-form; antd Input.Password -> shadcn Input type=password. Retains aria-label='loading' spinner so existing test selector continues to work. - src/app/page.tsx: drop antd ConfigProvider/theme wrapper; dark-mode state now toggles the 'dark' class on so shadcn CSS variables switch. Removed unused QueryClient imports. Adds src/app/onboarding/OnboardingFormBody.tsx to the raw-tailwind-colors lint exception list (SSO info-banner uses bg-blue-50 per existing design pattern). Co-authored-by: yuneng-jiang --- ui/litellm-dashboard/.eslintrc.json | 1 + .../src/app/login/LoginPage.tsx | 298 ++++++++++-------- .../src/app/onboarding/OnboardingFormBody.tsx | 99 ++++-- ui/litellm-dashboard/src/app/page.tsx | 16 +- 4 files changed, 240 insertions(+), 174 deletions(-) diff --git a/ui/litellm-dashboard/.eslintrc.json b/ui/litellm-dashboard/.eslintrc.json index 98a8ff7fe7a..26c573efd0f 100644 --- a/ui/litellm-dashboard/.eslintrc.json +++ b/ui/litellm-dashboard/.eslintrc.json @@ -136,6 +136,7 @@ "src/components/prompts/prompt_editor_view/VersionHistorySidePanel.tsx", "src/app/(dashboard)/teams/components/TeamsTable/ModelsCell.tsx", "src/app/(dashboard)/teams/components/TeamsTable/TeamsTable.tsx", + "src/app/onboarding/OnboardingFormBody.tsx", "src/components/prompts/prompt_editor_view/conversation_panel/MessageBubble.tsx", "src/components/playground/chat_ui/SessionManagement.tsx", "src/components/playground/chat_ui/SearchResultsDisplay.tsx", diff --git a/ui/litellm-dashboard/src/app/login/LoginPage.tsx b/ui/litellm-dashboard/src/app/login/LoginPage.tsx index f8ee570cd19..99482b59220 100644 --- a/ui/litellm-dashboard/src/app/login/LoginPage.tsx +++ b/ui/litellm-dashboard/src/app/login/LoginPage.tsx @@ -7,8 +7,24 @@ import { exchangeLoginCode, getProxyBaseUrl, switchToWorkerUrl } from "@/compone import { clearTokenCookies, getCookie } from "@/utils/cookieUtils"; import { isJwtExpired } from "@/utils/jwtUtils"; import { consumeReturnUrl, getReturnUrl, isValidReturnUrl } from "@/utils/returnUrlUtils"; -import { Info as InfoCircleOutlined, Server as CloudServerOutlined } from "lucide-react"; -import { Alert, Button, Card, Form, Input, Popover, Select, Space, Typography } from "antd"; +import { Info, Server, LoaderCircle } from "lucide-react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { useWorker } from "@/hooks/useWorker"; @@ -115,7 +131,8 @@ function LoginPageContent() { setIsLoading(false); }, [isConfigLoading, router, uiConfig]); - const handleSubmit = () => { + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); // If a worker is selected, point proxyBaseUrl at it before login const selectedWorker = workers.find((w) => w.worker_id === selectedWorkerId); if (selectedWorker) { @@ -154,8 +171,6 @@ function LoginPageContent() { const error = loginMutation.error instanceof Error ? loginMutation.error.message : null; const isLoginLoading = loginMutation.isPending; - const { Title, Text, Paragraph } = Typography; - if (isConfigLoading || isLoading) { return ; } @@ -163,175 +178,192 @@ function LoginPageContent() { // Show disabled message if admin UI is disabled if (uiConfig && uiConfig.admin_ui_disabled) { return ( -
- - +
+ +
- 🚅 LiteLLM +

🚅 LiteLLM

- - - The Admin UI has been disabled by the administrator. To re-enable it, please update the following - environment variable: - - - DISABLE_ADMIN_UI=False - - - } - type="warning" - showIcon - /> - + + + Admin UI Disabled + +

+ The Admin UI has been disabled by the administrator. To re-enable it, please update the following + environment variable: +

+

+ DISABLE_ADMIN_UI=False +

+
+
+
); } return ( -
- - +
+ +
- 🚅 LiteLLM +

🚅 LiteLLM

- Login - Access your LiteLLM Admin UI. +

Login

+

Access your LiteLLM Admin UI.

- - - By default, Username is admin and - Password is your set LiteLLM Proxy - MASTER_KEY. - - - Need to set UI credentials or SSO?{" "} - - Check the documentation - - . - - - } - type="info" - icon={} - showIcon - /> + + + Default Credentials + +

+ By default, Username is admin and + Password is your set LiteLLM Proxy{" "} + MASTER_KEY. +

+

+ Need to set UI credentials or SSO?{" "} + + Check the documentation + + . +

+
+
- {error && } + {error && ( + + {error} + + )} -
+ {uiConfig?.is_control_plane && workers.length > 0 && ( - +
+ +
)} - +
+ setUsername(e.target.value)} disabled={isLoginLoading} - size="large" - className="rounded-md border-gray-300" + required + className="h-11" /> - +
- - + + setPassword(e.target.value)} disabled={isLoginLoading} - size="large" + required + className="h-11" /> - +
- + + + {!uiConfig?.sso_configured ? ( + + + + + + + + Please configure SSO to log in with SSO. + + + ) : ( - - - {!uiConfig?.sso_configured ? ( - - - - ) : ( - - )} - - - + )} + +
{uiConfig?.sso_configured && ( - Single Sign-On (SSO) is enabled. LiteLLM no longer automatically redirects to the SSO login flow upon loading this page. To re-enable auto-redirect-to-SSO, set AUTO_REDIRECT_UI_LOGIN_TO_SSO=true in your environment configuration.} - /> + + + + Single Sign-On (SSO) is enabled. LiteLLM no longer automatically redirects to the SSO login flow upon + loading this page. To re-enable auto-redirect-to-SSO, set{" "} + AUTO_REDIRECT_UI_LOGIN_TO_SSO=true in your + environment configuration. + + )}
diff --git a/ui/litellm-dashboard/src/app/onboarding/OnboardingFormBody.tsx b/ui/litellm-dashboard/src/app/onboarding/OnboardingFormBody.tsx index 8038c33e1f0..a4c1d661ca3 100644 --- a/ui/litellm-dashboard/src/app/onboarding/OnboardingFormBody.tsx +++ b/ui/litellm-dashboard/src/app/onboarding/OnboardingFormBody.tsx @@ -1,9 +1,10 @@ import React from "react"; +import { useForm } from "react-hook-form"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; -import { Info } from "lucide-react"; -import { Form, Input as AntInput } from "antd"; +import { Label } from "@/components/ui/label"; +import { Info, LoaderCircle } from "lucide-react"; type OnboardingFormBodyProps = { variant: "signup" | "reset_password"; @@ -13,6 +14,11 @@ type OnboardingFormBodyProps = { onSubmit: (values: { password: string }) => void; }; +type FormValues = { + user_email: string; + password: string; +}; + export function OnboardingFormBody({ variant, userEmail, @@ -20,11 +26,18 @@ export function OnboardingFormBody({ claimError, onSubmit, }: OnboardingFormBodyProps) { - const [form] = Form.useForm(); + const { + register, + handleSubmit, + formState: { errors }, + setValue, + } = useForm({ + defaultValues: { user_email: userEmail, password: "" }, + }); React.useEffect(() => { - if (userEmail) form.setFieldValue("user_email", userEmail); - }, [userEmail, form]); + if (userEmail) setValue("user_email", userEmail); + }, [userEmail, setValue]); return (
@@ -62,48 +75,64 @@ export function OnboardingFormBody({
)} -
onSubmit({ password: values.password })} + + onSubmit({ password: values.password }), + )} > - - - +
+ + +
- - - +
+ + + {errors.password ? ( +

+ {errors.password.message as string} +

+ ) : ( +

+ {variant === "reset_password" + ? "Enter your new password" + : "Create a password for your account"} +

+ )} +
{claimError && ( -
+
{claimError}
)} -
+
- +
); diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 4312148d532..496038411bc 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -46,11 +46,9 @@ import { clearTokenCookies, getCookie } from "@/utils/cookieUtils"; import { isJwtExpired } from "@/utils/jwtUtils"; import { buildLoginUrlWithReturn, consumeReturnUrl, isValidReturnUrl, normalizeUrlForCompare, storeReturnUrl } from "@/utils/returnUrlUtils"; import { formatUserRole, isAdminRole } from "@/utils/roles"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { jwtDecode } from "jwt-decode"; import { useRouter, useSearchParams } from "next/navigation"; import { Suspense, useEffect, useMemo, useRef, useState } from "react"; -import { ConfigProvider, theme } from "antd"; function deleteCookie(name: string, path = "/") { // Best-effort client-side clear (works for non-HttpOnly cookies without Domain) @@ -114,6 +112,16 @@ function CreateKeyPageContent() { setIsDarkMode(!isDarkMode); }; + // Apply dark-mode class to the root element so shadcn CSS variables switch. + useEffect(() => { + const root = document.documentElement; + if (isDarkMode) { + root.classList.add("dark"); + } else { + root.classList.remove("dark"); + } + }, [isDarkMode]); + const invitation_id = searchParams.get("invitation_id"); // Parse URL query parameters for pre-filling the create key form @@ -451,9 +459,6 @@ function CreateKeyPageContent() { return ( }> - {invitation_id ? ( )} - ); }