diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index 6d38a7d70b..cf2b21dc83 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -17,6 +17,7 @@ "@tremor/react": "^3.13.3", "@types/papaparse": "^5.3.15", "antd": "^5.13.2", + "cva": "^1.0.0-beta.3", "fs": "^0.0.1-security", "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", @@ -28,7 +29,8 @@ "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18", "react-markdown": "^9.0.1", - "react-syntax-highlighter": "^15.6.1" + "react-syntax-highlighter": "^15.6.1", + "tailwind-merge": "^3.2.0" }, "devDependencies": { "@tailwindcss/forms": "^0.5.7", @@ -869,6 +871,16 @@ "react-dom": ">=16.6.0" } }, + "node_modules/@tremor/react/node_modules/tailwind-merge": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", + "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/@types/d3-array": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", @@ -1854,9 +1866,10 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -1953,6 +1966,26 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" }, + "node_modules/cva": { + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/cva/-/cva-1.0.0-beta.3.tgz", + "integrity": "sha512-CZa8pTkpEygxJRLH9aod/wfnSgK5z/0GJqG/NNehlwam+S8llqCWUXS3eCenvAiW5sTUpwTWE6bJaeeZ/b4pzA==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + }, + "peerDependencies": { + "typescript": ">= 4.5.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -7195,9 +7228,10 @@ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, "node_modules/tailwind-merge": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", - "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", + "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" @@ -7452,7 +7486,7 @@ "version": "5.3.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", - "dev": true, + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index b7455f7ab1..4864bc2ca0 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -18,6 +18,7 @@ "@tremor/react": "^3.13.3", "@types/papaparse": "^5.3.15", "antd": "^5.13.2", + "cva": "^1.0.0-beta.3", "fs": "^0.0.1-security", "jsonwebtoken": "^9.0.2", "jwt-decode": "^4.0.0", @@ -29,7 +30,8 @@ "react-copy-to-clipboard": "^5.1.0", "react-dom": "^18", "react-markdown": "^9.0.1", - "react-syntax-highlighter": "^15.6.1" + "react-syntax-highlighter": "^15.6.1", + "tailwind-merge": "^3.2.0" }, "devDependencies": { "@tailwindcss/forms": "^0.5.7", diff --git a/ui/litellm-dashboard/src/app/page.tsx b/ui/litellm-dashboard/src/app/page.tsx index 10df55cda5..bcfdfc280b 100644 --- a/ui/litellm-dashboard/src/app/page.tsx +++ b/ui/litellm-dashboard/src/app/page.tsx @@ -26,7 +26,7 @@ import ChatUI from "@/components/chat_ui"; import Sidebar from "@/components/leftnav"; import Usage from "@/components/usage"; import CacheDashboard from "@/components/cache_dashboard"; -import { setGlobalLitellmHeaderName } from "@/components/networking"; +import { proxyBaseUrl, setGlobalLitellmHeaderName } from "@/components/networking"; import { Organization } from "@/components/networking"; import GuardrailsPanel from "@/components/guardrails"; import TransformRequestPanel from "@/components/transform_request"; @@ -34,6 +34,8 @@ import { fetchUserModels } from "@/components/create_key_button"; import { fetchTeams } from "@/components/common_components/fetch_teams"; import MCPToolsViewer from "@/components/mcp_tools"; import TagManagement from "@/components/tag_management"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; +import { cx } from '@/lib/cva.config'; function getCookie(name: string) { const cookieValue = document.cookie @@ -79,6 +81,21 @@ interface ProxySettings { const queryClient = new QueryClient(); +function LoadingScreen() { + return ( +
+
+ 🚅 LiteLLM +
+ +
+ + Loading... +
+
+ ); +} + export default function CreateKeyPage() { const [userRole, setUserRole] = useState(""); const [premiumUser, setPremiumUser] = useState(false); @@ -98,6 +115,7 @@ export default function CreateKeyPage() { const searchParams = useSearchParams()!; const [modelData, setModelData] = useState({ data: [] }); const [token, setToken] = useState(null); + const [authLoading, setAuthLoading] = useState(true); const [userID, setUserID] = useState(null); const invitation_id = searchParams.get("invitation_id"); @@ -124,8 +142,15 @@ export default function CreateKeyPage() { useEffect(() => { const token = getCookie("token"); setToken(token); + setAuthLoading(false); }, []); + useEffect(() => { + if (authLoading === false && token === null) { + window.location.href = (proxyBaseUrl || "") + "/sso/key/generate" + } + }, [token, authLoading]) + useEffect(() => { if (!token) { return; @@ -196,9 +221,12 @@ export default function CreateKeyPage() { } }, [accessToken, userID, userRole]); + if (authLoading || (authLoading == false && token === null)) { + return + } return ( - Loading...}> + }> {invitation_id ? ( ; + +export function UiLoadingSpinner({ className = "", ...props }: LoadingSpinnerProps) { + const id = useId(); + + useSafeLayoutEffect(() => { + const animations = document + .getAnimations() + .filter((a) => a instanceof CSSAnimation && a.animationName === 'spin') as CSSAnimation[]; + + const self = animations.find( + (a) => (a.effect as KeyframeEffect).target?.getAttribute('data-spinner-id') === id, + ); + + const anyOther = animations.find( + (a) => + a.effect instanceof KeyframeEffect && + a.effect.target?.getAttribute('data-spinner-id') !== id, + ); + + if (self && anyOther) { + self.currentTime = anyOther.currentTime; + } + }, [id]); + + return ( + + + + + ); +} diff --git a/ui/litellm-dashboard/src/hooks/use-safe-layout-effect.ts b/ui/litellm-dashboard/src/hooks/use-safe-layout-effect.ts new file mode 100644 index 0000000000..1ccc961534 --- /dev/null +++ b/ui/litellm-dashboard/src/hooks/use-safe-layout-effect.ts @@ -0,0 +1,7 @@ +import { DependencyList, EffectCallback, useEffect, useLayoutEffect } from 'react'; + +export function useSafeLayoutEffect(effect: EffectCallback, deps?: DependencyList) { + const isSSR = typeof window === 'undefined'; + const safeUseLayoutEffect = isSSR ? useEffect : useLayoutEffect; + return safeUseLayoutEffect(effect, deps); +} diff --git a/ui/litellm-dashboard/src/lib/cva.config.ts b/ui/litellm-dashboard/src/lib/cva.config.ts new file mode 100644 index 0000000000..dbb90c3e2d --- /dev/null +++ b/ui/litellm-dashboard/src/lib/cva.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'cva'; +import { twMerge } from 'tailwind-merge'; + +export const { cva, cx, compose } = defineConfig({ + hooks: { + onComplete: (className) => twMerge(className), + }, +});