From f947ef14a2a7db3e2d81914977358636463bea71 Mon Sep 17 00:00:00 2001 From: yucheng-berri Date: Sat, 11 Jul 2026 17:50:13 -0700 Subject: [PATCH 1/4] fix(xecguard): use StandardLoggingGuardrailInformation in logging hook (#32911) XecGuard's async_logging_hook wrote a bare dict to standard_logging_object["guardrail_information"] while the typed contract is Optional[List[StandardLoggingGuardrailInformation]]. Readers that iterated the field walked dict keys, raised on info.get, or silently dropped the entry from guardrail usage tracking and spend-log writes Construct the typed entry and append it to the existing list or create a new one, matching the shared helper pattern. Record the configured guardrail name instead of a hardcoded "xecguard" and pass the GuardrailEventHooks enum for guardrail_mode --- .../guardrail_hooks/xecguard/xecguard.py | 31 ++++++++++------- .../guardrail_hooks/test_xecguard.py | 33 +++++++++++++++++-- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/litellm/proxy/guardrails/guardrail_hooks/xecguard/xecguard.py b/litellm/proxy/guardrails/guardrail_hooks/xecguard/xecguard.py index 1b663c16d5b..f4a6f0aeb3b 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/xecguard/xecguard.py +++ b/litellm/proxy/guardrails/guardrail_hooks/xecguard/xecguard.py @@ -49,7 +49,11 @@ from litellm.llms.custom_httpx.http_handler import ( httpxSpecialProvider, ) from litellm.types.guardrails import GuardrailEventHooks -from litellm.types.utils import GenericGuardrailAPIInputs, GuardrailStatus +from litellm.types.utils import ( + GenericGuardrailAPIInputs, + GuardrailStatus, + StandardLoggingGuardrailInformation, +) if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import ( @@ -246,16 +250,21 @@ class XecGuardGuardrail(CustomGuardrail): "guardrail_intervened" if scan_result.get("decision") == "UNSAFE" else "success" ) end_time = datetime.now() - kwargs["standard_logging_object"]["guardrail_information"] = { - "duration": (end_time - start_time).total_seconds(), - "end_time": end_time.timestamp(), - "guardrail_mode": "logging_only", - "guardrail_name": "xecguard", - "guardrail_response": scan_result, - "guardrail_status": guardrail_status, - "masked_entity_count": None, - "start_time": start_time.timestamp(), - } + slg = StandardLoggingGuardrailInformation( + guardrail_name=self.guardrail_name or "xecguard", + guardrail_mode=GuardrailEventHooks.logging_only, + guardrail_response=scan_result, + guardrail_status=guardrail_status, + start_time=start_time.timestamp(), + end_time=end_time.timestamp(), + duration=(end_time - start_time).total_seconds(), + masked_entity_count=None, + ) + existing = kwargs["standard_logging_object"].get("guardrail_information") + if isinstance(existing, list): + existing.append(slg) + else: + kwargs["standard_logging_object"]["guardrail_information"] = [slg] except Exception as exc: verbose_proxy_logger.debug( diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_xecguard.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_xecguard.py index 60c595c64e5..f64967abbb0 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_xecguard.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_xecguard.py @@ -1644,12 +1644,37 @@ class TestXecGuardLoggingHook: ) assert out_kwargs is kwargs assert out_result is result - info = kwargs["standard_logging_object"]["guardrail_information"] + info_list = kwargs["standard_logging_object"]["guardrail_information"] + assert isinstance(info_list, list), "guardrail_information must be a list" + assert len(info_list) == 1 + info = info_list[0] assert info["guardrail_mode"] == "logging_only" - assert info["guardrail_name"] == "xecguard" + assert info["guardrail_name"] == "test-xecguard" assert info["guardrail_status"] == "success" assert info["guardrail_response"]["trace_id"] == "lg-1" + @pytest.mark.asyncio + async def test_async_logging_hook_appends_to_existing_guardrail_info( + self, xecguard_guardrail, mock_request_data + ): + resp = _make_response({"decision": "SAFE", "trace_id": "lg-4"}) + prior_entry = {"guardrail_name": "other-guardrail"} + with patch.object(xecguard_guardrail.async_handler, "post", return_value=resp): + kwargs = { + **mock_request_data, + "standard_logging_object": {"guardrail_information": [prior_entry]}, + } + await xecguard_guardrail.async_logging_hook( + kwargs=kwargs, + result=_build_model_response("some answer"), + call_type="acompletion", + ) + info_list = kwargs["standard_logging_object"]["guardrail_information"] + assert len(info_list) == 2 + assert info_list[0] is prior_entry + assert info_list[1]["guardrail_name"] == "test-xecguard" + assert info_list[1]["guardrail_response"]["trace_id"] == "lg-4" + @pytest.mark.asyncio async def test_async_logging_hook_without_response_records_info( self, xecguard_guardrail, mock_request_data @@ -1680,7 +1705,9 @@ class TestXecGuardLoggingHook: result=_build_model_response("x"), call_type="acompletion", ) - info = kwargs["standard_logging_object"]["guardrail_information"] + info_list = kwargs["standard_logging_object"]["guardrail_information"] + assert isinstance(info_list, list), "guardrail_information must be a list" + info = info_list[0] assert info["guardrail_status"] == "guardrail_intervened" @pytest.mark.asyncio From 8c5473f198b71213d9b703c68e6ab2115036acd8 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Sat, 11 Jul 2026 18:01:33 -0700 Subject: [PATCH 2/4] feat(ui): adopt openapi-react-query ($api) and convert useCustomers (#32949) * feat(ui): adopt openapi-react-query and convert useCustomers to $api Add openapi-react-query and expose $api = createQueryClient(fetchClient) alongside fetchClient. Rewrite useCustomers as $api.useQuery("get", "/customer/list", {}, { enabled, select }), which derives the query key from method + path (dropping the hand-written createQueryKeys entry and the manual key) and forwards the request signal for cancellation. The response type still flows from schema.d.ts as CustomerResponse[]. Tests assert the path, the admin/token enabled gate, and the empty-body select fallback. * test(ui): read the last render's options in useCustomers helper The lastCallOptions helper was named for the last call but read mock.calls[0]. Harmless while each test renders once, but it would silently assert against first-render options if a test ever re-renders. Read the final call instead. --- ui/litellm-dashboard/package-lock.json | 14 +++ ui/litellm-dashboard/package.json | 1 + .../hooks/customers/useCustomers.test.ts | 108 ++++++------------ .../hooks/customers/useCustomers.ts | 20 ++-- ui/litellm-dashboard/src/lib/http/api.ts | 9 ++ 5 files changed, 69 insertions(+), 83 deletions(-) diff --git a/ui/litellm-dashboard/package-lock.json b/ui/litellm-dashboard/package-lock.json index 648503abb27..b8f6265441b 100644 --- a/ui/litellm-dashboard/package-lock.json +++ b/ui/litellm-dashboard/package-lock.json @@ -29,6 +29,7 @@ "next": "16.2.6", "openai": "4.104.0", "openapi-fetch": "^0.17.0", + "openapi-react-query": "^0.5.4", "papaparse": "5.5.3", "react": "18.3.1", "react-copy-to-clipboard": "5.1.1", @@ -10544,6 +10545,19 @@ "openapi-typescript-helpers": "^0.1.0" } }, + "node_modules/openapi-react-query": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/openapi-react-query/-/openapi-react-query-0.5.4.tgz", + "integrity": "sha512-V9lRiozjHot19/BYSgXYoyznDxDJQhEBSdi26+SJ0UqjMANLQhkni4XG+Z7e3Ag7X46ZLMrL9VxYkghU3QvbWg==", + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.1.0" + }, + "peerDependencies": { + "@tanstack/react-query": "^5.80.0", + "openapi-fetch": "^0.17.0" + } + }, "node_modules/openapi-typescript": { "version": "7.13.0", "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", diff --git a/ui/litellm-dashboard/package.json b/ui/litellm-dashboard/package.json index 40c495cebf1..a14349c7239 100644 --- a/ui/litellm-dashboard/package.json +++ b/ui/litellm-dashboard/package.json @@ -44,6 +44,7 @@ "next": "16.2.6", "openai": "4.104.0", "openapi-fetch": "^0.17.0", + "openapi-react-query": "^0.5.4", "papaparse": "5.5.3", "react": "18.3.1", "react-copy-to-clipboard": "5.1.1", diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.test.ts index 1e614b709e2..b09ab4498f7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.test.ts @@ -1,12 +1,10 @@ -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { renderHook, waitFor } from "@testing-library/react"; -import React, { ReactNode } from "react"; +import { renderHook } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { useCustomers, type EndUser } from "./useCustomers"; -const mockGet = vi.fn(); +const useQueryMock = vi.fn(); vi.mock("@/lib/http/api", () => ({ - fetchClient: { GET: (...args: unknown[]) => mockGet(...args) }, + $api: { useQuery: (...args: unknown[]) => useQueryMock(...args) }, })); const mockUseAuthorized = vi.fn(); @@ -14,91 +12,55 @@ vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ default: () => mockUseAuthorized(), })); -const mockCustomers: EndUser[] = [ - { user_id: "customer-1", alias: "Test Customer 1", spend: 150.5, blocked: false }, - { user_id: "customer-2", alias: null, spend: 0, blocked: true }, -]; +const authorized = { accessToken: "test-access-token", userRole: "Admin" }; -const authorized = { - accessToken: "test-access-token", - userRole: "Admin", - userId: "test-user-id", - token: "test-token", - userEmail: "test@example.com", - premiumUser: false, - disabledPersonalKeyCreation: null, - showSSOBanner: false, +type QueryOptions = { enabled: boolean; select: (data: EndUser[] | undefined) => EndUser[] }; + +const lastCallOptions = (): QueryOptions => { + const calls = useQueryMock.mock.calls; + return calls[calls.length - 1][3] as QueryOptions; }; describe("useCustomers", () => { - let queryClient: QueryClient; - beforeEach(() => { - queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); vi.clearAllMocks(); + useQueryMock.mockReturnValue({ data: [] }); mockUseAuthorized.mockReturnValue(authorized); }); - const wrapper = ({ children }: { children: ReactNode }) => - React.createElement(QueryClientProvider, { client: queryClient }, children); - - it("fetches /customer/list and returns the typed list on success", async () => { - mockGet.mockResolvedValue({ data: mockCustomers }); - - const { result } = renderHook(() => useCustomers(), { wrapper }); - - expect(result.current.isLoading).toBe(true); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(result.current.data).toEqual(mockCustomers); - expect(mockGet).toHaveBeenCalledWith("/customer/list"); - expect(mockGet).toHaveBeenCalledTimes(1); + it("queries GET /customer/list with a derived key (no hand-written queryKey)", () => { + renderHook(() => useCustomers()); + expect(useQueryMock).toHaveBeenCalledWith("get", "/customer/list", {}, expect.any(Object)); }); - it("surfaces an error when the request rejects", async () => { - const testError = new Error("Failed to fetch customers"); - mockGet.mockRejectedValue(testError); - - const { result } = renderHook(() => useCustomers(), { wrapper }); - - await waitFor(() => { - expect(result.current.isError).toBe(true); - }); - - expect(result.current.error).toEqual(testError); - expect(result.current.data).toBeUndefined(); + it("enables the query only for an admin holding an access token", () => { + renderHook(() => useCustomers()); + expect(lastCallOptions().enabled).toBe(true); }); - it("falls back to an empty list when the response has no body", async () => { - mockGet.mockResolvedValue({ data: undefined }); - - const { result } = renderHook(() => useCustomers(), { wrapper }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(result.current.data).toEqual([]); + it("disables the query when the access token is missing", () => { + mockUseAuthorized.mockReturnValue({ ...authorized, accessToken: null }); + renderHook(() => useCustomers()); + expect(lastCallOptions().enabled).toBe(false); }); - it("does not fetch when the access token is missing", () => { - mockUseAuthorized.mockReturnValue({ ...authorized, accessToken: null, token: null }); - - const { result } = renderHook(() => useCustomers(), { wrapper }); - - expect(result.current.isFetched).toBe(false); - expect(mockGet).not.toHaveBeenCalled(); - }); - - it("does not fetch when the user is not an admin", () => { + it("disables the query for a non-admin role", () => { mockUseAuthorized.mockReturnValue({ ...authorized, userRole: "member" }); + renderHook(() => useCustomers()); + expect(lastCallOptions().enabled).toBe(false); + }); - const { result } = renderHook(() => useCustomers(), { wrapper }); + it("selects an empty list when the response body is missing", () => { + renderHook(() => useCustomers()); + expect(lastCallOptions().select(undefined)).toEqual([]); + }); - expect(result.current.isFetched).toBe(false); - expect(mockGet).not.toHaveBeenCalled(); + it("selects the customer list through unchanged", () => { + const customers: EndUser[] = [ + { user_id: "customer-1", alias: "Test Customer 1", spend: 150.5, blocked: false }, + { user_id: "customer-2", alias: null, spend: 0, blocked: true }, + ]; + renderHook(() => useCustomers()); + expect(lastCallOptions().select(customers)).toEqual(customers); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.ts index 25e2e3f5e90..ebea4618b7f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/customers/useCustomers.ts @@ -1,19 +1,19 @@ -import { useQuery } from "@tanstack/react-query"; -import { createQueryKeys } from "../common/queryKeysFactory"; -import { fetchClient } from "@/lib/http/api"; +import { $api } from "@/lib/http/api"; import { all_admin_roles } from "@/utils/roles"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import type { components } from "@/lib/http/schema"; export type EndUser = components["schemas"]["CustomerResponse"]; -const customersKeys = createQueryKeys("customers"); - export const useCustomers = () => { const { accessToken, userRole } = useAuthorized(); - return useQuery({ - queryKey: customersKeys.list({}), - queryFn: async () => (await fetchClient.GET("/customer/list")).data ?? [], - enabled: Boolean(accessToken) && all_admin_roles.includes(userRole!), - }); + return $api.useQuery( + "get", + "/customer/list", + {}, + { + enabled: Boolean(accessToken) && all_admin_roles.includes(userRole!), + select: (data) => data ?? [], + }, + ); }; diff --git a/ui/litellm-dashboard/src/lib/http/api.ts b/ui/litellm-dashboard/src/lib/http/api.ts index 2e8e1d36c5b..aa6c2d6c0fd 100644 --- a/ui/litellm-dashboard/src/lib/http/api.ts +++ b/ui/litellm-dashboard/src/lib/http/api.ts @@ -1,4 +1,5 @@ import createFetchClient, { type Middleware } from "openapi-fetch"; +import createQueryClient from "openapi-react-query"; import type { paths } from "./schema"; import { ApiError, deriveErrorMessage } from "./client"; import { getAuthHeaderName, getAuthToken, getRequestBaseUrl, reportError } from "./runtime"; @@ -46,3 +47,11 @@ const middleware: Middleware = { */ export const fetchClient = createFetchClient({ baseUrl: globalThis.location?.origin ?? "" }); fetchClient.use(middleware); + +/** + * TanStack Query bound to the typed client. Callers write + * `$api.useQuery("get", "/path", init, options)`; the query key is derived from + * method + path + init (no hand-maintained key), the request signal is + * forwarded for cancellation, and the response type comes from schema.d.ts. + */ +export const $api = createQueryClient(fetchClient); From b6dbda48f9e88ed0c3e1918488e0545946e9d627 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Sat, 11 Jul 2026 18:01:41 -0700 Subject: [PATCH 3/4] refactor(ui): colocate the mcp-servers view, keeping the shared mcp_tools surface (#32968) * refactor(ui): colocate the usage view, keeping the shared usage components Split for the usage (UsagePage) segment. Most of the folder is the usage page's own view, but four pieces are reused elsewhere and stay in @/components/UsagePage: TopKeyView (old-usage), KeyModelUsageView and value_formatters (activity_metrics), and the shared types (activity_metrics, chartUtils). The other 21 files move into usage/_components, preserving the folder structure. The external consumers import only the retained files, so they are untouched. The moved files' imports of the retained files become @/components/UsagePage paths, other escaping relative imports are absolutized, and lint suppressions are re-keyed for moved files only. No behavior change. * refactor(ui): colocate the mcp-servers view, keeping the shared mcp_tools surface --- ui/litellm-dashboard/eslint-suppressions.json | 40 +++++++++---------- .../_components}/DcrBridgeToggle.tsx | 2 +- .../_components}/EnvVarsSection.tsx | 0 .../_components}/MCPLogoSelector.test.tsx | 0 .../_components}/MCPLogoSelector.tsx | 0 .../_components}/MCPNetworkSettings.tsx | 4 +- .../MCPPermissionManagement.test.tsx | 0 .../_components}/MCPPermissionManagement.tsx | 2 +- .../_components}/MCPServerCard.test.tsx | 2 +- .../_components}/MCPServerCard.tsx | 2 +- .../MCPStandardsSettings.test.tsx | 2 +- .../_components}/MCPStandardsSettings.tsx | 2 +- .../_components}/MCPSubmissionsTab.tsx | 2 +- .../_components}/MCPToolsetsTab.tsx | 12 ++++-- .../_components}/OAuthFormFields.test.tsx | 0 .../_components}/OAuthFormFields.tsx | 2 +- .../_components}/OpenAPIFormSection.tsx | 2 +- .../_components}/OpenAPIQuickPicker.tsx | 2 +- .../PassthroughAuthorizeSection.test.tsx | 0 .../PassthroughAuthorizeSection.tsx | 2 +- .../_components}/StdioConfiguration.tsx | 0 .../TokenEndpointAuthMethodField.tsx | 0 .../_components}/TokenExchangeFormFields.tsx | 0 .../_components}/ToolTestPanel.test.tsx | 4 +- .../_components}/ToolTestPanel.tsx | 4 +- .../_components}/TruePassthroughWarning.tsx | 2 +- .../_components}/UserEnvVarsModal.tsx | 6 +-- .../_components}/create_mcp_server.test.tsx | 4 +- .../_components}/create_mcp_server.tsx | 6 +-- .../mcp-servers/_components}/index.tsx | 0 .../mcp-servers/_components}/mcp_connect.tsx | 4 +- .../mcp_connection_status.test.tsx | 0 .../_components}/mcp_connection_status.tsx | 0 .../_components}/mcp_discovery.tsx | 4 +- .../_components}/mcp_server_cost_config.tsx | 2 +- .../_components}/mcp_server_cost_display.tsx | 2 +- .../_components}/mcp_server_edit.test.tsx | 8 ++-- .../_components}/mcp_server_edit.tsx | 11 +++-- .../_components}/mcp_server_view.tsx | 2 +- .../_components}/mcp_servers.test.tsx | 6 +-- .../mcp-servers/_components}/mcp_servers.tsx | 24 ++++++----- .../mcp_tool_configuration.test.tsx | 0 .../_components}/mcp_tool_configuration.tsx | 2 +- .../_components}/mcp_tools.test.tsx | 4 +- .../mcp-servers/_components}/mcp_tools.tsx | 4 +- .../mcp-servers/_components}/testUtils.ts | 0 .../mcp-servers/_components}/utils.test.tsx | 0 .../mcp-servers/_components}/utils.tsx | 2 +- .../src/app/(dashboard)/mcp-servers/page.tsx | 2 +- .../tests/CreateKeyPage.expiredToken.test.tsx | 2 +- 50 files changed, 100 insertions(+), 83 deletions(-) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/DcrBridgeToggle.tsx (95%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/EnvVarsSection.tsx (100%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/MCPLogoSelector.test.tsx (100%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/MCPLogoSelector.tsx (100%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/MCPNetworkSettings.tsx (97%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/MCPPermissionManagement.test.tsx (100%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/MCPPermissionManagement.tsx (99%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/MCPServerCard.test.tsx (96%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/MCPServerCard.tsx (99%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/MCPStandardsSettings.test.tsx (97%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/MCPStandardsSettings.tsx (96%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/MCPSubmissionsTab.tsx (99%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/MCPToolsetsTab.tsx (98%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/OAuthFormFields.test.tsx (100%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/OAuthFormFields.tsx (99%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/OpenAPIFormSection.tsx (98%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/OpenAPIQuickPicker.tsx (98%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/PassthroughAuthorizeSection.test.tsx (100%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/PassthroughAuthorizeSection.tsx (99%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/StdioConfiguration.tsx (100%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/TokenEndpointAuthMethodField.tsx (100%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/TokenExchangeFormFields.tsx (100%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/ToolTestPanel.test.tsx (97%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/ToolTestPanel.tsx (99%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/TruePassthroughWarning.tsx (94%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/UserEnvVarsModal.tsx (95%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/create_mcp_server.test.tsx (99%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/create_mcp_server.tsx (99%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/index.tsx (100%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/mcp_connect.tsx (99%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/mcp_connection_status.test.tsx (100%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/mcp_connection_status.tsx (100%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/mcp_discovery.tsx (98%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/mcp_server_cost_config.tsx (98%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/mcp_server_cost_display.tsx (97%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/mcp_server_edit.test.tsx (99%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/mcp_server_edit.tsx (99%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/mcp_server_view.tsx (99%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/mcp_servers.test.tsx (98%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/mcp_servers.tsx (97%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/mcp_tool_configuration.test.tsx (100%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/mcp_tool_configuration.tsx (99%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/mcp_tools.test.tsx (98%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/mcp_tools.tsx (99%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/testUtils.ts (100%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/utils.test.tsx (100%) rename ui/litellm-dashboard/src/{components/mcp_tools => app/(dashboard)/mcp-servers/_components}/utils.tsx (97%) diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 48b400536bd..eb8bdee0ea2 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -1878,17 +1878,17 @@ "count": 1 } }, - "src/components/mcp_tools/MCPLogoSelector.test.tsx": { + "src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.test.tsx": { "unused-imports/no-unused-imports": { "count": 1 } }, - "src/components/mcp_tools/MCPNetworkSettings.tsx": { + "src/app/(dashboard)/mcp-servers/_components/MCPNetworkSettings.tsx": { "react-hooks/immutability": { "count": 2 } }, - "src/components/mcp_tools/MCPSubmissionsTab.tsx": { + "src/app/(dashboard)/mcp-servers/_components/MCPSubmissionsTab.tsx": { "react-hooks/set-state-in-effect": { "count": 1 } @@ -1898,7 +1898,7 @@ "count": 5 } }, - "src/components/mcp_tools/MCPToolsetsTab.tsx": { + "src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.tsx": { "no-nested-ternary": { "count": 1 }, @@ -1920,7 +1920,7 @@ "count": 1 } }, - "src/components/mcp_tools/OAuthFormFields.tsx": { + "src/app/(dashboard)/mcp-servers/_components/OAuthFormFields.tsx": { "no-nested-ternary": { "count": 1 }, @@ -1928,12 +1928,12 @@ "count": 1 } }, - "src/components/mcp_tools/OpenAPIQuickPicker.tsx": { + "src/app/(dashboard)/mcp-servers/_components/OpenAPIQuickPicker.tsx": { "react-hooks/set-state-in-effect": { "count": 1 } }, - "src/components/mcp_tools/ToolTestPanel.tsx": { + "src/app/(dashboard)/mcp-servers/_components/ToolTestPanel.tsx": { "no-nested-ternary": { "count": 3 }, @@ -1944,12 +1944,12 @@ "count": 1 } }, - "src/components/mcp_tools/UserEnvVarsModal.tsx": { + "src/app/(dashboard)/mcp-servers/_components/UserEnvVarsModal.tsx": { "no-nested-ternary": { "count": 2 } }, - "src/components/mcp_tools/create_mcp_server.tsx": { + "src/app/(dashboard)/mcp-servers/_components/create_mcp_server.tsx": { "no-nested-ternary": { "count": 1 }, @@ -1960,7 +1960,7 @@ "count": 4 } }, - "src/components/mcp_tools/mcp_connect.tsx": { + "src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx": { "no-restricted-imports": { "count": 1 }, @@ -1968,7 +1968,7 @@ "count": 4 } }, - "src/components/mcp_tools/mcp_connection_status.tsx": { + "src/app/(dashboard)/mcp-servers/_components/mcp_connection_status.tsx": { "no-nested-ternary": { "count": 3 }, @@ -1976,22 +1976,22 @@ "count": 1 } }, - "src/components/mcp_tools/mcp_discovery.tsx": { + "src/app/(dashboard)/mcp-servers/_components/mcp_discovery.tsx": { "react-hooks/set-state-in-effect": { "count": 2 } }, - "src/components/mcp_tools/mcp_server_cost_config.tsx": { + "src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_config.tsx": { "no-restricted-imports": { "count": 1 } }, - "src/components/mcp_tools/mcp_server_cost_display.tsx": { + "src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_display.tsx": { "no-restricted-imports": { "count": 1 } }, - "src/components/mcp_tools/mcp_server_edit.tsx": { + "src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx": { "no-nested-ternary": { "count": 1 }, @@ -2005,12 +2005,12 @@ "count": 5 } }, - "src/components/mcp_tools/mcp_server_view.tsx": { + "src/app/(dashboard)/mcp-servers/_components/mcp_server_view.tsx": { "no-restricted-imports": { "count": 1 } }, - "src/components/mcp_tools/mcp_servers.tsx": { + "src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx": { "no-nested-ternary": { "count": 1 }, @@ -2021,12 +2021,12 @@ "count": 2 } }, - "src/components/mcp_tools/mcp_tool_configuration.tsx": { + "src/app/(dashboard)/mcp-servers/_components/mcp_tool_configuration.tsx": { "no-restricted-imports": { "count": 1 } }, - "src/components/mcp_tools/mcp_tools.tsx": { + "src/app/(dashboard)/mcp-servers/_components/mcp_tools.tsx": { "no-nested-ternary": { "count": 1 }, @@ -2558,4 +2558,4 @@ "count": 1 } } -} \ No newline at end of file +} diff --git a/ui/litellm-dashboard/src/components/mcp_tools/DcrBridgeToggle.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/DcrBridgeToggle.tsx similarity index 95% rename from ui/litellm-dashboard/src/components/mcp_tools/DcrBridgeToggle.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/DcrBridgeToggle.tsx index f1b642293c0..49c182aa6be 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/DcrBridgeToggle.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/DcrBridgeToggle.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Form, Switch, Tooltip } from "antd"; import { InfoCircleOutlined } from "@ant-design/icons"; -import { isClientForwardedTokenMode } from "./types"; +import { isClientForwardedTokenMode } from "@/components/mcp_tools/types"; /** * DCR-bridge toggle for the client-forwarded token modes (true_passthrough / diff --git a/ui/litellm-dashboard/src/components/mcp_tools/EnvVarsSection.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/EnvVarsSection.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/mcp_tools/EnvVarsSection.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/EnvVarsSection.tsx diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPLogoSelector.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/mcp_tools/MCPLogoSelector.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.test.tsx diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPLogoSelector.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/mcp_tools/MCPLogoSelector.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPLogoSelector.tsx diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPNetworkSettings.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPNetworkSettings.tsx similarity index 97% rename from ui/litellm-dashboard/src/components/mcp_tools/MCPNetworkSettings.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPNetworkSettings.tsx index 00323465731..7ab240389f3 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPNetworkSettings.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPNetworkSettings.tsx @@ -1,13 +1,13 @@ import React, { useState, useEffect } from "react"; import { Select, Button, Card, Typography, Spin, Tag } from "antd"; import { SaveOutlined, PlusOutlined } from "@ant-design/icons"; -import { DeprecationBanner } from "../DeprecationBanner"; +import { DeprecationBanner } from "@/components/DeprecationBanner"; import { getGeneralSettingsCall, updateConfigFieldSetting, deleteConfigFieldSetting, fetchMCPClientIp, -} from "../networking"; +} from "@/components/networking"; const { Text } = Typography; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPPermissionManagement.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPPermissionManagement.test.tsx diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPPermissionManagement.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPPermissionManagement.tsx index 27cbdf2ea34..aae13d4b467 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPPermissionManagement.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPPermissionManagement.tsx @@ -1,7 +1,7 @@ import React, { useEffect } from "react"; import { Alert, Form, Select, Tooltip, Collapse, Input, Space, Button, Switch } from "antd"; import { InfoCircleOutlined, MinusCircleOutlined, PlusOutlined } from "@ant-design/icons"; -import { MCPServer, AUTH_TYPE } from "./types"; +import { MCPServer, AUTH_TYPE } from "@/components/mcp_tools/types"; const { Panel } = Collapse; interface MCPPermissionManagementProps { diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPServerCard.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.test.tsx similarity index 96% rename from ui/litellm-dashboard/src/components/mcp_tools/MCPServerCard.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.test.tsx index 8463a23cc16..a0998b587fb 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPServerCard.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.test.tsx @@ -2,7 +2,7 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import { describe, it, expect, vi } from "vitest"; import MCPServerCard from "./MCPServerCard"; -import type { MCPServer } from "./types"; +import type { MCPServer } from "@/components/mcp_tools/types"; const baseServer: MCPServer = { server_id: "srv-1", diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPServerCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/mcp_tools/MCPServerCard.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx index c16ad87980e..4282cdba278 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPServerCard.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx @@ -8,7 +8,7 @@ import { MoreOutlined, ThunderboltOutlined, } from "@ant-design/icons"; -import { AUTH_TYPE, type MCPServer } from "./types"; +import { AUTH_TYPE, type MCPServer } from "@/components/mcp_tools/types"; import { getMaskedAndFullUrl } from "./utils"; const { Text } = Typography; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPStandardsSettings.test.tsx similarity index 97% rename from ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPStandardsSettings.test.tsx index fd8d9f92c48..08f65a72537 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPStandardsSettings.test.tsx @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; import { FIELD_GROUPS, MCP_REQUIRED_FIELD_DEFS, SETTINGS_KEY } from "./MCPStandardsSettings"; -import { MCPServer } from "./types"; +import { MCPServer } from "@/components/mcp_tools/types"; const makeServer = (overrides: Partial = {}): MCPServer => ({ server_id: "s1", diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPStandardsSettings.tsx similarity index 96% rename from ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPStandardsSettings.tsx index fb38e392631..13a9b7c171e 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPStandardsSettings.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPStandardsSettings.tsx @@ -1,6 +1,6 @@ "use client"; -import { MCPServer } from "./types"; +import { MCPServer } from "@/components/mcp_tools/types"; export interface RequiredFieldDef { key: string; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPSubmissionsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPSubmissionsTab.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/mcp_tools/MCPSubmissionsTab.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPSubmissionsTab.tsx index 50c88a63582..de030420bc3 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPSubmissionsTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPSubmissionsTab.tsx @@ -18,7 +18,7 @@ import { getGeneralSettingsCall, updateConfigFieldSetting, } from "@/components/networking"; -import { MCPServer, MCPSubmissionsSummary } from "./types"; +import { MCPServer, MCPSubmissionsSummary } from "@/components/mcp_tools/types"; import { FIELD_GROUPS, MCP_REQUIRED_FIELD_DEFS, SETTINGS_KEY } from "./MCPStandardsSettings"; import NotificationsManager from "@/components/molecules/notifications_manager"; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.tsx similarity index 98% rename from ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.tsx index 5e7e99ee8ec..fa44694887b 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/MCPToolsetsTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPToolsetsTab.tsx @@ -7,9 +7,15 @@ import { useMCPToolsets } from "@/app/(dashboard)/hooks/mcpServers/useMCPToolset import { useMCPServers } from "@/app/(dashboard)/hooks/mcpServers/useMCPServers"; import { useQueryClient } from "@tanstack/react-query"; import { DateCell, IdCell } from "@/components/shared/table_cells"; -import { DataTable } from "../view_logs/table"; -import { createMCPToolset, updateMCPToolset, deleteMCPToolset, listMCPTools, getProxyBaseUrl } from "../networking"; -import { MCPToolset, MCPToolsetTool } from "./types"; +import { DataTable } from "@/components/view_logs/table"; +import { + createMCPToolset, + updateMCPToolset, + deleteMCPToolset, + listMCPTools, + getProxyBaseUrl, +} from "@/components/networking"; +import { MCPToolset, MCPToolsetTool } from "@/components/mcp_tools/types"; const { Text: AntdText } = Typography; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OAuthFormFields.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OAuthFormFields.test.tsx diff --git a/ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OAuthFormFields.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OAuthFormFields.tsx index 93afefe4358..f359bdee065 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/OAuthFormFields.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OAuthFormFields.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Form, Input, InputNumber, Select, Tooltip } from "antd"; import { InfoCircleOutlined } from "@ant-design/icons"; import { Button, TextInput } from "@tremor/react"; -import { OAUTH_FLOW } from "./types"; +import { OAUTH_FLOW } from "@/components/mcp_tools/types"; import TokenEndpointAuthMethodField from "./TokenEndpointAuthMethodField"; interface OAuthFlowStatus { diff --git a/ui/litellm-dashboard/src/components/mcp_tools/OpenAPIFormSection.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OpenAPIFormSection.tsx similarity index 98% rename from ui/litellm-dashboard/src/components/mcp_tools/OpenAPIFormSection.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OpenAPIFormSection.tsx index d606b8ba1fc..073780b359f 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/OpenAPIFormSection.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OpenAPIFormSection.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { Form, Input, Tooltip } from "antd"; import { InfoCircleOutlined } from "@ant-design/icons"; import { FormInstance } from "antd/es/form"; -import { AUTH_TYPE, OAUTH_FLOW } from "./types"; +import { AUTH_TYPE, OAUTH_FLOW } from "@/components/mcp_tools/types"; import OpenAPIQuickPicker, { OpenAPIRegistryEntry, OpenAPIKeyTool } from "./OpenAPIQuickPicker"; interface OpenAPIFormSectionProps { diff --git a/ui/litellm-dashboard/src/components/mcp_tools/OpenAPIQuickPicker.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OpenAPIQuickPicker.tsx similarity index 98% rename from ui/litellm-dashboard/src/components/mcp_tools/OpenAPIQuickPicker.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OpenAPIQuickPicker.tsx index a8208a09286..0aec81fdf4b 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/OpenAPIQuickPicker.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/OpenAPIQuickPicker.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import { Spin } from "antd"; -import { fetchOpenAPIRegistry } from "../networking"; +import { fetchOpenAPIRegistry } from "@/components/networking"; export interface OpenAPIKeyTool { name: string; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/PassthroughAuthorizeSection.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/PassthroughAuthorizeSection.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/mcp_tools/PassthroughAuthorizeSection.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/PassthroughAuthorizeSection.test.tsx diff --git a/ui/litellm-dashboard/src/components/mcp_tools/PassthroughAuthorizeSection.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/PassthroughAuthorizeSection.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/mcp_tools/PassthroughAuthorizeSection.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/PassthroughAuthorizeSection.tsx index 0ed4ee555d1..dc10f0f1392 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/PassthroughAuthorizeSection.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/PassthroughAuthorizeSection.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Button, Checkbox, Form, Input } from "antd"; import DcrBridgeToggle from "./DcrBridgeToggle"; -import { credentialAuthClass, isClientForwardedTokenMode } from "./types"; +import { credentialAuthClass, isClientForwardedTokenMode } from "@/components/mcp_tools/types"; interface PassthroughOAuthFlow { startOAuthFlow: () => void | Promise; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/StdioConfiguration.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/StdioConfiguration.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/mcp_tools/StdioConfiguration.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/StdioConfiguration.tsx diff --git a/ui/litellm-dashboard/src/components/mcp_tools/TokenEndpointAuthMethodField.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/TokenEndpointAuthMethodField.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/mcp_tools/TokenEndpointAuthMethodField.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/TokenEndpointAuthMethodField.tsx diff --git a/ui/litellm-dashboard/src/components/mcp_tools/TokenExchangeFormFields.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/TokenExchangeFormFields.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/mcp_tools/TokenExchangeFormFields.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/TokenExchangeFormFields.tsx diff --git a/ui/litellm-dashboard/src/components/mcp_tools/ToolTestPanel.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/ToolTestPanel.test.tsx similarity index 97% rename from ui/litellm-dashboard/src/components/mcp_tools/ToolTestPanel.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/ToolTestPanel.test.tsx index 4bd351216b8..0613e84feed 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/ToolTestPanel.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/ToolTestPanel.test.tsx @@ -3,9 +3,9 @@ import { render, screen } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import { ToolTestPanel } from "./ToolTestPanel"; -import { InputSchema, MCPTool } from "./types"; +import { InputSchema, MCPTool } from "@/components/mcp_tools/types"; -vi.mock("../molecules/notifications_manager", () => ({ +vi.mock("@/components/molecules/notifications_manager", () => ({ default: { success: vi.fn(), fromBackend: vi.fn(), diff --git a/ui/litellm-dashboard/src/components/mcp_tools/ToolTestPanel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/ToolTestPanel.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/mcp_tools/ToolTestPanel.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/ToolTestPanel.tsx index c15150eee58..8f042445c01 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/ToolTestPanel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/ToolTestPanel.tsx @@ -1,10 +1,10 @@ import React from "react"; import { Button, TextInput } from "@tremor/react"; -import { MCPTool, InputSchema, InputSchemaProperty } from "./types"; +import { MCPTool, InputSchema, InputSchemaProperty } from "@/components/mcp_tools/types"; import { resolveLogoSrc } from "@/lib/assetPaths"; import { Form, Select, Tooltip } from "antd"; import { InfoCircleOutlined } from "@ant-design/icons"; -import NotificationsManager from "../molecules/notifications_manager"; +import NotificationsManager from "@/components/molecules/notifications_manager"; const isPlainObject = (value: unknown): value is Record => typeof value === "object" && value !== null && !Array.isArray(value); diff --git a/ui/litellm-dashboard/src/components/mcp_tools/TruePassthroughWarning.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/TruePassthroughWarning.tsx similarity index 94% rename from ui/litellm-dashboard/src/components/mcp_tools/TruePassthroughWarning.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/TruePassthroughWarning.tsx index b52d3f4c672..9c57cbd7d14 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/TruePassthroughWarning.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/TruePassthroughWarning.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Alert } from "antd"; -import { AUTH_TYPE } from "./types"; +import { AUTH_TYPE } from "@/components/mcp_tools/types"; /** * Warning shown in the create/edit MCP server forms when auth_type diff --git a/ui/litellm-dashboard/src/components/mcp_tools/UserEnvVarsModal.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/UserEnvVarsModal.tsx similarity index 95% rename from ui/litellm-dashboard/src/components/mcp_tools/UserEnvVarsModal.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/UserEnvVarsModal.tsx index 08a285cd56b..f76aef365f7 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/UserEnvVarsModal.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/UserEnvVarsModal.tsx @@ -1,9 +1,9 @@ import React from "react"; import { Modal, Form, Input, Button, Alert, Spin, Tag, Typography } from "antd"; import { useMutation, useQuery } from "@tanstack/react-query"; -import { MCPServer, MCPUserEnvVarsStatus } from "./types"; -import { getMCPUserEnvVars, storeMCPUserEnvVars } from "../networking"; -import NotificationsManager from "../molecules/notifications_manager"; +import { MCPServer, MCPUserEnvVarsStatus } from "@/components/mcp_tools/types"; +import { getMCPUserEnvVars, storeMCPUserEnvVars } from "@/components/networking"; +import NotificationsManager from "@/components/molecules/notifications_manager"; const { Text, Title } = Typography; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/create_mcp_server.test.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/create_mcp_server.test.tsx index 32fe439a316..6ce7f5c75ed 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/create_mcp_server.test.tsx @@ -1,12 +1,12 @@ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import * as networking from "../networking"; +import * as networking from "@/components/networking"; import { setToken } from "@/utils/mcpTokenStore"; import CreateMCPServer from "./create_mcp_server"; import { selectAntOption } from "./testUtils"; -vi.mock("../networking", () => ({ +vi.mock("@/components/networking", () => ({ createMCPServer: vi.fn(), fetchOpenAPIRegistry: vi.fn().mockResolvedValue({ apis: [] }), registerMCPServer: vi.fn(), diff --git a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/create_mcp_server.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/create_mcp_server.tsx index b9a6f5d229d..70838c592c8 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/create_mcp_server.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/create_mcp_server.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { Modal, Tooltip, Form, Select, Input, InputNumber, Switch, Collapse } from "antd"; import { InfoCircleOutlined } from "@ant-design/icons"; import { Button, TextInput } from "@tremor/react"; -import { createMCPServer, registerMCPServer, storeMCPOAuthUserCredential } from "../networking"; +import { createMCPServer, registerMCPServer, storeMCPOAuthUserCredential } from "@/components/networking"; import { setToken } from "@/utils/mcpTokenStore"; import { AUTH_TYPE, @@ -20,7 +20,7 @@ import { isHeldOAuthTokenStale, preservedDeclaredAppCredentials, withoutMintedTokenCredentials, -} from "./types"; +} from "@/components/mcp_tools/types"; import OAuthFormFields from "./OAuthFormFields"; import TruePassthroughWarning from "./TruePassthroughWarning"; import PassthroughAuthorizeSection from "./PassthroughAuthorizeSection"; @@ -35,7 +35,7 @@ import MCPLogoSelector from "./MCPLogoSelector"; import EnvVarsSection from "./EnvVarsSection"; import { isAdminRole } from "@/utils/roles"; import { validateMCPServerUrl, validateMCPServerName, normalizeEnvVars, TOOL_DISPLAY_NAME_PATTERN } from "./utils"; -import NotificationsManager from "../molecules/notifications_manager"; +import NotificationsManager from "@/components/molecules/notifications_manager"; import { useMcpOAuthFlow } from "@/hooks/useMcpOAuthFlow"; import { useTestMCPConnection } from "@/hooks/useTestMCPConnection"; import { getSecureItem, setSecureItem } from "@/utils/secureStorage"; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/index.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/index.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/mcp_tools/index.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/index.tsx diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_connect.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/mcp_tools/mcp_connect.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx index 42c557f142f..7bdfd9c6b8f 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_connect.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connect.tsx @@ -4,8 +4,8 @@ import React, { useState } from "react"; import { Card, Typography, Space, Alert, Button, Switch, Form, Collapse } from "antd"; import { TabPanel, TabPanels, TabGroup, TabList, Tab, Title as TremorTitle, Text as TremorText } from "@tremor/react"; import { CopyIcon, Code, Terminal, Globe, CheckIcon, ExternalLinkIcon, KeyIcon, ServerIcon, Zap } from "lucide-react"; -import { getProxyBaseUrl } from "../networking"; -import { copyToClipboard as utilCopyToClipboard } from "../../utils/dataUtils"; +import { getProxyBaseUrl } from "@/components/networking"; +import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils"; const { Title, Text } = Typography; const { Panel } = Collapse; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connection_status.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connection_status.test.tsx diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connection_status.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/mcp_tools/mcp_connection_status.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_connection_status.tsx diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_discovery.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_discovery.tsx similarity index 98% rename from ui/litellm-dashboard/src/components/mcp_tools/mcp_discovery.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_discovery.tsx index 189b52dd6b2..6fcff011ba6 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_discovery.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_discovery.tsx @@ -1,7 +1,7 @@ import React, { useState, useMemo, useEffect } from "react"; import { Modal, Input, Typography } from "antd"; -import { fetchDiscoverableMCPServers } from "../networking"; -import { DiscoverableMCPServer, DiscoverMCPServersResponse } from "./types"; +import { fetchDiscoverableMCPServers } from "@/components/networking"; +import { DiscoverableMCPServer, DiscoverMCPServersResponse } from "@/components/mcp_tools/types"; import { mcpLogoImg } from "./create_mcp_server"; import { resolveLogoSrc } from "@/lib/assetPaths"; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_cost_config.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_config.tsx similarity index 98% rename from ui/litellm-dashboard/src/components/mcp_tools/mcp_server_cost_config.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_config.tsx index 3f3986d5a2e..89c41693a4c 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_cost_config.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_config.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Tooltip, InputNumber, Collapse, Badge } from "antd"; import { InfoCircleOutlined, DollarOutlined, ToolOutlined } from "@ant-design/icons"; import { Card, Title, Text } from "@tremor/react"; -import { MCPServerCostInfo } from "./types"; +import { MCPServerCostInfo } from "@/components/mcp_tools/types"; interface MCPServerCostConfigProps { value?: MCPServerCostInfo; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_cost_display.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_display.tsx similarity index 97% rename from ui/litellm-dashboard/src/components/mcp_tools/mcp_server_cost_display.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_display.tsx index e41a06b879e..f26f7ba2320 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_cost_display.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_cost_display.tsx @@ -1,6 +1,6 @@ import React from "react"; import { Text } from "@tremor/react"; -import { MCPServerCostInfo } from "./types"; +import { MCPServerCostInfo } from "@/components/mcp_tools/types"; interface MCPServerCostDisplayProps { costConfig?: MCPServerCostInfo | null; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.test.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.test.tsx index 5b8d0aac8c0..278bf6f7e13 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.test.tsx @@ -4,18 +4,18 @@ import { render, screen, waitFor, fireEvent, act } from "@testing-library/react" import userEvent from "@testing-library/user-event"; import MCPServerEdit, { EDIT_OAUTH_UI_STATE_KEY } from "./mcp_server_edit"; import { setSecureItem } from "@/utils/secureStorage"; -import * as networking from "../networking"; -import NotificationsManager from "../molecules/notifications_manager"; +import * as networking from "@/components/networking"; +import NotificationsManager from "@/components/molecules/notifications_manager"; import { selectAntOption } from "./testUtils"; -vi.mock("../networking", () => ({ +vi.mock("@/components/networking", () => ({ updateMCPServer: vi.fn(), listMCPTools: vi.fn().mockResolvedValue({ tools: [], error: null }), storeMCPOAuthUserCredential: vi.fn().mockResolvedValue({}), testMCPToolsListRequest: vi.fn().mockResolvedValue({ tools: [], error: null }), })); -vi.mock("../molecules/notifications_manager", () => ({ +vi.mock("@/components/molecules/notifications_manager", () => ({ default: { success: vi.fn(), fromBackend: vi.fn(), diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx index 6709eb02c65..3b184cc6f1e 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_edit.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx @@ -18,8 +18,13 @@ import { TRANSPORT, getMcpOAuthMode, oauth2FlowToFormValue, -} from "./types"; -import { updateMCPServer, listMCPTools, storeMCPOAuthUserCredential, testMCPToolsListRequest } from "../networking"; +} from "@/components/mcp_tools/types"; +import { + updateMCPServer, + listMCPTools, + storeMCPOAuthUserCredential, + testMCPToolsListRequest, +} from "@/components/networking"; import { getToken, isTokenValid, removeToken, setToken } from "@/utils/mcpTokenStore"; import { buildMcpPassthroughAuthHeader } from "@/utils/mcpHeaderUtils"; import MCPServerCostConfig from "./mcp_server_cost_config"; @@ -39,7 +44,7 @@ import { normalizeToolOverrideMap, TOOL_DISPLAY_NAME_PATTERN, } from "./utils"; -import NotificationsManager from "../molecules/notifications_manager"; +import NotificationsManager from "@/components/molecules/notifications_manager"; import { useMcpOAuthFlow } from "@/hooks/useMcpOAuthFlow"; import { getSecureItem, setSecureItem } from "@/utils/secureStorage"; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_view.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_view.tsx index 7ed00f74225..620f76a739a 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_server_view.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_view.tsx @@ -2,7 +2,7 @@ import React, { useState } from "react"; import { ArrowLeftIcon, EyeIcon, EyeOffIcon } from "@heroicons/react/outline"; import { Title, Card, Button, Text, Grid, TabGroup, TabList, TabPanel, TabPanels, Tab, Icon } from "@tremor/react"; -import { MCPServer, handleTransport, handleAuth } from "./types"; +import { MCPServer, handleTransport, handleAuth } from "@/components/mcp_tools/types"; // TODO: Move Tools viewer from index file import { MCPToolsViewer } from "."; import MCPServerEdit, { EDIT_OAUTH_UI_STATE_KEY } from "./mcp_server_edit"; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.test.tsx similarity index 98% rename from ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.test.tsx index 446fdd8c22d..d61bc23c757 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.test.tsx @@ -3,10 +3,10 @@ import { render, waitFor, screen, fireEvent, act } from "@testing-library/react" import { describe, it, expect, vi, beforeEach } from "vitest"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import MCPServers from "./mcp_servers"; -import * as networking from "../networking"; +import * as networking from "@/components/networking"; // Mock the networking module -vi.mock("../networking", () => ({ +vi.mock("@/components/networking", () => ({ fetchMCPServers: vi.fn(), fetchMCPServerHealth: vi.fn(), deleteMCPServer: vi.fn(), @@ -19,7 +19,7 @@ vi.mock("../networking", () => ({ })); // Mock NotificationsManager -vi.mock("../molecules/notifications_manager", () => ({ +vi.mock("@/components/molecules/notifications_manager", () => ({ default: { success: vi.fn(), fromBackend: vi.fn(), diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx similarity index 97% rename from ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx index f0d3e60b32e..0afb4bd9314 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_servers.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx @@ -1,29 +1,35 @@ import { isAdminRole } from "@/utils/roles"; import { QuestionCircleOutlined, SearchOutlined } from "@ant-design/icons"; import { Button, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title } from "@tremor/react"; -import NewBadge from "../common_components/NewBadge"; +import NewBadge from "@/components/common_components/NewBadge"; import { Descriptions, Empty, Input, Modal, Select, Spin, Tooltip, Typography } from "antd"; import React, { useEffect, useState, useMemo, useCallback } from "react"; import { useQuery } from "@tanstack/react-query"; -import { useMCPServers } from "../../app/(dashboard)/hooks/mcpServers/useMCPServers"; -import { useMCPServerHealth } from "../../app/(dashboard)/hooks/mcpServers/useMCPServerHealth"; -import NotificationsManager from "../molecules/notifications_manager"; -import { deleteMCPServer } from "../networking"; +import { useMCPServers } from "@/app/(dashboard)/hooks/mcpServers/useMCPServers"; +import { useMCPServerHealth } from "@/app/(dashboard)/hooks/mcpServers/useMCPServerHealth"; +import NotificationsManager from "@/components/molecules/notifications_manager"; +import { deleteMCPServer } from "@/components/networking"; import { MCPSubmissionsTab } from "./MCPSubmissionsTab"; import { MCPToolsetsTab } from "./MCPToolsetsTab"; import CreateMCPServer from "./create_mcp_server"; import MCPConnect from "./mcp_connect"; import MCPServerCard from "./MCPServerCard"; import { MCPServerView } from "./mcp_server_view"; -import type { DiscoverableMCPServer, MCPServer, MCPServerProps, MCPUserEnvVarsStatus, Team } from "./types"; -import MCPSemanticFilterSettings from "../Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings"; +import type { + DiscoverableMCPServer, + MCPServer, + MCPServerProps, + MCPUserEnvVarsStatus, + Team, +} from "@/components/mcp_tools/types"; +import MCPSemanticFilterSettings from "@/components/Settings/AdminSettings/MCPSemanticFilterSettings/MCPSemanticFilterSettings"; import MCPNetworkSettings from "./MCPNetworkSettings"; import MCPDiscovery from "./mcp_discovery"; -import { ByokCredentialModal } from "./ByokCredentialModal"; +import { ByokCredentialModal } from "@/components/mcp_tools/ByokCredentialModal"; import { getSecureItem } from "@/utils/secureStorage"; import { TOOLS_OAUTH_UI_STATE_KEY } from "@/hooks/mcpOAuthUtils"; import UserEnvVarsModal from "./UserEnvVarsModal"; -import { listMCPUserEnvVarStatus } from "../networking"; +import { listMCPUserEnvVarStatus } from "@/components/networking"; type SortKey = "created_desc" | "updated_desc" | "name_asc" | "health"; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tool_configuration.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tool_configuration.test.tsx diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tool_configuration.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tool_configuration.tsx index 1ebc07eac86..60c4c264c3c 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tool_configuration.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tool_configuration.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { Card, Title, Text } from "@tremor/react"; import { ToolOutlined, CheckCircleOutlined, SearchOutlined, EditOutlined } from "@ant-design/icons"; import { Badge, Spin, Checkbox, Input, Radio } from "antd"; -import McpCrudPermissionPanel from "./McpCrudPermissionPanel"; +import McpCrudPermissionPanel from "@/components/mcp_tools/McpCrudPermissionPanel"; import { TOOL_DISPLAY_NAME_PATTERN } from "./utils"; interface KeyTool { diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tools.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tools.test.tsx similarity index 98% rename from ui/litellm-dashboard/src/components/mcp_tools/mcp_tools.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tools.test.tsx index 2e8fa901f6d..8b0e6d62f66 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tools.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tools.test.tsx @@ -2,10 +2,10 @@ import { render, screen, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { describe, expect, it, vi, beforeEach } from "vitest"; import MCPToolsViewer from "./mcp_tools"; -import { listMCPTools, getMCPOAuthUserCredentialStatus } from "../networking"; +import { listMCPTools, getMCPOAuthUserCredentialStatus } from "@/components/networking"; import { isTokenValid, getToken } from "@/utils/mcpTokenStore"; -vi.mock("../networking", () => ({ +vi.mock("@/components/networking", () => ({ listMCPTools: vi.fn(), callMCPTool: vi.fn(), getMCPOAuthUserCredentialStatus: vi.fn(), diff --git a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tools.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tools.tsx similarity index 99% rename from ui/litellm-dashboard/src/components/mcp_tools/mcp_tools.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tools.tsx index 928a2e3c6bb..428c10da284 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/mcp_tools.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_tools.tsx @@ -9,8 +9,8 @@ import { MCPContent, CallMCPToolResponse, getMcpOAuthMode, -} from "./types"; -import { listMCPTools, callMCPTool, getMCPOAuthUserCredentialStatus } from "../networking"; +} from "@/components/mcp_tools/types"; +import { listMCPTools, callMCPTool, getMCPOAuthUserCredentialStatus } from "@/components/networking"; import { isTokenValid, getToken, removeToken } from "@/utils/mcpTokenStore"; import { sanitizeMcpAliasForHeader, buildMcpPassthroughAuthHeader } from "@/utils/mcpHeaderUtils"; import { useToolsOAuthFlow } from "@/hooks/useToolsOAuthFlow"; diff --git a/ui/litellm-dashboard/src/components/mcp_tools/testUtils.ts b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/testUtils.ts similarity index 100% rename from ui/litellm-dashboard/src/components/mcp_tools/testUtils.ts rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/testUtils.ts diff --git a/ui/litellm-dashboard/src/components/mcp_tools/utils.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/utils.test.tsx similarity index 100% rename from ui/litellm-dashboard/src/components/mcp_tools/utils.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/utils.test.tsx diff --git a/ui/litellm-dashboard/src/components/mcp_tools/utils.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/utils.tsx similarity index 97% rename from ui/litellm-dashboard/src/components/mcp_tools/utils.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/utils.tsx index 7d6e24fc480..4738e1e8fba 100644 --- a/ui/litellm-dashboard/src/components/mcp_tools/utils.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/utils.tsx @@ -1,4 +1,4 @@ -import { MCPEnvVar, MCPEnvVarScope } from "./types"; +import { MCPEnvVar, MCPEnvVarScope } from "@/components/mcp_tools/types"; export const extractMCPToken = (url: string): { token: string | null; baseUrl: string } => { try { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/page.tsx index dfc7ca15896..462c48360cd 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { MCPServers } from "@/components/mcp_tools"; +import { MCPServers } from "./_components"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; export default function McpServers() { diff --git a/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx b/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx index e871bbe8272..62346eff057 100644 --- a/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx +++ b/ui/litellm-dashboard/tests/CreateKeyPage.expiredToken.test.tsx @@ -130,7 +130,7 @@ vi.mock("@/components/cache_dashboard", () => ({ default: stub("cache-dashboard" vi.mock("@/app/(dashboard)/guardrails/_components", () => ({ default: stub("guardrails") })); vi.mock("@/components/prompts", () => ({ default: stub("prompts") })); vi.mock("@/components/transform_request", () => ({ default: stub("transform-request") })); -vi.mock("@/components/mcp_tools", () => ({ MCPServers: stub("mcp-servers") })); +vi.mock("@/app/(dashboard)/mcp-servers/_components", () => ({ MCPServers: stub("mcp-servers") })); vi.mock("@/app/(dashboard)/tag-management/_components", () => ({ default: stub("tag-management") })); vi.mock("@/app/(dashboard)/vector-stores/_components", () => ({ default: stub("vector-stores") })); vi.mock("@/components/ui_theme_settings", () => ({ default: stub("ui-theme-settings") })); From 5fc1a3c671bf9f25e1410efd4ebb9c2617a9219a Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Sat, 11 Jul 2026 18:16:18 -0700 Subject: [PATCH 4/4] refactor(ui): convert endpoint usage charts to shadcn/recharts (#32723) * refactor(ui): convert endpoint usage charts to shadcn/recharts Adds a LineChart wrapper to the shared charts kit, mirroring the BarChart/AreaChart composition with connectNulls and curveType props, and converts EndpointUsageBarChart and EndpointUsageLineChart from tremor to the shared wrappers. Both endpoint chart tests now assert on real recharts SVG output instead of tremor mocks. * refactor(ui): drop unused endpointData prop from EndpointUsageLineChart * fix(ui): point endpoint chart test type imports at the UsagePage types alias after colocation move --- ui/litellm-dashboard/eslint-suppressions.json | 103 +++++++-------- .../EndpointUsage/EndpointUsage.tsx | 2 +- .../components/EndpointUsageBarChart.test.tsx | 89 ++++++++----- .../components/EndpointUsageBarChart.tsx | 45 +++---- .../EndpointUsageLineChart.test.tsx | 119 ++++++++++++++---- .../components/EndpointUsageLineChart.tsx | 53 +++++--- .../src/components/shared/charts/index.ts | 1 + .../shared/charts/line_chart.test.tsx | 117 +++++++++++++++++ .../components/shared/charts/line_chart.tsx | 99 +++++++++++++++ 9 files changed, 473 insertions(+), 155 deletions(-) create mode 100644 ui/litellm-dashboard/src/components/shared/charts/line_chart.test.tsx create mode 100644 ui/litellm-dashboard/src/components/shared/charts/line_chart.tsx diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index eb8bdee0ea2..3f6f4da25d0 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -858,9 +858,6 @@ "src/app/(dashboard)/projects/_components/ProjectDetailsPage.tsx": { "no-nested-ternary": { "count": 3 - }, - "no-restricted-imports": { - "count": 1 } }, "src/app/(dashboard)/projects/_components/ProjectKeysSection.tsx": { @@ -1079,6 +1076,51 @@ "count": 1 } }, + "src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageTable.tsx": { + "no-nested-ternary": { + "count": 1 + } + }, + "src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/usage/_components/components/EntityUsage/SpendByProvider.tsx": { + "no-restricted-imports": { + "count": 1 + } + }, + "src/app/(dashboard)/usage/_components/components/UsageAIChatPanel.tsx": { + "no-nested-ternary": { + "count": 1 + }, + "react-hooks/immutability": { + "count": 1 + } + }, + "src/app/(dashboard)/usage/_components/components/UsagePageView.tsx": { + "no-nested-ternary": { + "count": 1 + }, + "no-restricted-imports": { + "count": 1 + }, + "react-hooks/purity": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 3 + } + }, + "src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts": { + "react-hooks/refs": { + "count": 1 + }, + "react-hooks/set-state-in-effect": { + "count": 1 + } + }, "src/app/(dashboard)/users/_components/DefaultUserSettings.tsx": { "no-restricted-imports": { "count": 1 @@ -1502,61 +1544,6 @@ "count": 1 } }, - "src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageTable.tsx": { - "no-nested-ternary": { - "count": 1 - } - }, - "src/app/(dashboard)/usage/_components/components/EntityUsage/EntityUsage.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/usage/_components/components/EntityUsage/SpendByProvider.tsx": { - "no-restricted-imports": { - "count": 1 - } - }, - "src/app/(dashboard)/usage/_components/components/UsageAIChatPanel.tsx": { - "no-nested-ternary": { - "count": 1 - }, - "react-hooks/immutability": { - "count": 1 - } - }, - "src/app/(dashboard)/usage/_components/components/UsagePageView.tsx": { - "no-nested-ternary": { - "count": 1 - }, - "no-restricted-imports": { - "count": 1 - }, - "react-hooks/purity": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 3 - } - }, - "src/app/(dashboard)/usage/_components/hooks/usePaginatedDailyActivity.ts": { - "react-hooks/refs": { - "count": 1 - }, - "react-hooks/set-state-in-effect": { - "count": 1 - } - }, "src/components/VirtualKeysPage/VirtualKeysTable.tsx": { "no-nested-ternary": { "count": 2 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/EndpointUsage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/EndpointUsage.tsx index 64fdb13a0b9..51e451ca770 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/EndpointUsage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/EndpointUsage.tsx @@ -59,7 +59,7 @@ const EndpointUsage: React.FC = ({ userSpendData }) => {
- +
); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.test.tsx index f200b8a6a18..a9e65b21f4b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.test.tsx @@ -1,39 +1,70 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; +import { screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { renderWithProviders } from "@/../tests/test-utils"; +import { MetricWithMetadata } from "@/components/UsagePage/types"; import EndpointUsageBarChart from "./EndpointUsageBarChart"; -vi.mock("@tremor/react", async () => { - const React = await import("react"); - - function Card({ children }: any) { - return React.createElement("div", { "data-testid": "tremor-card" }, children); - } - (Card as any).displayName = "Card"; - - function Title({ children }: any) { - return React.createElement("h2", { "data-testid": "tremor-title" }, children); - } - (Title as any).displayName = "Title"; - - function BarChart(_props: any) { - return React.createElement("div", { "data-testid": "tremor-bar-chart" }, "Bar Chart"); - } - (BarChart as any).displayName = "BarChart"; - - return { Card, Title, BarChart }; +const metric = (successful: number, failed: number): MetricWithMetadata => ({ + metrics: { + spend: 0, + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + api_requests: successful + failed, + successful_requests: successful, + failed_requests: failed, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, + }, + metadata: {}, + api_key_breakdown: {}, }); -vi.mock("@/components/common_components/chartUtils", () => ({ - CustomLegend: ({ categories }: any) =>
{categories.join(", ")}
, - CustomTooltip: () =>
Tooltip
, -})); +const endpointData = { + "/chat/completions": metric(120, 5), + "/embeddings": metric(40, 2), +}; describe("EndpointUsageBarChart", () => { - it("should render", () => { - render(); + it("renders the title and the header legend labels", () => { + renderWithProviders(); - expect(screen.getByTestId("tremor-card")).toBeInTheDocument(); expect(screen.getByText("Success vs Failed Requests by Endpoint")).toBeInTheDocument(); - expect(screen.getByTestId("tremor-bar-chart")).toBeInTheDocument(); + expect(screen.getByText("Successful Requests")).toBeInTheDocument(); + expect(screen.getByText("Failed Requests")).toBeInTheDocument(); + }); + + it("renders stacked green and red bars per endpoint", () => { + const { container } = renderWithProviders(); + + expect(container.querySelectorAll(".recharts-bar")).toHaveLength(2); + const rectangles = Array.from(container.querySelectorAll("path.recharts-rectangle")); + expect(rectangles).toHaveLength(4); + const fills = new Set(rectangles.map((rect) => rect.getAttribute("fill"))); + expect(fills).toEqual(new Set(["var(--color-green-500, #22c55e)", "var(--color-red-500, #ef4444)"])); + + const xPositions = rectangles.map((rect) => rect.getAttribute("d")?.split(",")[0]); + expect(new Set(xPositions).size).toBe(2); + }); + + it("labels the x axis with endpoint names", () => { + renderWithProviders(); + + expect(screen.getAllByText("/chat/completions").length).toBeGreaterThan(0); + expect(screen.getAllByText("/embeddings").length).toBeGreaterThan(0); + }); + + it("keeps the chart's own legend off; only the header legend is shown", () => { + const { container } = renderWithProviders(); + + expect(container.querySelector(".recharts-legend-wrapper")).toBeNull(); + expect(screen.queryByText("metrics.successful_requests")).not.toBeInTheDocument(); + }); + + it("renders an empty chart without bars when endpointData is absent", () => { + const { container } = renderWithProviders(); + + expect(screen.getByText("Success vs Failed Requests by Endpoint")).toBeInTheDocument(); + expect(container.querySelectorAll("path.recharts-rectangle")).toHaveLength(0); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.tsx index 2badbe30868..bf9868d77cf 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageBarChart.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { BarChart, Card, Title } from "@tremor/react"; -import { CustomLegend, CustomTooltip } from "@/components/common_components/chartUtils"; +import { BarChart, CustomLegend, CustomTooltip } from "@/components/shared/charts"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { MetricWithMetadata } from "@/components/UsagePage/types"; interface EndpointUsageBarChartProps { @@ -8,11 +8,9 @@ interface EndpointUsageBarChartProps { } const EndpointUsageBarChart: React.FC = ({ endpointData }) => { - const dataToUse = endpointData || {}; - // Transform endpoint data into chart format const chartData = React.useMemo(() => { - return Object.entries(dataToUse).map(([endpoint, data]) => ({ + return Object.entries(endpointData || {}).map(([endpoint, data]) => ({ endpoint, "metrics.successful_requests": data.metrics.successful_requests, "metrics.failed_requests": data.metrics.failed_requests, @@ -21,31 +19,34 @@ const EndpointUsageBarChart: React.FC = ({ endpointD failed_requests: data.metrics.failed_requests, }, })); - }, [dataToUse]); + }, [endpointData]); const valueFormatter = (value: number) => value.toLocaleString(); return ( -
- Success vs Failed Requests by Endpoint - +
+ Success vs Failed Requests by Endpoint + +
+ + + -
- +
); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.test.tsx index ec99825289c..33914e627dc 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.test.tsx @@ -1,34 +1,103 @@ -import { render, screen } from "@testing-library/react"; -import { describe, expect, it, vi } from "vitest"; +import { screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { renderWithProviders } from "@/../tests/test-utils"; +import { DailyData, MetricWithMetadata, SpendMetrics } from "@/components/UsagePage/types"; import EndpointUsageLineChart from "./EndpointUsageLineChart"; -vi.mock("@tremor/react", async () => { - const React = await import("react"); - - function Card({ children }: any) { - return React.createElement("div", { "data-testid": "tremor-card" }, children); - } - (Card as any).displayName = "Card"; - - function Title({ children }: any) { - return React.createElement("h2", { "data-testid": "tremor-title" }, children); - } - (Title as any).displayName = "Title"; - - function LineChart(_props: any) { - return React.createElement("div", { "data-testid": "tremor-line-chart" }, "Line Chart"); - } - (LineChart as any).displayName = "LineChart"; - - return { Card, Title, LineChart }; +const spendMetrics = (apiRequests: number): SpendMetrics => ({ + spend: 0, + prompt_tokens: 0, + completion_tokens: 0, + total_tokens: 0, + api_requests: apiRequests, + successful_requests: apiRequests, + failed_requests: 0, + cache_read_input_tokens: 0, + cache_creation_input_tokens: 0, }); +const endpointMetric = (apiRequests: number): MetricWithMetadata => ({ + metrics: spendMetrics(apiRequests), + metadata: {}, + api_key_breakdown: {}, +}); + +const day = (date: string, endpoints: Record): DailyData => ({ + date, + metrics: spendMetrics(0), + breakdown: { + models: {}, + model_groups: {}, + mcp_servers: {}, + providers: {}, + api_keys: {}, + entities: {}, + endpoints: Object.fromEntries( + Object.entries(endpoints).map(([name, requests]) => [name, endpointMetric(requests)]), + ), + }, +}); + +const dailyData = { + results: [ + day("2026-06-03T12:00:00", { "/chat/completions": 4000, "/embeddings": 900 }), + day("2026-06-02T12:00:00", { "/chat/completions": 2500, "/embeddings": 700 }), + day("2026-06-01T12:00:00", { "/chat/completions": 1200 }), + ], +}; + describe("EndpointUsageLineChart", () => { - it("should render", () => { - render(); + it("renders the title", () => { + renderWithProviders(); - expect(screen.getByTestId("tremor-card")).toBeInTheDocument(); expect(screen.getByText("Endpoint Usage Trends")).toBeInTheDocument(); - expect(screen.getByTestId("tremor-line-chart")).toBeInTheDocument(); + }); + + it("renders one line per endpoint with the tremor palette strokes", () => { + const { container } = renderWithProviders(); + + const curves = Array.from(container.querySelectorAll("path.recharts-line-curve")); + expect(curves).toHaveLength(2); + expect(new Set(curves.map((curve) => curve.getAttribute("stroke")))).toEqual( + new Set(["var(--color-blue-500, #3b82f6)", "var(--color-cyan-500, #06b6d4)"]), + ); + }); + + it("shows a legend with the endpoint names", () => { + const { container } = renderWithProviders(); + + const legend = container.querySelector(".recharts-legend-wrapper"); + expect(legend).not.toBeNull(); + expect(legend!.textContent).toContain("/chat/completions"); + expect(legend!.textContent).toContain("/embeddings"); + }); + + it("orders formatted dates oldest to newest on the x axis", () => { + const { container } = renderWithProviders(); + + const tickLabels = Array.from(container.querySelectorAll(".recharts-xAxis-tick-labels text")).map( + (tick) => tick.textContent, + ); + expect(tickLabels).toEqual(["Jun 1", "Jun 2", "Jun 3"]); + }); + + it("formats y axis ticks with toLocaleString", () => { + renderWithProviders(); + + expect(screen.getAllByText(/^\d,\d{3}$/).length).toBeGreaterThan(0); + }); + + it("draws smooth natural curves", () => { + const { container } = renderWithProviders(); + + const path = container.querySelector("path.recharts-line-curve")?.getAttribute("d") ?? ""; + expect(path).toContain("C"); + }); + + it("renders an empty chart without lines when dailyData is absent", () => { + const { container } = renderWithProviders(); + + expect(screen.getByText("Endpoint Usage Trends")).toBeInTheDocument(); + expect(container.querySelectorAll("path.recharts-line-curve")).toHaveLength(0); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.tsx b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.tsx index 9f838d6156a..483a30b1639 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/usage/_components/components/EndpointUsage/components/EndpointUsageLineChart.tsx @@ -1,10 +1,10 @@ -import { Card, LineChart, Title } from "@tremor/react"; import { useMemo } from "react"; +import { LineChart, type ChartColor } from "@/components/shared/charts"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { DailyData } from "@/components/UsagePage/types"; interface EndpointUsageLineChartProps { dailyData?: { results: DailyData[] }; - endpointData?: Record; } // Transform daily data into chart format @@ -42,7 +42,7 @@ function transformDailyDataToChart(dailyData: DailyData[]): Array { if (!dailyData?.results || dailyData.results.length === 0) { return []; @@ -59,26 +59,39 @@ export function EndpointUsageLineChart({ dailyData, endpointData }: EndpointUsag }, [chartData]); // Tremor color palette for multiple lines - const colors = ["blue", "cyan", "indigo", "violet", "purple", "fuchsia", "pink", "rose", "red", "orange"]; + const colors: readonly ChartColor[] = [ + "blue", + "cyan", + "indigo", + "violet", + "purple", + "fuchsia", + "pink", + "rose", + "red", + "orange", + ]; return ( -
- Endpoint Usage Trends -
- value.toLocaleString()} - showLegend={true} - showGridLines={true} - yAxisWidth={60} - connectNulls={true} - curveType="natural" - /> + + Endpoint Usage Trends + + + value.toLocaleString()} + showLegend={true} + showGridLines={true} + yAxisWidth={60} + connectNulls={true} + curveType="natural" + /> +
); } diff --git a/ui/litellm-dashboard/src/components/shared/charts/index.ts b/ui/litellm-dashboard/src/components/shared/charts/index.ts index ba0a7544ddb..8383c767064 100644 --- a/ui/litellm-dashboard/src/components/shared/charts/index.ts +++ b/ui/litellm-dashboard/src/components/shared/charts/index.ts @@ -10,3 +10,4 @@ export { } from "./chart_tooltip"; export { CHART_COLOR_HEX, DEFAULT_COLOR_CYCLE, categoryFills, chartColorValue, type ChartColor } from "./colors"; export { DonutChart, type DonutChartProps } from "./donut_chart"; +export { LineChart, type LineChartCurveType, type LineChartProps } from "./line_chart"; diff --git a/ui/litellm-dashboard/src/components/shared/charts/line_chart.test.tsx b/ui/litellm-dashboard/src/components/shared/charts/line_chart.test.tsx new file mode 100644 index 00000000000..9385dc49494 --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/charts/line_chart.test.tsx @@ -0,0 +1,117 @@ +import { render, screen } from "@testing-library/react"; +import React from "react"; +import { describe, expect, it } from "vitest"; +import { LineChart } from "./line_chart"; + +const data = [ + { date: "Jun 1", "/chat/completions": 10, "/embeddings": 4 }, + { date: "Jun 2", "/chat/completions": 15, "/embeddings": 6 }, + { date: "Jun 3", "/chat/completions": 12, "/embeddings": 9 }, +]; + +describe("LineChart", () => { + it("renders one line per category with the mapped tremor stroke colors", () => { + const { container } = render( + , + ); + + const curves = Array.from(container.querySelectorAll("path.recharts-line-curve")); + expect(curves).toHaveLength(2); + expect(curves.map((curve) => curve.getAttribute("stroke"))).toEqual([ + "var(--color-blue-500, #3b82f6)", + "var(--color-cyan-500, #06b6d4)", + ]); + }); + + it("falls back to the tremor default color cycle when no colors are passed", () => { + const { container } = render( + , + ); + + const strokes = Array.from(container.querySelectorAll("path.recharts-line-curve")).map((curve) => + curve.getAttribute("stroke"), + ); + expect(strokes).toEqual(["var(--color-blue-500, #3b82f6)", "var(--color-cyan-500, #06b6d4)"]); + }); + + it("applies valueFormatter to the value axis ticks", () => { + render( + `${v} req`} + />, + ); + + expect(screen.getAllByText(/ req$/).length).toBeGreaterThan(0); + }); + + it("renders a legend by default, matching tremor, and hides it when showLegend is false", () => { + const { container, rerender } = render( + , + ); + expect(screen.getByText("/chat/completions")).toBeInTheDocument(); + expect(container.querySelector(".recharts-legend-wrapper")).not.toBeNull(); + + rerender( + , + ); + expect(screen.queryByText("/chat/completions")).not.toBeInTheDocument(); + }); + + it("draws straight segments by default and curved segments for curveType natural", () => { + const { container: linear } = render( + , + ); + const { container: natural } = render( + , + ); + + const linearPath = linear.querySelector("path.recharts-line-curve")?.getAttribute("d") ?? ""; + const naturalPath = natural.querySelector("path.recharts-line-curve")?.getAttribute("d") ?? ""; + expect(linearPath).not.toContain("C"); + expect(naturalPath).toContain("C"); + }); + + it("bridges gaps over null values only when connectNulls is set", () => { + const gappedData = [ + { date: "Jun 1", "/chat/completions": 10 }, + { date: "Jun 2", "/chat/completions": null }, + { date: "Jun 3", "/chat/completions": 12 }, + { date: "Jun 4", "/chat/completions": 15 }, + ]; + + const { container: broken } = render( + , + ); + const { container: bridged } = render( + , + ); + + const brokenPath = broken.querySelector("path.recharts-line-curve")?.getAttribute("d") ?? ""; + const bridgedPath = bridged.querySelector("path.recharts-line-curve")?.getAttribute("d") ?? ""; + expect((brokenPath.match(/M/g) ?? []).length).toBeGreaterThan(1); + expect((bridgedPath.match(/M/g) ?? []).length).toBe(1); + }); + + it("renders an empty chart without lines when there are no categories", () => { + const { container } = render(); + + expect(container.querySelector("[data-slot='chart']")).not.toBeNull(); + expect(container.querySelectorAll("path.recharts-line-curve")).toHaveLength(0); + }); + + it("emits no per-chart style tag; colors flow through strokes, not CSS vars", () => { + const { container } = render( + , + ); + expect(container.querySelector("style")).toBeNull(); + }); +}); diff --git a/ui/litellm-dashboard/src/components/shared/charts/line_chart.tsx b/ui/litellm-dashboard/src/components/shared/charts/line_chart.tsx new file mode 100644 index 00000000000..2dc8747118d --- /dev/null +++ b/ui/litellm-dashboard/src/components/shared/charts/line_chart.tsx @@ -0,0 +1,99 @@ +"use client"; + +import * as React from "react"; +import { CartesianGrid, Line, LineChart as RechartsLineChart, XAxis, YAxis } from "recharts"; +import { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, type ChartConfig } from "@/components/ui/chart"; +import { cn } from "@/lib/cva.config"; +import { ValueTooltip, type ChartTooltipComponent } from "./chart_tooltip"; +import { categoryFills, type ChartColor } from "./colors"; + +export type LineChartCurveType = "linear" | "natural" | "monotone" | "step"; + +export type LineChartProps> = { + data: readonly TDatum[]; + index: string; + categories: readonly string[]; + colors?: readonly ChartColor[]; + valueFormatter?: (value: number) => string; + yAxisWidth?: number; + tickGap?: number; + showLegend?: boolean; + showXAxis?: boolean; + showGridLines?: boolean; + showTooltip?: boolean; + customTooltip?: ChartTooltipComponent; + connectNulls?: boolean; + curveType?: LineChartCurveType; + className?: string; + style?: React.CSSProperties; +}; + +export function LineChart>({ + data, + index, + categories, + colors, + valueFormatter, + yAxisWidth = 56, + tickGap = 5, + showLegend = true, + showXAxis = true, + showGridLines = true, + showTooltip = true, + customTooltip, + connectNulls = false, + curveType = "linear", + className, + style, +}: LineChartProps) { + const fills = categoryFills(categories.length, colors); + const config: ChartConfig = Object.fromEntries(categories.map((category) => [category, { label: category }])); + const TooltipContent = customTooltip ?? ValueTooltip; + + return ( + + + {showGridLines && } + + + {showTooltip && ( + ( + + )} + /> + )} + {showLegend && ( + } + /> + )} + {categories.map((category, i) => ( + + ))} + + + ); +}