diff --git a/ui/litellm-dashboard/eslint-suppressions.json b/ui/litellm-dashboard/eslint-suppressions.json index 7a3c8f4a42c..727dd81bcb3 100644 --- a/ui/litellm-dashboard/eslint-suppressions.json +++ b/ui/litellm-dashboard/eslint-suppressions.json @@ -4,31 +4,6 @@ "count": 1 } }, - "src/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails.ts": { - "no-restricted-syntax": { - "count": 1 - } - }, - "src/app/(dashboard)/hooks/accessGroups/useAccessGroups.ts": { - "no-restricted-syntax": { - "count": 1 - } - }, - "src/app/(dashboard)/hooks/accessGroups/useCreateAccessGroup.ts": { - "no-restricted-syntax": { - "count": 1 - } - }, - "src/app/(dashboard)/hooks/accessGroups/useDeleteAccessGroup.ts": { - "no-restricted-syntax": { - "count": 1 - } - }, - "src/app/(dashboard)/hooks/accessGroups/useEditAccessGroup.ts": { - "no-restricted-syntax": { - "count": 1 - } - }, "src/app/(dashboard)/hooks/blogPosts/useBlogPosts.ts": { "no-restricted-syntax": { "count": 1 diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails.ts index 3dcf73388a5..df44804c9a4 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails.ts @@ -1,51 +1,25 @@ -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { getProxyBaseUrl, getGlobalLitellmHeaderName, deriveErrorMessage, handleError } from "@/components/networking"; +import { useQueryClient } from "@tanstack/react-query"; +import { $api, authHeader } from "@/lib/http/api"; import { all_admin_roles } from "@/utils/roles"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import { AccessGroupResponse, accessGroupKeys } from "./useAccessGroups"; - -// ── Fetch function ─────────────────────────────────────────────────────────── - -const fetchAccessGroupDetails = async (accessToken: string, accessGroupId: string): Promise => { - const baseUrl = getProxyBaseUrl(); - const url = `${baseUrl}/v1/access_group/${encodeURIComponent(accessGroupId)}`; - - const response = await fetch(url, { - method: "GET", - headers: { - [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - return response.json(); -}; - -// ── Hook ───────────────────────────────────────────────────────────────────── +import { AccessGroupResponse } from "./useAccessGroups"; export const useAccessGroupDetails = (accessGroupId?: string) => { const { accessToken, userRole } = useAuthorized(); const queryClient = useQueryClient(); - return useQuery({ - queryKey: accessGroupKeys.detail(accessGroupId!), - queryFn: async () => fetchAccessGroupDetails(accessToken!, accessGroupId!), - enabled: Boolean(accessToken && accessGroupId) && all_admin_roles.includes(userRole || ""), - - // Seed from the list cache when available - initialData: () => { - if (!accessGroupId) return undefined; - - const groups = queryClient.getQueryData(accessGroupKeys.list({})); - - return groups?.find((g) => g.access_group_id === accessGroupId); + return $api.useQuery( + "get", + "/v1/access_group/{access_group_id}", + { params: { path: { access_group_id: accessGroupId ?? "" } }, headers: authHeader(accessToken!) }, + { + enabled: Boolean(accessToken && accessGroupId) && all_admin_roles.includes(userRole || ""), + initialData: () => { + if (!accessGroupId) return undefined; + const listKey = $api.queryOptions("get", "/v1/access_group", { headers: authHeader(accessToken!) }).queryKey; + const groups = queryClient.getQueryData(listKey); + return groups?.find((g) => g.access_group_id === accessGroupId); + }, }, - }); + ); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.test.ts index b15ea4491e9..a2f4a8c86c0 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.test.ts @@ -1,242 +1,88 @@ -/* @vitest-environment jsdom */ -import React from "react"; -import { renderHook, waitFor } from "@testing-library/react"; import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { useAccessGroups, AccessGroupResponse } from "./useAccessGroups"; -import * as networking from "@/components/networking"; +import React, { ReactNode } from "react"; +import { useAccessGroups } from "./useAccessGroups"; +import type { paths } from "@/lib/http/schema"; + +type AccessGroupListResponse = paths["/v1/access_group"]["get"]["responses"][200]["content"]["application/json"]; + +const { fetchMock } = vi.hoisted(() => { + const fetchMock = vi.fn(); + globalThis.fetch = fetchMock as unknown as typeof fetch; + return { fetchMock }; +}); vi.mock("@/components/networking", () => ({ - getProxyBaseUrl: vi.fn(() => "http://proxy.example"), - getGlobalLitellmHeaderName: vi.fn(() => "Authorization"), - deriveErrorMessage: vi.fn((data: unknown) => (data as { detail?: string })?.detail ?? "Unknown error"), - handleError: vi.fn(), + getProxyBaseUrl: () => "http://localhost:4000", + getGlobalLitellmHeaderName: () => "Authorization", })); +const mockUseAuthorized = vi.fn(); vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ - default: vi.fn(() => ({ - accessToken: "test-token-123", - userRole: "Admin", - })), + default: () => mockUseAuthorized(), })); -const createQueryClient = () => - new QueryClient({ - defaultOptions: { - queries: { - retry: false, - gcTime: 0, - }, - }, - }); +const DEFAULT_AUTH = { accessToken: "test-access-token", userRole: "Admin" }; -const wrapper = ({ children }: { children: React.ReactNode }) => { - const queryClient = createQueryClient(); - return React.createElement(QueryClientProvider, { client: queryClient }, children); -}; +const requestOf = (arg: unknown): Request => arg as Request; +const jsonResponse = (body: unknown, status = 200): Response => + new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); -const mockAccessToken = "test-token-123"; -const mockAccessGroups: AccessGroupResponse[] = [ +const groups: AccessGroupListResponse = [ { access_group_id: "ag-1", access_group_name: "Group One", - description: "First group", + description: null, access_model_names: [], access_mcp_server_ids: [], access_agent_ids: [], assigned_team_ids: [], assigned_key_ids: [], created_at: "2025-01-01T00:00:00Z", - created_by: "user-1", + created_by: null, updated_at: "2025-01-01T00:00:00Z", - updated_by: "user-1", + updated_by: null, }, ]; -const fetchMock = vi.fn(); - describe("useAccessGroups", () => { - beforeEach(async () => { - vi.clearAllMocks(); - vi.mocked(networking.getProxyBaseUrl).mockReturnValue("http://proxy.example"); - vi.mocked(networking.getGlobalLitellmHeaderName).mockReturnValue("Authorization"); + let queryClient: QueryClient; - const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized"); - vi.mocked(useAuthorizedModule.default).mockReturnValue({ - accessToken: mockAccessToken, - userRole: "Admin", - } as any); - - global.fetch = fetchMock; + beforeEach(() => { + queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + fetchMock.mockReset(); + mockUseAuthorized.mockReset(); + mockUseAuthorized.mockReturnValue(DEFAULT_AUTH); }); - it("should return hook result without errors", () => { - fetchMock.mockResolvedValue({ - ok: true, - json: () => Promise.resolve([]), - } as Response); + const wrapper = ({ children }: { children: ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); + + it("fetches GET /v1/access_group with the bearer header and returns typed data", async () => { + fetchMock.mockResolvedValue(jsonResponse(groups)); + const { result } = renderHook(() => useAccessGroups(), { wrapper }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + const request = requestOf(fetchMock.mock.calls[0][0]); + expect(new URL(request.url).pathname).toBe("/v1/access_group"); + expect(request.method).toBe("GET"); + expect(request.headers.get("Authorization")).toBe("Bearer test-access-token"); + expect(result.current.data?.[0].access_group_id).toBe("ag-1"); + }); + + it.each([ + ["null token", { accessToken: null }], + ["non-admin role", { userRole: "Internal User" }], + ])("does not fire a request when gated by %s", (_label, override) => { + fetchMock.mockResolvedValue(jsonResponse(groups)); + mockUseAuthorized.mockReturnValue({ ...DEFAULT_AUTH, ...override }); const { result } = renderHook(() => useAccessGroups(), { wrapper }); - expect(result.current).toBeDefined(); - expect(result.current).toHaveProperty("data"); - expect(result.current).toHaveProperty("isSuccess"); - expect(result.current).toHaveProperty("isError"); - expect(result.current).toHaveProperty("status"); - }); - - it("should return access groups when access token and admin role are present", async () => { - fetchMock.mockResolvedValue({ - ok: true, - json: () => Promise.resolve(mockAccessGroups), - } as Response); - - const { result } = renderHook(() => useAccessGroups(), { wrapper }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(fetchMock).toHaveBeenCalledWith( - "http://proxy.example/v1/access_group", - expect.objectContaining({ - method: "GET", - headers: expect.objectContaining({ - Authorization: `Bearer ${mockAccessToken}`, - "Content-Type": "application/json", - }), - }), - ); - expect(result.current.data).toEqual(mockAccessGroups); - }); - - it("should not fetch when access token is null", async () => { - const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized"); - vi.mocked(useAuthorizedModule.default).mockReturnValue({ - accessToken: null, - userRole: "Admin", - } as any); - - const { result } = renderHook(() => useAccessGroups(), { wrapper }); - - expect(result.current.isFetching).toBe(false); expect(result.current.isLoading).toBe(false); - expect(result.current.data).toBeUndefined(); + expect(result.current.isFetched).toBe(false); expect(fetchMock).not.toHaveBeenCalled(); }); - - it("should not fetch when access token is empty string", async () => { - const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized"); - vi.mocked(useAuthorizedModule.default).mockReturnValue({ - accessToken: "", - userRole: "Admin", - } as any); - - const { result } = renderHook(() => useAccessGroups(), { wrapper }); - - expect(result.current.isFetching).toBe(false); - expect(result.current.isLoading).toBe(false); - expect(result.current.data).toBeUndefined(); - expect(fetchMock).not.toHaveBeenCalled(); - }); - - it("should not fetch when user role is not an admin role", async () => { - const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized"); - vi.mocked(useAuthorizedModule.default).mockReturnValue({ - accessToken: mockAccessToken, - userRole: "Viewer", - } as any); - - const { result } = renderHook(() => useAccessGroups(), { wrapper }); - - expect(result.current.isFetching).toBe(false); - expect(result.current.isLoading).toBe(false); - expect(result.current.data).toBeUndefined(); - expect(fetchMock).not.toHaveBeenCalled(); - }); - - it("should not fetch when user role is null", async () => { - const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized"); - vi.mocked(useAuthorizedModule.default).mockReturnValue({ - accessToken: mockAccessToken, - userRole: null, - } as any); - - const { result } = renderHook(() => useAccessGroups(), { wrapper }); - - expect(result.current.isFetching).toBe(false); - expect(result.current.isLoading).toBe(false); - expect(result.current.data).toBeUndefined(); - expect(fetchMock).not.toHaveBeenCalled(); - }); - - it("should fetch when user role is proxy_admin", async () => { - const useAuthorizedModule = await import("@/app/(dashboard)/hooks/useAuthorized"); - vi.mocked(useAuthorizedModule.default).mockReturnValue({ - accessToken: mockAccessToken, - userRole: "proxy_admin", - } as any); - - fetchMock.mockResolvedValue({ - ok: true, - json: () => Promise.resolve(mockAccessGroups), - } as Response); - - const { result } = renderHook(() => useAccessGroups(), { wrapper }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(fetchMock).toHaveBeenCalled(); - expect(result.current.data).toEqual(mockAccessGroups); - }); - - it("should expose error state when fetch fails", async () => { - fetchMock.mockResolvedValue({ - ok: false, - json: () => Promise.resolve({ detail: "Forbidden" }), - } as Response); - vi.mocked(networking.deriveErrorMessage).mockReturnValue("Forbidden"); - - const { result } = renderHook(() => useAccessGroups(), { wrapper }); - - await waitFor(() => { - expect(result.current.isError).toBe(true); - }); - - expect(result.current.error).toBeInstanceOf(Error); - expect((result.current.error as Error).message).toBe("Forbidden"); - expect(result.current.data).toBeUndefined(); - expect(networking.handleError).toHaveBeenCalledWith("Forbidden"); - }); - - it("should return empty array when API returns empty list", async () => { - fetchMock.mockResolvedValue({ - ok: true, - json: () => Promise.resolve([]), - } as Response); - - const { result } = renderHook(() => useAccessGroups(), { wrapper }); - - await waitFor(() => { - expect(result.current.isSuccess).toBe(true); - }); - - expect(result.current.data).toEqual([]); - }); - - it("should propagate network errors", async () => { - const networkError = new Error("Network failure"); - fetchMock.mockRejectedValue(networkError); - - const { result } = renderHook(() => useAccessGroups(), { wrapper }); - - await waitFor(() => { - expect(result.current.isError).toBe(true); - }); - - expect(result.current.error).toEqual(networkError); - expect(result.current.data).toBeUndefined(); - }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.ts index 9f306c21459..d96df36a14e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useAccessGroups.ts @@ -1,62 +1,17 @@ -import { useQuery } from "@tanstack/react-query"; -import { createQueryKeys } from "../common/queryKeysFactory"; -import { getProxyBaseUrl, getGlobalLitellmHeaderName, deriveErrorMessage, handleError } from "@/components/networking"; +import type { components } from "@/lib/http/schema"; +import { $api, authHeader } from "@/lib/http/api"; import { all_admin_roles } from "@/utils/roles"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -// ── Types ──────────────────────────────────────────────────────────────────── - -export interface AccessGroupResponse { - access_group_id: string; - access_group_name: string; - description: string | null; - access_model_names: string[]; - access_mcp_server_ids: string[]; - access_agent_ids: string[]; - assigned_team_ids: string[]; - assigned_key_ids: string[]; - created_at: string; - created_by: string | null; - updated_at: string; - updated_by: string | null; -} - -// ── Query keys (shared across access-group hooks) ──────────────────────────── - -export const accessGroupKeys = createQueryKeys("accessGroups"); - -// ── Fetch function ─────────────────────────────────────────────────────────── - -const fetchAccessGroups = async (accessToken: string): Promise => { - const baseUrl = getProxyBaseUrl(); - const url = `${baseUrl}/v1/access_group`; - - const response = await fetch(url, { - method: "GET", - headers: { - [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - return response.json(); -}; - -// ── Hook ───────────────────────────────────────────────────────────────────── +export type AccessGroupResponse = components["schemas"]["AccessGroupResponse"]; export const useAccessGroups = () => { const { accessToken, userRole } = useAuthorized(); - return useQuery({ - queryKey: accessGroupKeys.list({}), - queryFn: async () => fetchAccessGroups(accessToken!), - enabled: Boolean(accessToken) && all_admin_roles.includes(userRole || ""), - }); + return $api.useQuery( + "get", + "/v1/access_group", + { headers: authHeader(accessToken!) }, + { enabled: Boolean(accessToken) && all_admin_roles.includes(userRole || "") }, + ); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useCreateAccessGroup.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useCreateAccessGroup.test.ts new file mode 100644 index 00000000000..2f728af2b49 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useCreateAccessGroup.test.ts @@ -0,0 +1,81 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { renderHook, waitFor } from "@testing-library/react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React, { ReactNode } from "react"; +import { useCreateAccessGroup } from "./useCreateAccessGroup"; + +const { fetchMock } = vi.hoisted(() => { + const fetchMock = vi.fn(); + globalThis.fetch = fetchMock as unknown as typeof fetch; + return { fetchMock }; +}); + +const handleError = vi.fn(); +const deriveErrorMessage = vi.fn(() => "derived message"); +vi.mock("@/components/networking", () => ({ + getProxyBaseUrl: () => "http://localhost:4000", + getGlobalLitellmHeaderName: () => "Authorization", + handleError: (...args: unknown[]) => handleError(...args), + deriveErrorMessage: (...args: unknown[]) => deriveErrorMessage(...args), +})); + +vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({ + default: () => ({ accessToken: "test-access-token" }), +})); + +const requestOf = (arg: unknown): Request => arg as Request; +const jsonResponse = (body: unknown, status = 200): Response => + new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } }); + +const created = { access_group_id: "ag-new", access_group_name: "New Group" }; + +describe("useCreateAccessGroup", () => { + let queryClient: QueryClient; + + beforeEach(() => { + queryClient = new QueryClient({ defaultOptions: { mutations: { retry: false } } }); + fetchMock.mockReset(); + handleError.mockReset(); + deriveErrorMessage.mockClear(); + }); + + const wrapper = ({ children }: { children: ReactNode }) => + React.createElement(QueryClientProvider, { client: queryClient }, children); + + it("POSTs the body to /v1/access_group with the bearer header", async () => { + fetchMock.mockResolvedValue(jsonResponse(created, 201)); + const { result } = renderHook(() => useCreateAccessGroup(), { wrapper }); + + result.current.mutate({ access_group_name: "New Group", access_model_names: ["gpt-4o"] }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + const request = requestOf(fetchMock.mock.calls[0][0]); + expect(new URL(request.url).pathname).toBe("/v1/access_group"); + expect(request.method).toBe("POST"); + expect(request.headers.get("Authorization")).toBe("Bearer test-access-token"); + expect(await request.json()).toEqual({ access_group_name: "New Group", access_model_names: ["gpt-4o"] }); + }); + + it("invalidates the access-group list on success", async () => { + const invalidate = vi.spyOn(queryClient, "invalidateQueries"); + fetchMock.mockResolvedValue(jsonResponse(created, 201)); + const { result } = renderHook(() => useCreateAccessGroup(), { wrapper }); + + result.current.mutate({ access_group_name: "New Group" }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + expect(invalidate).toHaveBeenCalledWith({ queryKey: ["get", "/v1/access_group"] }); + }); + + it("routes a failed create through the global error handler", async () => { + fetchMock.mockResolvedValue(jsonResponse({ detail: "boom" }, 400)); + const { result } = renderHook(() => useCreateAccessGroup(), { wrapper }); + + result.current.mutate({ access_group_name: "New Group" }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(deriveErrorMessage).toHaveBeenCalled(); + expect(handleError).toHaveBeenCalledWith("derived message"); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useCreateAccessGroup.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useCreateAccessGroup.ts index 5efa2da6557..d953c156fe3 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useCreateAccessGroup.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useCreateAccessGroup.ts @@ -1,63 +1,25 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { getProxyBaseUrl, getGlobalLitellmHeaderName, deriveErrorMessage, handleError } from "@/components/networking"; +import { useQueryClient } from "@tanstack/react-query"; +import type { components } from "@/lib/http/schema"; +import { $api, authHeader } from "@/lib/http/api"; +import { handleError, deriveErrorMessage } from "@/components/networking"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import { AccessGroupResponse, accessGroupKeys } from "./useAccessGroups"; -// ── Types ──────────────────────────────────────────────────────────────────── - -export interface AccessGroupCreateParams { - access_group_name: string; - description?: string | null; - access_model_names?: string[]; - access_mcp_server_ids?: string[]; - access_agent_ids?: string[]; - assigned_team_ids?: string[]; - assigned_key_ids?: string[]; -} - -// ── Fetch function ─────────────────────────────────────────────────────────── - -const createAccessGroup = async ( - accessToken: string, - params: AccessGroupCreateParams, -): Promise => { - const baseUrl = getProxyBaseUrl(); - const url = `${baseUrl}/v1/access_group`; - - const response = await fetch(url, { - method: "POST", - headers: { - [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(params), - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - return response.json(); -}; - -// ── Hook ───────────────────────────────────────────────────────────────────── +export type AccessGroupCreateParams = components["schemas"]["AccessGroupCreateRequest"]; export const useCreateAccessGroup = () => { const { accessToken } = useAuthorized(); const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async (params) => { - if (!accessToken) { - throw new Error("Access token is required"); - } - return createAccessGroup(accessToken, params); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: accessGroupKeys.all }); - }, + const mutation = $api.useMutation("post", "/v1/access_group", { + onError: (error) => handleError(deriveErrorMessage(error)), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["get", "/v1/access_group"] }), }); + + return { + ...mutation, + mutate: (body: AccessGroupCreateParams, options?: Parameters[1]) => + mutation.mutate({ body, headers: authHeader(accessToken!) }, options), + mutateAsync: (body: AccessGroupCreateParams, options?: Parameters[1]) => + mutation.mutateAsync({ body, headers: authHeader(accessToken!) }, options), + }; }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useDeleteAccessGroup.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useDeleteAccessGroup.ts index 01e317f6613..d1ecb270c0e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useDeleteAccessGroup.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useDeleteAccessGroup.ts @@ -1,47 +1,28 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { getProxyBaseUrl, getGlobalLitellmHeaderName, deriveErrorMessage, handleError } from "@/components/networking"; +import { useQueryClient } from "@tanstack/react-query"; +import { $api, authHeader } from "@/lib/http/api"; +import { handleError, deriveErrorMessage } from "@/components/networking"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import { accessGroupKeys } from "./useAccessGroups"; - -// ── Fetch function ─────────────────────────────────────────────────────────── - -const deleteAccessGroup = async (accessToken: string, accessGroupId: string): Promise => { - const baseUrl = getProxyBaseUrl(); - const url = `${baseUrl}/v1/access_group/${encodeURIComponent(accessGroupId)}`; - - const response = await fetch(url, { - method: "DELETE", - headers: { - [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - // 204 No Content — nothing to parse -}; - -// ── Hook ───────────────────────────────────────────────────────────────────── export const useDeleteAccessGroup = () => { const { accessToken } = useAuthorized(); const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async (accessGroupId) => { - if (!accessToken) { - throw new Error("Access token is required"); - } - return deleteAccessGroup(accessToken, accessGroupId); - }, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: accessGroupKeys.all }); - }, + const mutation = $api.useMutation("delete", "/v1/access_group/{access_group_id}", { + onError: (error) => handleError(deriveErrorMessage(error)), + onSuccess: () => queryClient.invalidateQueries({ queryKey: ["get", "/v1/access_group"] }), }); + + return { + ...mutation, + mutate: (accessGroupId: string, options?: Parameters[1]) => + mutation.mutate( + { params: { path: { access_group_id: accessGroupId } }, headers: authHeader(accessToken!) }, + options, + ), + mutateAsync: (accessGroupId: string, options?: Parameters[1]) => + mutation.mutateAsync( + { params: { path: { access_group_id: accessGroupId } }, headers: authHeader(accessToken!) }, + options, + ), + }; }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useEditAccessGroup.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useEditAccessGroup.ts index 7dd85ae93dc..d9b251e6f3f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useEditAccessGroup.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useEditAccessGroup.ts @@ -1,72 +1,42 @@ -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { getProxyBaseUrl, getGlobalLitellmHeaderName, deriveErrorMessage, handleError } from "@/components/networking"; +import { useQueryClient } from "@tanstack/react-query"; +import type { components } from "@/lib/http/schema"; +import { $api, authHeader } from "@/lib/http/api"; +import { handleError, deriveErrorMessage } from "@/components/networking"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import { AccessGroupResponse, accessGroupKeys } from "./useAccessGroups"; -// ── Types ──────────────────────────────────────────────────────────────────── - -export interface AccessGroupUpdateParams { - access_group_name?: string; - description?: string | null; - access_model_names?: string[]; - access_mcp_server_ids?: string[]; - access_agent_ids?: string[]; - assigned_team_ids?: string[]; - assigned_key_ids?: string[]; -} +export type AccessGroupUpdateParams = components["schemas"]["AccessGroupUpdateRequest"]; export interface EditAccessGroupVariables { accessGroupId: string; params: AccessGroupUpdateParams; } -// ── Fetch function ─────────────────────────────────────────────────────────── - -const updateAccessGroup = async ( - accessToken: string, - accessGroupId: string, - params: AccessGroupUpdateParams, -): Promise => { - const baseUrl = getProxyBaseUrl(); - const url = `${baseUrl}/v1/access_group/${encodeURIComponent(accessGroupId)}`; - - const response = await fetch(url, { - method: "PUT", - headers: { - [getGlobalLitellmHeaderName()]: `Bearer ${accessToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify(params), - }); - - if (!response.ok) { - const errorData = await response.json(); - const errorMessage = deriveErrorMessage(errorData); - handleError(errorMessage); - throw new Error(errorMessage); - } - - return response.json(); -}; - -// ── Hook ───────────────────────────────────────────────────────────────────── - export const useEditAccessGroup = () => { const { accessToken } = useAuthorized(); const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ accessGroupId, params }) => { - if (!accessToken) { - throw new Error("Access token is required"); - } - return updateAccessGroup(accessToken, accessGroupId, params); - }, - onSuccess: (_data, { accessGroupId }) => { - queryClient.invalidateQueries({ queryKey: accessGroupKeys.all }); - queryClient.invalidateQueries({ - queryKey: accessGroupKeys.detail(accessGroupId), - }); + const mutation = $api.useMutation("put", "/v1/access_group/{access_group_id}", { + onError: (error) => handleError(deriveErrorMessage(error)), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["get", "/v1/access_group"] }); + queryClient.invalidateQueries({ queryKey: ["get", "/v1/access_group/{access_group_id}"] }); }, }); + + return { + ...mutation, + mutate: ({ accessGroupId, params }: EditAccessGroupVariables, options?: Parameters[1]) => + mutation.mutate( + { params: { path: { access_group_id: accessGroupId } }, body: params, headers: authHeader(accessToken!) }, + options, + ), + mutateAsync: ( + { accessGroupId, params }: EditAccessGroupVariables, + options?: Parameters[1], + ) => + mutation.mutateAsync( + { params: { path: { access_group_id: accessGroupId } }, body: params, headers: authHeader(accessToken!) }, + options, + ), + }; };