Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 4 additions & 2 deletions ui/litellm-dashboard/e2e_tests/fixtures/migratedPages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
* Keep this in lockstep with MIGRATED_PAGES in src/utils/migratedPages.ts.
* Pending (add as each PR lands): the leaf-pages batch
* (budgets, caching, cost-tracking, guardrails, guardrails-monitor, logs,
* mcp-servers, memory, policies, projects, prompts, search-tools, skills,
* mcp-servers, memory, policies, prompts, search-tools, skills,
* tag-management, tool-policies, transform-request, ui-theme, vector-stores,
* workflows, access-groups).
* workflows).
*/
export const MIGRATED_E2E_PAGES: Record<string, string> = {
api_ref: "api-reference",
"llm-playground": "playground",
projects: "projects",
"access-groups": "access-groups",
};

export const MIGRATED_E2E_SEGMENTS: string[] = [...new Set(Object.values(MIGRATED_E2E_PAGES))];
17 changes: 16 additions & 1 deletion ui/litellm-dashboard/e2e_tests/globalSetup.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import { chromium, expect } from "@playwright/test";
import { chromium, expect, request } from "@playwright/test";
import { users, Role, STORAGE_PATHS } from "./fixtures/users";
import * as fs from "fs";

async function globalSetup() {
const browser = await chromium.launch();
const rootPath = process.env.SERVER_ROOT_PATH ?? "";

// The Projects sidebar item is hidden unless the enterprise-gated
// enable_projects_ui setting is on, and the seeded DB starts with it off.
// The proxy runs with LITELLM_LICENSE in CI, so enable it the same way
// the admin UI toggle does; the projects migration smoke needs the link.
const masterKey = process.env.LITELLM_MASTER_KEY || "sk-1234";
const api = await request.newContext();
const settingsRes = await api.patch(`http://localhost:4000${rootPath}/update/ui_settings`, {
headers: { Authorization: `Bearer ${masterKey}` },
data: { enable_projects_ui: true },
});
if (!settingsRes.ok()) {
throw new Error(`Enabling enable_projects_ui failed (${settingsRes.status()}): ${await settingsRes.text()}`);
}
await api.dispose();

for (const role of Object.values(Role)) {
const { email, password } = users[role];
const storagePath = STORAGE_PATHS[role];
Expand Down
8 changes: 4 additions & 4 deletions ui/litellm-dashboard/eslint-suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -472,22 +472,22 @@
"count": 4
}
},
"src/components/Projects/ProjectDetailsPage.tsx": {
"src/app/(dashboard)/projects/components/ProjectDetailsPage.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"src/components/Projects/ProjectKeysSection.tsx": {
"src/app/(dashboard)/projects/components/ProjectKeysSection.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
},
"src/components/Projects/ProjectModals/ProjectBaseForm.tsx": {
"src/app/(dashboard)/projects/components/ProjectModals/ProjectBaseForm.tsx": {
"react-hooks/set-state-in-effect": {
"count": 2
}
},
"src/components/Projects/ProjectsPage.tsx": {
"src/app/(dashboard)/projects/components/ProjectsPage.tsx": {
"react-hooks/set-state-in-effect": {
"count": 1
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { AccessGroupResponse } from "@/app/(dashboard)/hooks/accessGroups/useAcc
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { renderWithProviders } from "../../../tests/test-utils";
import { renderWithProviders } from "../../../../../tests/test-utils";
import { AccessGroupDetail } from "./AccessGroupsDetailsPage";

vi.mock("@/app/(dashboard)/hooks/accessGroups/useAccessGroupDetails");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
} from "antd";
import { ArrowLeftIcon, BotIcon, EditIcon, KeyIcon, LayersIcon, ServerIcon, UsersIcon } from "lucide-react";
import { useState } from "react";
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
import DefaultProxyAdminTag from "@/components/common_components/DefaultProxyAdminTag";
import { AccessGroupEditModal } from "./AccessGroupsModal/AccessGroupEditModal";

const { Title, Text } = Typography;
Expand All @@ -28,7 +28,7 @@
onBack: () => void;
}

export function AccessGroupDetail({ accessGroupId, onBack }: AccessGroupDetailProps) {

Check warning on line 31 in ui/litellm-dashboard/src/app/(dashboard)/access-groups/components/AccessGroupsDetailsPage.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Function 'AccessGroupDetail' has a complexity of 36. Maximum allowed is 20
const { data: accessGroup, isLoading } = useAccessGroupDetails(accessGroupId);
const { token } = theme.useToken();
const [isEditModalVisible, setIsEditModalVisible] = useState(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ vi.mock("./AccessGroupsModal/AccessGroupCreateModal", () => ({
) : null,
}));

vi.mock("../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton", () => ({
vi.mock("@/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton", () => ({
default: ({ variant, tooltipText, onClick }: { variant: string; tooltipText: string; onClick: () => void }) => (
<button data-testid={`action-button-${variant.toLowerCase()}`} aria-label={tooltipText} onClick={onClick}>
{variant}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import {
import { Button, Card, Flex, Input, Layout, Pagination, Space, Table, Tag, theme, Tooltip, Typography } from "antd";
import { BotIcon, LayersIcon, SearchIcon, ServerIcon } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
import TableIconActionButton from "../common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
import DeleteResourceModal from "@/components/common_components/DeleteResourceModal";
import TableIconActionButton from "@/components/common_components/IconActionButton/TableIconActionButtons/TableIconActionButton";
import {
SortState,
TableHeaderSortDropdown,
} from "../common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
} from "@/components/common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
import { AccessGroupDetail } from "./AccessGroupsDetailsPage";
import { AccessGroupCreateModal } from "./AccessGroupsModal/AccessGroupCreateModal";
import { AccessGroup } from "./types";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"use client";

import { AccessGroupsPage } from "./components/AccessGroupsPage";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";

export default function AccessGroups() {
useAuthorized();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 useAuthorized() is called twice when this route renders — once here in the wrapper and again inside AccessGroupsPage (line 106 of AccessGroupsPage.tsx), where it's needed for userRole. That doubles the useUIConfig subscription, the cookie read, and the useEffect redirect registration, with no benefit. The Projects route wrapper doesn't have this duplication because ProjectsPage doesn't call useAuthorized internally. Consider either removing the wrapper call (the inner call already handles the redirect guard) or removing the inner call and accepting userRole as a prop from the wrapper.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defensive check im leaving in for the migration

return <AccessGroupsPage />;
}
6 changes: 0 additions & 6 deletions ui/litellm-dashboard/src/app/(dashboard)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@
import UIThemeSettings from "@/components/ui_theme_settings";
import Usage from "@/components/usage";
import UserDashboard from "@/components/user_dashboard";
import { AccessGroupsPage } from "@/components/AccessGroups/AccessGroupsPage";
import { ProjectsPage } from "@/components/Projects/ProjectsPage";
import VectorStoreManagement from "@/components/vector_store_management";
import ToolPoliciesView from "@/components/ToolPoliciesView";
import { MemoryView } from "@/components/MemoryView";
Expand All @@ -55,19 +53,19 @@
import { useRouter, useSearchParams } from "next/navigation";
import { Suspense, useEffect, useMemo, useRef, useState } from "react";

function CreateKeyPageContent() {

Check warning on line 56 in ui/litellm-dashboard/src/app/(dashboard)/page.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Function 'CreateKeyPageContent' has a complexity of 47. Maximum allowed is 20
const { authLoading, token, userID, userRole, userEmail, accessToken, premiumUser, setUserRole, setUserEmail } =
useAuth();

const [teams, setTeams] = useState<Team[] | null>(null);
const [keys, setKeys] = useState<null | any[]>([]);

Check warning on line 61 in ui/litellm-dashboard/src/app/(dashboard)/page.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
const [organizations, setOrganizations] = useState<Organization[]>([]);
const [userModels, setUserModels] = useState<string[]>([]);
const proxySettings = useProxySettings(accessToken);

const router = useRouter();
const searchParams = useSearchParams()!;
const [modelData, setModelData] = useState<any>({ data: [] });

Check warning on line 68 in ui/litellm-dashboard/src/app/(dashboard)/page.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
const [createClicked, setCreateClicked] = useState<boolean>(false);

const { data: uiSettingsData, isLoading: uiSettingsLoading } = useUISettings();
Expand Down Expand Up @@ -139,7 +137,7 @@
// Track if we've already attempted a return URL redirect to prevent race conditions
const hasAttemptedReturnRedirectRef = useRef(false);

const addKey = (data: any) => {

Check warning on line 140 in ui/litellm-dashboard/src/app/(dashboard)/page.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
setKeys((prevData) => (prevData ? [...prevData, data] : [data]));
setCreateClicked(() => !createClicked);
};
Expand Down Expand Up @@ -448,10 +446,6 @@
<TagManagement accessToken={accessToken} userRole={userRole} userID={userID} />
) : page == "skills" || page == "claude-code-plugins" ? (
<ClaudeCodePluginsPanel accessToken={accessToken} userRole={userRole} />
) : page == "access-groups" ? (
<AccessGroupsPage />
) : page == "projects" ? (
<ProjectsPage />
) : page == "vector-stores" ? (
<VectorStoreManagement accessToken={accessToken} userRole={userRole} userID={userID} />
) : page == "tool-policies" ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import userEvent from "@testing-library/user-event";
import { renderWithProviders, screen } from "../../../tests/test-utils";
import { renderWithProviders, screen } from "../../../../../tests/test-utils";
import { ProjectDetail } from "./ProjectDetailsPage";
import { ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import { BarChart } from "@tremor/react";
import { ArrowLeftIcon, DollarSignIcon, EditIcon, KeyIcon, UsersIcon } from "lucide-react";
import { useMemo, useState } from "react";
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
import DefaultProxyAdminTag from "@/components/common_components/DefaultProxyAdminTag";
import { EditProjectModal } from "./ProjectModals/EditProjectModal";

const { Title, Text } = Typography;
Expand All @@ -40,7 +40,7 @@
onBack: () => void;
}

export function ProjectDetail({ projectId, onBack }: ProjectDetailProps) {

Check warning on line 43 in ui/litellm-dashboard/src/app/(dashboard)/projects/components/ProjectDetailsPage.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Function 'ProjectDetail' has a complexity of 28. Maximum allowed is 20
const { data: project, isLoading } = useProjectDetails(projectId);
const { data: teamData } = useTeam(project?.team_id ?? undefined);
// teamInfoCall returns { team_id, team_info: {...}, keys, team_memberships }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from "vitest";
import { renderWithProviders, screen } from "../../../tests/test-utils";
import { renderWithProviders, screen } from "../../../../../tests/test-utils";
import { ProjectKeysSection } from "./ProjectKeysSection";

const mockUseKeys = vi.fn();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from "vitest";
import { renderWithProviders, screen } from "../../../tests/test-utils";
import { renderWithProviders, screen } from "../../../../../tests/test-utils";
import { ProjectKeysTable } from "./ProjectKeysTable";
import { KeyResponse } from "@/components/key_team_helpers/key_list";

Expand All @@ -19,7 +19,7 @@
models: [],
aliases: {},
config: {},
user_id: null as any,

Check warning on line 22 in ui/litellm-dashboard/src/app/(dashboard)/projects/components/ProjectKeysTable.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
team_id: null,
project_id: null,
max_parallel_requests: 0,
Expand Down Expand Up @@ -86,7 +86,7 @@

it("should display '—' when the key alias is null", () => {
// Provide a user_id so only the alias column shows "—" (not the owner column too)
renderWithProviders(<ProjectKeysTable keys={[makeKey({ key_alias: null as any, user_id: "owner-1" })]} />);

Check warning on line 89 in ui/litellm-dashboard/src/app/(dashboard)/projects/components/ProjectKeysTable.test.tsx

View workflow job for this annotation

GitHub Actions / frontend-lint

Unexpected any. Specify a different type
expect(screen.getByText("—")).toBeInTheDocument();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { KeyResponse } from "@/components/key_team_helpers/key_list";
import { Empty, Table, Tooltip } from "antd";
import type { ColumnsType } from "antd/es/table";
import type { SpinProps } from "antd";
import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
import DefaultProxyAdminTag from "@/components/common_components/DefaultProxyAdminTag";

interface ProjectKeysTableProps {
keys: KeyResponse[];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import userEvent from "@testing-library/user-event";
import { renderWithProviders, screen } from "../../../../tests/test-utils";
import { renderWithProviders, screen } from "../../../../../../tests/test-utils";
import { CreateProjectModal } from "./CreateProjectModal";

const mockMutate = vi.fn();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import userEvent from "@testing-library/user-event";
import { renderWithProviders, screen } from "../../../../tests/test-utils";
import { renderWithProviders, screen } from "../../../../../../tests/test-utils";
import { EditProjectModal } from "./EditProjectModal";
import { ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects";

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import { describe, it, expect, vi, beforeEach } from "vitest";
import userEvent from "@testing-library/user-event";
import { renderWithProviders, screen, waitFor } from "../../../../tests/test-utils";
import { renderWithProviders, screen, waitFor } from "../../../../../../tests/test-utils";
import { Form } from "antd";
import { ProjectBaseForm, ProjectFormValues } from "./ProjectBaseForm";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ import type { FormInstance } from "antd";
import { PlusOutlined, MinusCircleOutlined } from "@ant-design/icons";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams";
import { Team } from "../../key_team_helpers/key_list";
import { fetchTeamModels } from "../../organisms/create_key_button";
import { getModelDisplayName } from "../../key_team_helpers/fetch_available_models_team_key";
import { Team } from "@/components/key_team_helpers/key_list";
import { fetchTeamModels } from "@/components/organisms/create_key_button";
import { getModelDisplayName } from "@/components/key_team_helpers/fetch_available_models_team_key";
import { getGuardrailsList } from "@/components/networking";

export interface ProjectFormValues {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import userEvent from "@testing-library/user-event";
import { renderWithProviders, screen, waitFor } from "../../../tests/test-utils";
import { renderWithProviders, screen, waitFor } from "../../../../../tests/test-utils";
import { ProjectsPage } from "./ProjectsPage";
import { ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects";

Expand Down
9 changes: 9 additions & 0 deletions ui/litellm-dashboard/src/app/(dashboard)/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"use client";

import { ProjectsPage } from "./components/ProjectsPage";
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";

export default function Projects() {
useAuthorized();
return <ProjectsPage />;
}
8 changes: 8 additions & 0 deletions ui/litellm-dashboard/src/utils/migratedPages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ describe("migratedHref / legacyPageHref", () => {

expect(MIGRATED_PAGES["llm-playground"]).toBe("playground");
});

it("maps the projects and access-groups sidebar ids to their routes", async () => {
vi.doMock("@/components/networking", () => ({ serverRootPath: "/" }));
const { MIGRATED_PAGES } = await import("./migratedPages");

expect(MIGRATED_PAGES.projects).toBe("projects");
expect(MIGRATED_PAGES["access-groups"]).toBe("access-groups");
});
});

describe("dev server (NODE_ENV=development)", () => {
Expand Down
2 changes: 2 additions & 0 deletions ui/litellm-dashboard/src/utils/migratedPages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const MIGRATED_PAGES: Record<string, string> = {
// Legacy alias: older bookmarks used the hyphenated ?page=api-reference form.
"api-reference": "api-reference",
"llm-playground": "playground",
projects: "projects",
"access-groups": "access-groups",
};

function uiBase(): string {
Expand Down
2 changes: 1 addition & 1 deletion ui/litellm-dashboard/tsconfig.tsbuildinfo

Large diffs are not rendered by default.

Loading