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}
/>