diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.test.tsx index 1c06b61698b..005cd8dc69b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.test.tsx @@ -1,5 +1,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; import userEvent from "@testing-library/user-event"; +import type { UrlUpdateEvent } from "nuqs/adapters/testing"; import { renderWithProviders, screen, waitFor, within } from "../../../../../tests/test-utils"; import { ProjectsPage } from "./ProjectsPage"; import { ProjectResponse } from "@/app/(dashboard)/hooks/projects/useProjects"; @@ -20,7 +21,12 @@ vi.mock("./ProjectModals/CreateProjectModal", () => ({ })); vi.mock("./ProjectDetailsPage", () => ({ - ProjectDetail: ({ projectId }: { projectId: string }) =>
{projectId}
, + ProjectDetail: ({ projectId, onBack }: { projectId: string; onBack: () => void }) => ( +
+ {projectId} + +
+ ), })); const mockProjects: ProjectResponse[] = [ @@ -200,6 +206,47 @@ describe("ProjectsPage", () => { }); }); + it("should open the detail view directly from a ?project= deep link", () => { + mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false }); + renderWithProviders(, { searchParams: "?project=proj-2" }); + + expect(screen.getByTestId("project-detail")).toHaveTextContent("proj-2"); + expect(screen.queryByRole("heading", { name: /projects/i })).not.toBeInTheDocument(); + }); + + it("should push ?project= as a new history entry when a project is opened", async () => { + const user = userEvent.setup(); + const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>(); + mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false }); + renderWithProviders(, { onUrlUpdate }); + + await user.click(screen.getByText("proj-1")); + + await waitFor(() => { + expect(onUrlUpdate).toHaveBeenLastCalledWith( + expect.objectContaining({ + queryString: "?project=proj-1", + options: expect.objectContaining({ history: "push" }), + }), + ); + }); + }); + + it("should clear ?project= and return to the list when the detail view is closed", async () => { + const user = userEvent.setup(); + const onUrlUpdate = vi.fn<(event: UrlUpdateEvent) => void>(); + mockUseProjects.mockReturnValue({ data: mockProjects, isLoading: false }); + renderWithProviders(, { searchParams: "?project=proj-1", onUrlUpdate }); + + await user.click(screen.getByRole("button", { name: /back to projects/i })); + + await waitFor(() => { + expect(onUrlUpdate).toHaveBeenLastCalledWith(expect.objectContaining({ queryString: "" })); + }); + expect(screen.queryByTestId("project-detail")).not.toBeInTheDocument(); + expect(screen.getByText("Alpha Project")).toBeInTheDocument(); + }); + it("should resolve team alias from the teams list in the Team column", () => { mockUseTeams.mockReturnValue({ data: [{ team_id: "team-1", team_alias: "Engineering", models: [] }], diff --git a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx index 58c2c4c3ad8..a8419874000 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/projects/_components/ProjectsPage.tsx @@ -3,6 +3,7 @@ import { useTeams } from "@/app/(dashboard)/hooks/teams/useTeams"; import { PlusOutlined } from "@ant-design/icons"; import { Button, Flex, Input, Layout, Space, theme, Typography } from "antd"; import { SearchIcon } from "lucide-react"; +import { parseAsString, useQueryState } from "nuqs"; import { useMemo, useState } from "react"; import { CreateProjectModal } from "./ProjectModals/CreateProjectModal"; import { ProjectDetail } from "./ProjectDetailsPage"; @@ -16,7 +17,10 @@ export function ProjectsPage() { const { data: projects, isLoading } = useProjects(); const { data: teams, isLoading: isTeamsLoading } = useTeams(); - const [selectedProjectId, setSelectedProjectId] = useState(null); + const [selectedProjectId, setSelectedProjectId] = useQueryState( + "project", + parseAsString.withOptions({ history: "push" }), + ); const [isCreateModalVisible, setIsCreateModalVisible] = useState(false); const [searchText, setSearchText] = useState(""); @@ -44,7 +48,7 @@ export function ProjectsPage() { }, [projects, searchText, teamAliasMap]); if (selectedProjectId) { - return setSelectedProjectId(null)} />; + return void setSelectedProjectId(null)} />; } return ( @@ -76,7 +80,7 @@ export function ProjectsPage() { projects={filteredProjects} isLoading={isLoading} isFiltered={searchText.trim().length > 0} - onProjectClick={setSelectedProjectId} + onProjectClick={(id) => void setSelectedProjectId(id)} teamAliasMap={teamAliasMap} isTeamsLoading={isTeamsLoading} />