Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const MIGRATED_E2E_PAGES: Record<string, string> = {
agents: "agents",
"router-settings": "router-settings",
users: "users",
organizations: "organizations",
};

export const MIGRATED_E2E_SEGMENTS: string[] = [...new Set(Object.values(MIGRATED_E2E_PAGES))];
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
useModelHub,
useModelsInfo,
useSelectedTeamModels,
useUserModels,
type AllProxyModelsResponse,
type PaginatedModelInfoResponse,
type ProxyModel,
Expand Down Expand Up @@ -75,7 +76,7 @@
React.createElement(QueryClientProvider, { client: queryClient }, children);

it("should render without crashing", () => {
(modelInfoCall as any).mockResolvedValue(mockPaginatedModelInfoResponse);

Check warning on line 79 in ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const { result } = renderHook(() => useModelsInfo(), { wrapper });

Expand All @@ -83,7 +84,7 @@
});

it("should return models data when query is successful", async () => {
(modelInfoCall as any).mockResolvedValue(mockPaginatedModelInfoResponse);

Check warning on line 87 in ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const { result } = renderHook(() => useModelsInfo(), { wrapper });

Expand Down Expand Up @@ -113,7 +114,7 @@
});

it("should use custom page and size parameters", async () => {
(modelInfoCall as any).mockResolvedValue(mockPaginatedModelInfoResponse);

Check warning on line 117 in ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const { result } = renderHook(() => useModelsInfo(2, 25), { wrapper });

Expand All @@ -139,7 +140,7 @@
const errorMessage = "Failed to fetch models";
const testError = new Error(errorMessage);

(modelInfoCall as any).mockRejectedValue(testError);

Check warning on line 143 in ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const { result } = renderHook(() => useModelsInfo(), { wrapper });

Expand Down Expand Up @@ -266,7 +267,7 @@
React.createElement(QueryClientProvider, { client: queryClient }, children);

it("should render without crashing", () => {
(modelHubCall as any).mockResolvedValue({ data: [] });

Check warning on line 270 in ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const { result } = renderHook(() => useModelHub(), { wrapper });

Expand All @@ -275,7 +276,7 @@

it("should return model hub data when query is successful", async () => {
const mockHubData = { data: [{ id: "hub-1", name: "Test Hub" }] };
(modelHubCall as any).mockResolvedValue(mockHubData);

Check warning on line 279 in ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const { result } = renderHook(() => useModelHub(), { wrapper });

Expand All @@ -297,7 +298,7 @@
const errorMessage = "Failed to fetch model hub";
const testError = new Error(errorMessage);

(modelHubCall as any).mockRejectedValue(testError);

Check warning on line 301 in ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const { result } = renderHook(() => useModelHub(), { wrapper });

Expand Down Expand Up @@ -364,7 +365,7 @@
React.createElement(QueryClientProvider, { client: queryClient }, children);

it("should render without crashing", () => {
(modelAvailableCall as any).mockResolvedValue(mockAllProxyModelsResponse);

Check warning on line 368 in ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const { result } = renderHook(() => useAllProxyModels(), { wrapper });

Expand All @@ -372,7 +373,7 @@
});

it("should return all proxy models data when query is successful", async () => {
(modelAvailableCall as any).mockResolvedValue(mockAllProxyModelsResponse);

Check warning on line 376 in ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const { result } = renderHook(() => useAllProxyModels(), { wrapper });

Expand Down Expand Up @@ -403,7 +404,7 @@
const errorMessage = "Failed to fetch proxy models";
const testError = new Error(errorMessage);

(modelAvailableCall as any).mockRejectedValue(testError);

Check warning on line 407 in ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModels.test.ts

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type

const { result } = renderHook(() => useAllProxyModels(), { wrapper });

Expand Down Expand Up @@ -480,6 +481,70 @@
});
});

describe("useUserModels", () => {
let queryClient: QueryClient;

beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});

vi.clearAllMocks();

mockUseAuthorized.mockReturnValue({
accessToken: "test-access-token",
userId: "test-user-id",
userRole: "Admin",
token: "test-token",
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
});

const wrapper = ({ children }: { children: ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);

it("maps the available-models response to a list of model ids", async () => {
(modelAvailableCall as any).mockResolvedValue({
data: [{ id: "gpt-4" }, { id: "claude-3-opus" }],
});

const { result } = renderHook(() => useUserModels(), { wrapper });

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

expect(result.current.data).toEqual(["gpt-4", "claude-3-opus"]);
expect(modelAvailableCall).toHaveBeenCalledWith("test-access-token", "test-user-id", "Admin");
expect(modelAvailableCall).toHaveBeenCalledTimes(1);
});

it("should not execute query when accessToken is missing", () => {
mockUseAuthorized.mockReturnValue({
accessToken: null,
userId: "test-user-id",
userRole: "Admin",
token: null,
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});

const { result } = renderHook(() => useUserModels(), { wrapper });

expect(result.current.isFetched).toBe(false);
expect(modelAvailableCall).not.toHaveBeenCalled();
});
});

describe("useSelectedTeamModels", () => {
let queryClient: QueryClient;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useQuery, useInfiniteQuery } from "@tanstack/react-query";
import { useQuery, useInfiniteQuery, UseQueryResult } from "@tanstack/react-query";
import { createQueryKeys } from "../common/queryKeysFactory";
import { modelInfoCall, modelHubCall, modelAvailableCall } from "@/components/networking";
import useAuthorized from "../useAuthorized";
Expand Down Expand Up @@ -27,6 +27,7 @@ const modelHubKeys = createQueryKeys("modelHub");
const allProxyModelsKeys = createQueryKeys("allProxyModels");
const selectedTeamModelsKeys = createQueryKeys("selectedTeamModels");
const infiniteModelKeys = createQueryKeys("infiniteModels");
const userModelsKeys = createQueryKeys("userModels");

export const useModelsInfo = (
page: number = 1,
Expand Down Expand Up @@ -76,6 +77,18 @@ export const useAllProxyModels = () => {
});
};

export const useUserModels = (): UseQueryResult<string[]> => {
const { accessToken, userId, userRole } = useAuthorized();
return useQuery<string[]>({
queryKey: userModelsKeys.list({}),
queryFn: async () => {
const response = await modelAvailableCall(accessToken!, userId!, userRole!);
return response["data"].map((model: { id: string }) => model.id);
},
enabled: Boolean(accessToken && userId && userRole),
});
};
Comment thread
ryan-crabbe-berri marked this conversation as resolved.

export const useSelectedTeamModels = (teamID: string | null) => {
const { accessToken, userId, userRole } = useAuthorized();
return useQuery<AllProxyModelsResponse>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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 { useOrganizations } from "./useOrganizations";
import { organizationKeys, useOrganizations } from "./useOrganizations";
import { organizationListCall } from "@/components/networking";
import type { Organization } from "@/components/networking";

Expand Down Expand Up @@ -107,7 +107,7 @@ describe("useOrganizations", () => {

expect(result.current.data).toEqual(mockOrganizations);
expect(result.current.error).toBeNull();
expect(organizationListCall).toHaveBeenCalledWith("test-access-token");
expect(organizationListCall).toHaveBeenCalledWith("test-access-token", null, null);
expect(organizationListCall).toHaveBeenCalledTimes(1);
});

Expand All @@ -131,10 +131,47 @@ describe("useOrganizations", () => {

expect(result.current.error).toEqual(testError);
expect(result.current.data).toBeUndefined();
expect(organizationListCall).toHaveBeenCalledWith("test-access-token");
expect(organizationListCall).toHaveBeenCalledWith("test-access-token", null, null);
expect(organizationListCall).toHaveBeenCalledTimes(1);
});

it("passes org_id and org_alias filters to organizationListCall and caches separately from the unfiltered list", async () => {
(organizationListCall as any).mockResolvedValue(mockOrganizations);

const { result } = renderHook(() => useOrganizations({ org_id: "org-1", org_alias: "Test Organization 1" }), {
wrapper,
});

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

expect(organizationListCall).toHaveBeenCalledWith("test-access-token", "org-1", "Test Organization 1");

(organizationListCall as any).mockResolvedValue([]);
const { result: unfiltered } = renderHook(() => useOrganizations(), { wrapper });

await waitFor(() => {
expect(unfiltered.current.isSuccess).toBe(true);
});

expect(organizationListCall).toHaveBeenLastCalledWith("test-access-token", null, null);
expect(organizationListCall).toHaveBeenCalledTimes(2);
});

it("treats empty-string filters as no filters, writing to the unfiltered cache entry", async () => {
(organizationListCall as any).mockResolvedValue(mockOrganizations);

const { result } = renderHook(() => useOrganizations({ org_id: "", org_alias: "" }), { wrapper });

await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});

expect(organizationListCall).toHaveBeenCalledWith("test-access-token", null, null);
expect(queryClient.getQueryData(organizationKeys.list({}))).toEqual(mockOrganizations);
});

it("should not execute query when accessToken is missing", async () => {
// Mock missing accessToken
mockUseAuthorized.mockReturnValue({
Expand Down Expand Up @@ -243,7 +280,7 @@ describe("useOrganizations", () => {
expect(result.current.isLoading).toBe(false);
});

expect(organizationListCall).toHaveBeenCalledWith("test-access-token");
expect(organizationListCall).toHaveBeenCalledWith("test-access-token", null, null);
expect(organizationListCall).toHaveBeenCalledTimes(1);
});

Expand All @@ -260,7 +297,7 @@ describe("useOrganizations", () => {
});

expect(result.current.data).toEqual([]);
expect(organizationListCall).toHaveBeenCalledWith("test-access-token");
expect(organizationListCall).toHaveBeenCalledWith("test-access-token", null, null);
});

it("should handle network timeout error", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,23 @@ import { useQuery, useQueryClient, UseQueryResult } from "@tanstack/react-query"
import { createQueryKeys } from "../common/queryKeysFactory";

export const organizationKeys = createQueryKeys("organizations");
export const useOrganizations = (): UseQueryResult<Organization[]> => {

export interface OrganizationListFilters {
org_id?: string | null;
org_alias?: string | null;
}

export const useOrganizations = (filters?: OrganizationListFilters): UseQueryResult<Organization[]> => {
const { accessToken, userId, userRole } = useAuthorized();
const orgId = filters?.org_id || null;
const orgAlias = filters?.org_alias || null;
return useQuery<Organization[]>({
queryKey: organizationKeys.list({}),
queryFn: async () => await organizationListCall(accessToken!),
queryKey: organizationKeys.list(
orgId || orgAlias
? { filters: { ...(orgId && { org_id: orgId }), ...(orgAlias && { org_alias: orgAlias }) } }
: {},
),
queryFn: async () => await organizationListCall(accessToken!, orgId, orgAlias),
enabled: Boolean(accessToken && userId && userRole),
});
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"use client";

import OrganizationsTable from "@/components/organizations";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";

export default function OrganizationsPage() {
const { accessToken, userRole, premiumUser } = useAuthorized();
return <OrganizationsTable userRole={userRole ?? ""} accessToken={accessToken} premiumUser={premiumUser ?? false} />;
}
17 changes: 2 additions & 15 deletions ui/litellm-dashboard/src/app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import LoadingScreen from "@/components/common_components/LoadingScreen";
import { Team } from "@/components/key_team_helpers/key_list";
import { Organization, proxyBaseUrl, getInProductNudgesCall } from "@/components/networking";
import OldTeams from "@/components/OldTeams";
import { fetchUserModels, CreateKeyPrefillData } from "@/components/organisms/create_key_button";
import Organizations, { fetchOrganizations } from "@/components/organizations";
import { CreateKeyPrefillData } from "@/components/organisms/create_key_button";
import { fetchOrganizations } from "@/components/organizations";
import PassThroughSettings from "@/components/pass_through_settings";
import { SurveyPrompt, SurveyModal, ClaudeCodePrompt, ClaudeCodeModal } from "@/components/survey";
import Usage from "@/components/usage";
Expand All @@ -32,7 +32,6 @@ function CreateKeyPageContent() {
const [teams, setTeams] = useState<Team[] | null>(null);
const [keys, setKeys] = useState<null | any[]>([]);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const [userModels, setUserModels] = useState<string[]>([]);

const router = useRouter();
const searchParams = useSearchParams()!;
Expand Down Expand Up @@ -172,9 +171,6 @@ function CreateKeyPageContent() {
}, [token]);

useEffect(() => {
if (accessToken && userID && userRole) {
fetchUserModels(userID, userRole, accessToken, setUserModels);
}
if (accessToken && userID && userRole) {
v2TeamListCall(accessToken, 1, 100, {
userID: userRole !== "Admin" && userRole !== "Admin Viewer" ? userID : null,
Expand Down Expand Up @@ -333,15 +329,6 @@ function CreateKeyPageContent() {
premiumUser={premiumUser}
searchParams={searchParams}
/>
) : page == "organizations" ? (
<Organizations
organizations={organizations}
setOrganizations={setOrganizations}
userModels={userModels}
accessToken={accessToken}
userRole={userRole}
premiumUser={premiumUser}
/>
) : page == "pass-through-settings" ? (
<PassThroughSettings
userID={userID}
Expand Down
30 changes: 18 additions & 12 deletions ui/litellm-dashboard/src/components/organizations.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { render } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import React from "react";
import { describe, expect, it, vi } from "vitest";

vi.mock("./vector_store_management/VectorStoreSelector", () => ({
__esModule: true,
Expand All @@ -10,22 +11,27 @@ vi.mock("./mcp_server_management/MCPServerSelector", () => ({
__esModule: true,
default: () => null,
}));
vi.mock("@/app/(dashboard)/hooks/useAuthorized", () => ({
default: () => ({
accessToken: null,
userId: null,
userRole: null,
}),
}));

import OrganizationsTable from "./organizations";

const renderWithQueryClient = (ui: React.ReactElement) => {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } },
});
return render(<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>);
};

describe("OrganizationsTable", () => {
it("should render the OrganizationsTable component", () => {
const setOrganizations = vi.fn();

const { getByText } = render(
<OrganizationsTable
organizations={[]}
userRole="Admin"
userModels={[]}
accessToken={null}
setOrganizations={setOrganizations}
premiumUser={true}
/>,
const { getByText } = renderWithQueryClient(
<OrganizationsTable userRole="Admin" accessToken={null} premiumUser={true} />,
);

expect(getByText("+ Create New Organization")).toBeInTheDocument();
Expand Down
Loading
Loading