diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 09d0032e6c9..ae09a893194 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1061,7 +1061,17 @@ "count": 1 } }, - "src/app/(dashboard)/models-and-endpoints/layout.tsx": { + "src/app/(dashboard)/models-and-endpoints/page.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/models-and-endpoints/panels/LlmCredentialsPanel.tsx": { "no-restricted-imports": { "count": 1 } @@ -4340,4 +4350,4 @@ "count": 1 } } -} \ No newline at end of file +} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/layout.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/layout.test.tsx deleted file mode 100644 index d47d8b9a04e..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/layout.test.tsx +++ /dev/null @@ -1,126 +0,0 @@ -/* @vitest-environment jsdom */ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { act, render } from "@testing-library/react"; -import { beforeEach, describe, expect, it, vi } from "vitest"; -import ModelsAndEndpointsLayout from "./layout"; - -const { mockPush, mockReplace, navState } = vi.hoisted(() => ({ - mockPush: vi.fn(), - mockReplace: vi.fn(), - navState: { pathname: "/models-and-endpoints", search: "" }, -})); -vi.mock("next/navigation", () => ({ - usePathname: () => navState.pathname, - useRouter: () => ({ push: mockPush, replace: mockReplace }), - useSearchParams: () => new URLSearchParams(navState.search), -})); - -vi.mock("@/components/networking", () => ({ serverRootPath: "" })); - -vi.mock("@/components/molecules/cost_optimization_feedback_banner", () => ({ default: () => null })); -vi.mock("@/components/model_info_view", () => ({ - default: ({ modelId }: { modelId: string }) =>
model:{modelId}
, -})); -vi.mock("@/components/team/TeamInfo", () => ({ - default: ({ teamId }: { teamId: string }) =>
team:{teamId}
, -})); - -const mockUseAuthorized = vi.fn(); -vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ default: () => mockUseAuthorized() })); -vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({ useTeams: () => ({ data: [] }) })); -vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({ - useUISettings: () => ({ data: { values: {} } }), -})); -vi.mock("@/app/(dashboard)/models-and-endpoints/useModelDashboardData", () => ({ - useModelDashboardData: () => ({ - availableModelGroups: [], - availableModelAccessGroups: [], - allModelsOnProxy: [], - isLoading: false, - }), -})); - -const renderLayout = () => { - const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } }); - return render( - - -
CHILD
-
-
, - ); -}; - -describe("ModelsAndEndpointsLayout", () => { - beforeEach(() => { - navState.pathname = "/models-and-endpoints"; - navState.search = ""; - mockPush.mockClear(); - mockReplace.mockClear(); - mockUseAuthorized.mockReturnValue({ - accessToken: "123", - token: "123", - userRole: "Admin", - userId: "123", - premiumUser: false, - }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (global as any).ResizeObserver = class { - observe() {} - unobserve() {} - disconnect() {} - }; - }); - - it("renders the admin tab bar and the active tab's page content", () => { - const { getByRole, getByTestId } = renderLayout(); - expect(getByRole("tab", { name: "LLM Credentials" })).toBeInTheDocument(); - expect(getByRole("tab", { name: "Health Status" })).toBeInTheDocument(); - expect(getByTestId("tab-content")).toHaveTextContent("CHILD"); - }); - - it("navigates to a tab's path when its tab is clicked", async () => { - const { getByRole } = renderLayout(); - await act(async () => { - getByRole("tab", { name: "Health Status" }).click(); - }); - expect(mockPush).toHaveBeenCalledWith(expect.stringMatching(/\/models-and-endpoints\/health\/$/)); - }); - - it("redirects to the base models path when the tab path is not permitted for the role", async () => { - const replaceMock = vi.fn(); - const originalLocation = window.location; - Object.defineProperty(window, "location", { - configurable: true, - value: { replace: replaceMock, assign: vi.fn(), href: "http://localhost/", pathname: "/", search: "" }, - }); - mockUseAuthorized.mockReturnValue({ - accessToken: "123", - token: "123", - userRole: "Internal User", - userId: "123", - premiumUser: false, - }); - navState.pathname = "/models-and-endpoints/llm-credentials"; - await act(async () => { - renderLayout(); - }); - expect(replaceMock).toHaveBeenCalledWith(expect.stringMatching(/\/models-and-endpoints\/$/)); - Object.defineProperty(window, "location", { configurable: true, value: originalLocation }); - }); - - it("renders the model detail overlay from ?model and hides the tabs and page content", () => { - navState.search = "model=abc-123"; - const { getByTestId, queryByTestId, queryByRole } = renderLayout(); - expect(getByTestId("model-info")).toHaveTextContent("model:abc-123"); - expect(queryByTestId("tab-content")).toBeNull(); - expect(queryByRole("tab", { name: "Health Status" })).toBeNull(); - }); - - it("renders the team detail overlay from ?team", () => { - navState.search = "team=team-9"; - const { getByTestId, queryByTestId } = renderLayout(); - expect(getByTestId("team-info")).toHaveTextContent("team:team-9"); - expect(queryByTestId("tab-content")).toBeNull(); - }); -}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/layout.tsx deleted file mode 100644 index e855ebeb5c8..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/layout.tsx +++ /dev/null @@ -1,155 +0,0 @@ -"use client"; - -import type { ReactNode } from "react"; -import { useMemo, useState } from "react"; -import { Tabs } from "antd"; -import { RefreshIcon } from "@heroicons/react/outline"; -import { useQueryClient } from "@tanstack/react-query"; -import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; -import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"; -import { useTabRouting } from "@/app/(dashboard)/hooks/useTabRouting"; -import { all_admin_roles, internalUserRoles, isProxyAdminRole, isUserTeamAdminForAnyTeam } from "@/utils/roles"; -import CostOptimizationFeedbackBanner from "@/components/molecules/cost_optimization_feedback_banner"; -import ModelInfoView from "@/components/model_info_view"; -import TeamInfoView from "@/components/team/TeamInfo"; -import { modelsRoutes, type ModelTabSlug } from "@/app/(dashboard)/models-and-endpoints/tabRoutes"; -import { useModelDetailRouting } from "@/app/(dashboard)/models-and-endpoints/detailNavigation"; -import { useModelDashboardData } from "@/app/(dashboard)/models-and-endpoints/useModelDashboardData"; - -const BASE_TAB_KEY = "all-models"; - -const TAB_LABELS: Record = { - add: "Add Model", - "llm-credentials": "LLM Credentials", - "pass-through": "Pass-Through Endpoints", - health: "Health Status", - "retry-settings": "Model Retry Settings", - "model-group-alias": "Model Group Alias", - "price-data": "Price Data Reload", -}; - -export default function ModelsAndEndpointsLayout({ children }: { children: ReactNode }) { - const { accessToken, userRole, userId: userID, premiumUser } = useAuthorized(); - const { data: teams, isLoading: teamsLoading } = useTeams(); - const { data: uiSettings, isLoading: uiSettingsLoading } = useUISettings(); - const queryClient = useQueryClient(); - const { modelId, teamId, close } = useModelDetailRouting(); - const { availableModelAccessGroups, allModelsOnProxy } = useModelDashboardData(); - - const [lastRefreshed, setLastRefreshed] = useState(""); - - const isProxyAdmin = userRole && isProxyAdminRole(userRole); - const isInternalUser = userRole && internalUserRoles.includes(userRole); - const isUserTeamAdmin = userID && isUserTeamAdminForAnyTeam(teams ?? null, userID); - const addModelDisabledForInternalUsers = - isInternalUser && uiSettings?.values?.disable_model_add_for_internal_users === true; - const shouldHideAddModelTab = !isProxyAdmin && (addModelDisabledForInternalUsers || !isUserTeamAdmin); - const isAdmin = all_admin_roles.includes(userRole); - - const visibleSlugs = useMemo>( - () => [ - "", - ...(shouldHideAddModelTab ? [] : (["add"] as const)), - ...(isAdmin - ? (["llm-credentials", "pass-through", "health", "retry-settings", "model-group-alias", "price-data"] as const) - : []), - ], - [shouldHideAddModelTab, isAdmin], - ); - - const tabRoutingConfig = { - routes: modelsRoutes, - baseTabKey: BASE_TAB_KEY, - visibleKeys: visibleSlugs.filter(Boolean), - ready: !teamsLoading && !uiSettingsLoading, - }; - const { activeKey, onTabChange } = useTabRouting(tabRoutingConfig); - - const allModelsLabel = isAdmin ? "All Models" : "Your Models"; - const tabItems = visibleSlugs.map((slug) => { - const key = slug || BASE_TAB_KEY; - return { - key, - label: slug ? TAB_LABELS[slug] : allModelsLabel, - children: key === activeKey ? children : null, - }; - }); - - const handleRefreshClick = () => { - setLastRefreshed(new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })); - queryClient.invalidateQueries({ queryKey: ["models", "list"] }); - }; - - const invalidateModels = () => queryClient.invalidateQueries({ queryKey: ["models", "list"] }); - - if (teamId) { - return ( -
- -
- ); - } - - return ( -
-
-
-
-

Model Management

- {isAdmin ? ( -

Add and manage models for the proxy

- ) : ( -

Add models for teams you are an admin for.

- )} -
-
- - - - {modelId ? ( - - ) : ( - - {lastRefreshed && Last Refreshed: {lastRefreshed}} - -
- ), - }} - /> - )} -
- - ); -} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.test.tsx new file mode 100644 index 00000000000..b9dd09d5a71 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.test.tsx @@ -0,0 +1,100 @@ +/* @vitest-environment jsdom */ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import ModelsAndEndpointsPage from "./page"; + +vi.mock("./panels/AllModelsPanel", () => ({ default: () =>
})); +vi.mock("./panels/AddModelPanel", () => ({ default: () =>
})); +vi.mock("./panels/LlmCredentialsPanel", () => ({ default: () =>
})); +vi.mock("./panels/PassThroughPanel", () => ({ default: () =>
})); +vi.mock("./panels/HealthStatusPanel", () => ({ default: () =>
})); +vi.mock("./panels/ModelRetrySettingsPanel", () => ({ default: () =>
})); +vi.mock("./panels/ModelGroupAliasPanel", () => ({ default: () =>
})); +vi.mock("./panels/PriceDataPanel", () => ({ default: () =>
})); + +const detailState = { modelId: null as string | null, teamId: null as string | null }; +vi.mock("./detailNavigation", () => ({ + useModelDetailRouting: () => ({ ...detailState, close: vi.fn(), openModel: vi.fn(), openTeam: vi.fn() }), +})); + +vi.mock("@/components/molecules/cost_optimization_feedback_banner", () => ({ default: () => null })); +vi.mock("@/components/model_info_view", () => ({ + default: ({ modelId }: { modelId: string }) =>
model:{modelId}
, +})); +vi.mock("@/components/team/TeamInfo", () => ({ + default: ({ teamId }: { teamId: string }) =>
team:{teamId}
, +})); + +const mockUseAuthorized = vi.fn(); +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ default: () => mockUseAuthorized() })); +vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({ useTeams: () => ({ data: [] }) })); +vi.mock("@/app/(dashboard)/hooks/uiSettings/useUISettings", () => ({ + useUISettings: () => ({ data: { values: {} } }), +})); +vi.mock("./useModelDashboardData", () => ({ + useModelDashboardData: () => ({ availableModelAccessGroups: [], allModelsOnProxy: [], availableModelGroups: [] }), +})); + +const ADMIN = { accessToken: "at", token: "t", userRole: "Admin", userId: "u1", premiumUser: false }; +const NON_ADMIN = { accessToken: "at", token: "t", userRole: "Internal User", userId: "u1", premiumUser: false }; + +const renderPage = () => { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, gcTime: 0 } } }); + return render( + + + , + ); +}; + +describe("ModelsAndEndpointsPage", () => { + beforeEach(() => { + detailState.modelId = null; + detailState.teamId = null; + mockUseAuthorized.mockReturnValue(ADMIN); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (global as any).ResizeObserver = class { + observe() {} + unobserve() {} + disconnect() {} + }; + }); + + it("renders the admin tab bar and the All Models panel by default", () => { + const { getByRole, getByTestId } = renderPage(); + expect(getByRole("tab", { name: "All Models" })).toBeInTheDocument(); + expect(getByRole("tab", { name: "LLM Credentials" })).toBeInTheDocument(); + expect(getByRole("tab", { name: "Health Status" })).toBeInTheDocument(); + expect(getByTestId("panel-all-models")).toBeInTheDocument(); + }); + + it("switches tabs in-memory, mounting only the active panel", async () => { + const user = userEvent.setup(); + const { getByRole, getByTestId, queryByTestId } = renderPage(); + await user.click(getByRole("tab", { name: "Health Status" })); + expect(getByTestId("panel-health")).toBeInTheDocument(); + expect(queryByTestId("panel-all-models")).toBeNull(); + }); + + it("renders the model detail overlay from the ?model drill-in and hides the tabs", () => { + detailState.modelId = "abc-123"; + const { getByTestId, queryByRole } = renderPage(); + expect(getByTestId("model-info")).toHaveTextContent("model:abc-123"); + expect(queryByRole("tab", { name: "All Models" })).toBeNull(); + }); + + it("renders the team detail overlay from the ?team drill-in", () => { + detailState.teamId = "team-9"; + const { getByTestId } = renderPage(); + expect(getByTestId("team-info")).toHaveTextContent("team:team-9"); + }); + + it("hides admin-only tabs for a non-admin user", () => { + mockUseAuthorized.mockReturnValue(NON_ADMIN); + const { queryByRole } = renderPage(); + expect(queryByRole("tab", { name: "LLM Credentials" })).toBeNull(); + expect(queryByRole("tab", { name: "Health Status" })).toBeNull(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx index 546309cfcc1..cb173367459 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/page.tsx @@ -1,23 +1,185 @@ "use client"; -import { useState } from "react"; -import AllModelsTab from "@/app/(dashboard)/models-and-endpoints/components/AllModelsTab"; -import { useModelDashboardData } from "@/app/(dashboard)/models-and-endpoints/useModelDashboardData"; +import { useMemo, useState } from "react"; +import { Tabs } from "antd"; +import { RefreshIcon } from "@heroicons/react/outline"; +import { useQueryClient } from "@tanstack/react-query"; +import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; +import { useUISettings } from "@/app/(dashboard)/hooks/uiSettings/useUISettings"; +import { all_admin_roles, internalUserRoles, isProxyAdminRole, isUserTeamAdminForAnyTeam } from "@/utils/roles"; +import CostOptimizationFeedbackBanner from "@/components/molecules/cost_optimization_feedback_banner"; +import ModelInfoView from "@/components/model_info_view"; +import TeamInfoView from "@/components/team/TeamInfo"; import { useModelDetailRouting } from "@/app/(dashboard)/models-and-endpoints/detailNavigation"; +import { useModelDashboardData } from "@/app/(dashboard)/models-and-endpoints/useModelDashboardData"; +import AllModelsPanel from "@/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel"; +import AddModelPanel from "@/app/(dashboard)/models-and-endpoints/panels/AddModelPanel"; +import LlmCredentialsPanel from "@/app/(dashboard)/models-and-endpoints/panels/LlmCredentialsPanel"; +import PassThroughPanel from "@/app/(dashboard)/models-and-endpoints/panels/PassThroughPanel"; +import HealthStatusPanel from "@/app/(dashboard)/models-and-endpoints/panels/HealthStatusPanel"; +import ModelRetrySettingsPanel from "@/app/(dashboard)/models-and-endpoints/panels/ModelRetrySettingsPanel"; +import ModelGroupAliasPanel from "@/app/(dashboard)/models-and-endpoints/panels/ModelGroupAliasPanel"; +import PriceDataPanel from "@/app/(dashboard)/models-and-endpoints/panels/PriceDataPanel"; -export default function AllModelsPage() { - const [selectedModelGroup, setSelectedModelGroup] = useState(null); - const { availableModelGroups, availableModelAccessGroups } = useModelDashboardData(); - const { openModel, openTeam } = useModelDetailRouting(); +type ModelTabSlug = + | "add" + | "llm-credentials" + | "pass-through" + | "health" + | "retry-settings" + | "model-group-alias" + | "price-data"; + +const BASE_TAB_KEY = "all-models"; + +const TAB_LABELS: Record = { + add: "Add Model", + "llm-credentials": "LLM Credentials", + "pass-through": "Pass-Through Endpoints", + health: "Health Status", + "retry-settings": "Model Retry Settings", + "model-group-alias": "Model Group Alias", + "price-data": "Price Data Reload", +}; + +const renderPanel = (key: string) => { + switch (key) { + case BASE_TAB_KEY: + return ; + case "add": + return ; + case "llm-credentials": + return ; + case "pass-through": + return ; + case "health": + return ; + case "retry-settings": + return ; + case "model-group-alias": + return ; + case "price-data": + return ; + default: + return null; + } +}; + +export default function ModelsAndEndpointsPage() { + const { accessToken, userRole, userId: userID, premiumUser } = useAuthorized(); + const { data: teams } = useTeams(); + const { data: uiSettings } = useUISettings(); + const queryClient = useQueryClient(); + const { modelId, teamId, close } = useModelDetailRouting(); + const { availableModelAccessGroups, allModelsOnProxy } = useModelDashboardData(); + + const [activeKey, setActiveKey] = useState(BASE_TAB_KEY); + const [lastRefreshed, setLastRefreshed] = useState(""); + + const isProxyAdmin = userRole && isProxyAdminRole(userRole); + const isInternalUser = userRole && internalUserRoles.includes(userRole); + const isUserTeamAdmin = userID && isUserTeamAdminForAnyTeam(teams ?? null, userID); + const addModelDisabledForInternalUsers = + isInternalUser && uiSettings?.values?.disable_model_add_for_internal_users === true; + const shouldHideAddModelTab = !isProxyAdmin && (addModelDisabledForInternalUsers || !isUserTeamAdmin); + const isAdmin = all_admin_roles.includes(userRole); + + const visibleSlugs = useMemo>( + () => [ + "", + ...(shouldHideAddModelTab ? [] : (["add"] as const)), + ...(isAdmin + ? (["llm-credentials", "pass-through", "health", "retry-settings", "model-group-alias", "price-data"] as const) + : []), + ], + [shouldHideAddModelTab, isAdmin], + ); + + const allModelsLabel = isAdmin ? "All Models" : "Your Models"; + const tabItems = visibleSlugs.map((slug) => { + const key = slug || BASE_TAB_KEY; + return { + key, + label: slug ? TAB_LABELS[slug] : allModelsLabel, + children: key === activeKey ? renderPanel(key) : null, + }; + }); + + const handleRefreshClick = () => { + setLastRefreshed(new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })); + queryClient.invalidateQueries({ queryKey: ["models", "list"] }); + }; + + const invalidateModels = () => queryClient.invalidateQueries({ queryKey: ["models", "list"] }); + + if (teamId) { + return ( +
+ +
+ ); + } return ( - +
+
+
+
+

Model Management

+ {isAdmin ? ( +

Add and manage models for the proxy

+ ) : ( +

Add models for teams you are an admin for.

+ )} +
+
+ + + + {modelId ? ( + + ) : ( + + {lastRefreshed && Last Refreshed: {lastRefreshed}} + +
+ ), + }} + /> + )} +
+
); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/add/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.tsx similarity index 98% rename from ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/add/page.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.tsx index 7e60ca58fc3..26dcc60d717 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/add/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AddModelPanel.tsx @@ -13,7 +13,7 @@ import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { vertexCredentialsUploadProps } from "@/app/(dashboard)/models-and-endpoints/vertexCredentialsUpload"; -export default function AddModelPage() { +export default function AddModelPanel() { const { accessToken, userRole } = useAuthorized(); const [form] = Form.useForm(); const queryClient = useQueryClient(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.tsx new file mode 100644 index 00000000000..9d40ea32185 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/AllModelsPanel.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { useState } from "react"; +import AllModelsTab from "@/app/(dashboard)/models-and-endpoints/components/AllModelsTab"; +import { useModelDashboardData } from "@/app/(dashboard)/models-and-endpoints/useModelDashboardData"; +import { useModelDetailRouting } from "@/app/(dashboard)/models-and-endpoints/detailNavigation"; + +export default function AllModelsPanel() { + const [selectedModelGroup, setSelectedModelGroup] = useState(null); + const { availableModelGroups, availableModelAccessGroups } = useModelDashboardData(); + const { openModel, openTeam } = useModelDetailRouting(); + + return ( + + ); +} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/health/page.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/HealthStatusPanel.test.tsx similarity index 93% rename from ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/health/page.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/HealthStatusPanel.test.tsx index 677796957cc..e0f35b5f3b8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/health/page.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/HealthStatusPanel.test.tsx @@ -1,7 +1,7 @@ /* @vitest-environment jsdom */ import { render } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import HealthStatusPage from "./page"; +import HealthStatusPanel from "./HealthStatusPanel"; vi.mock("next/navigation", () => ({ usePathname: () => "/models-and-endpoints/health", @@ -27,7 +27,7 @@ vi.mock("@/app/(dashboard)/hooks/models/useModelCostMap", () => ({ useModelCostM vi.mock("@/app/(dashboard)/hooks/teams/useTeams", () => ({ useTeams: () => ({ data: [] }) })); vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ default: () => ({ accessToken: "123" }) })); -describe("HealthStatusPage", () => { +describe("HealthStatusPanel", () => { beforeEach(() => { mockHealthCheckComponent.mockClear(); }); @@ -44,7 +44,7 @@ describe("HealthStatusPage", () => { isLoading: false, }); - render(); + render(); expect(mockHealthCheckComponent).toHaveBeenCalled(); const props = mockHealthCheckComponent.mock.calls[0][0]; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/health/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/HealthStatusPanel.tsx similarity index 98% rename from ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/health/page.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/HealthStatusPanel.tsx index 8942db5f002..451088c87d8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/health/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/HealthStatusPanel.tsx @@ -13,7 +13,7 @@ import { useModelDetailRouting } from "@/app/(dashboard)/models-and-endpoints/de const HEALTH_PAGE_SIZE = 50; -export default function HealthStatusPage() { +export default function HealthStatusPanel() { const { accessToken } = useAuthorized(); const { data: teams } = useTeams(); const { data: modelCostMapData } = useModelCostMap(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/llm-credentials/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/LlmCredentialsPanel.tsx similarity index 87% rename from ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/llm-credentials/page.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/LlmCredentialsPanel.tsx index 207ced5be0d..6112e6bffbd 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/llm-credentials/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/LlmCredentialsPanel.tsx @@ -4,7 +4,7 @@ import { Form } from "antd"; import CredentialsPanel from "@/components/model_add/CredentialsPanel"; import { vertexCredentialsUploadProps } from "@/app/(dashboard)/models-and-endpoints/vertexCredentialsUpload"; -export default function LlmCredentialsPage() { +export default function LlmCredentialsPanel() { const [form] = Form.useForm(); return ; } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/model-group-alias/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/ModelGroupAliasPanel.tsx similarity index 95% rename from ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/model-group-alias/page.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/ModelGroupAliasPanel.tsx index c06de353ddf..ec808984c34 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/model-group-alias/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/ModelGroupAliasPanel.tsx @@ -5,7 +5,7 @@ import ModelGroupAliasSettings from "@/components/model_group_alias_settings"; import { getCallbacksCall } from "@/components/networking"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -export default function ModelGroupAliasPage() { +export default function ModelGroupAliasPanel() { const { accessToken, userId: userID, userRole } = useAuthorized(); const [modelGroupAlias, setModelGroupAlias] = useState<{ [key: string]: string }>({}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/retry-settings/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/ModelRetrySettingsPanel.tsx similarity index 98% rename from ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/retry-settings/page.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/ModelRetrySettingsPanel.tsx index 6442be3e54d..57e32b5624c 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/retry-settings/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/ModelRetrySettingsPanel.tsx @@ -22,7 +22,7 @@ interface RouterSettings { num_retries?: number | null; } -export default function ModelRetrySettingsPage() { +export default function ModelRetrySettingsPanel() { const { accessToken, userId: userID, userRole } = useAuthorized(); const { availableModelGroups } = useModelDashboardData(); const updateRetryPolicy = useUpdateRetryPolicy(accessToken); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/pass-through/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/PassThroughPanel.tsx similarity index 89% rename from ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/pass-through/page.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/PassThroughPanel.tsx index 4ba7b8b260b..7ae7e976651 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/pass-through/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/PassThroughPanel.tsx @@ -3,7 +3,7 @@ import PassThroughSettings from "@/components/PassThroughSettings/PassThroughSettings"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -export default function PassThroughPage() { +export default function PassThroughPanel() { const { accessToken, userRole, userId: userID, premiumUser } = useAuthorized(); return ( diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/price-data/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/PriceDataPanel.tsx similarity index 79% rename from ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/price-data/page.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/PriceDataPanel.tsx index b8f385be13f..506f22957fc 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/price-data/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/panels/PriceDataPanel.tsx @@ -2,6 +2,6 @@ import PriceDataManagementTab from "@/app/(dashboard)/models-and-endpoints/components/PriceDataManagementTab"; -export default function PriceDataPage() { +export default function PriceDataPanel() { return ; } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/tabRoutes.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/tabRoutes.test.ts deleted file mode 100644 index 920bd3dc156..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/tabRoutes.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* @vitest-environment jsdom */ -import { describe, expect, it, vi } from "vitest"; - -vi.mock("@/components/networking", () => ({ serverRootPath: "" })); - -import { MODEL_TAB_SLUGS, modelTabHref, slugFromPathname } from "./tabRoutes"; - -describe("slugFromPathname", () => { - it("returns empty string for the base path with or without a trailing slash", () => { - expect(slugFromPathname("/models-and-endpoints")).toBe(""); - expect(slugFromPathname("/models-and-endpoints/")).toBe(""); - }); - - it("extracts the tab slug from dev and proxy-mounted (/ui) paths", () => { - expect(slugFromPathname("/models-and-endpoints/add")).toBe("add"); - expect(slugFromPathname("/ui/models-and-endpoints/llm-credentials/")).toBe("llm-credentials"); - }); - - it("returns the raw segment for an unknown tab so the view can redirect to base", () => { - expect(slugFromPathname("/ui/models-and-endpoints/bogus")).toBe("bogus"); - }); - - it("returns empty string when the models base segment is not in the path", () => { - expect(slugFromPathname("/teams")).toBe(""); - }); -}); - -describe("modelTabHref", () => { - it("builds the trailing-slash base href for the empty slug", () => { - expect(modelTabHref("")).toBe("/ui/models-and-endpoints/"); - }); - - it("builds a trailing-slash href for every tab slug (required by static export)", () => { - for (const slug of MODEL_TAB_SLUGS) { - expect(modelTabHref(slug)).toBe(`/ui/models-and-endpoints/${slug}/`); - } - }); -}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/tabRoutes.ts b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/tabRoutes.ts deleted file mode 100644 index e56a664df45..00000000000 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/tabRoutes.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { createTabRoutes } from "@/utils/tabRoutes"; - -export const modelsRoutes = createTabRoutes("models-and-endpoints", [ - "add", - "llm-credentials", - "pass-through", - "health", - "retry-settings", - "model-group-alias", - "price-data", -] as const); - -export type ModelTabSlug = (typeof modelsRoutes.slugs)[number]; - -export const MODELS_BASE_SEGMENT = modelsRoutes.baseSegment; -export const MODEL_TAB_SLUGS = modelsRoutes.slugs; -export const modelTabHref = modelsRoutes.tabHref; -export const slugFromPathname = modelsRoutes.slugFromPathname;