From 62d3b5edc2353a5907bc7fce832dc088230d62e5 Mon Sep 17 00:00:00 2001 From: ryan-crabbe-berri Date: Sat, 6 Jun 2026 21:16:54 -0700 Subject: [PATCH] feat(ui): migrate access groups to the openapi-react-query client Rebody the access-group hooks on $api so reads and mutations go through the typed openapi-fetch client instead of hand-rolled fetch helpers. This drops the local fetch wrappers, manual auth headers, manual error parsing, and the createQueryKeys factory; the response/request types now come from schema.d.ts. Public hook signatures are unchanged, so the page, modals, and selector that consume them are untouched. Reads rely on the global QueryCache.onError sink; mutations keep an explicit onError since there is no global mutation sink yet. Tests adopt the vi.hoisted fetch pattern already used by useUsers, asserting the outgoing request and the cache invalidation. Stale raw-fetch suppressions for these files are pruned. --- ui/litellm-dashboard/eslint-suppressions.json | 25 -- .../accessGroups/useAccessGroupDetails.ts | 58 ++-- .../accessGroups/useAccessGroups.test.ts | 260 ++++-------------- .../hooks/accessGroups/useAccessGroups.ts | 63 +---- .../accessGroups/useCreateAccessGroup.test.ts | 81 ++++++ .../accessGroups/useCreateAccessGroup.ts | 70 ++--- .../accessGroups/useDeleteAccessGroup.ts | 59 ++-- .../hooks/accessGroups/useEditAccessGroup.ts | 84 ++---- 8 files changed, 222 insertions(+), 478 deletions(-) create mode 100644 ui/litellm-dashboard/src/app/(dashboard)/hooks/accessGroups/useCreateAccessGroup.test.ts 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, + ), + }; };