mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-17 23:51:30 +00:00
refactor(ui): give each Cost Optimization tab its own route
Split the Cost Optimization page's four tabs (Usage, Prompt Compression, Autorouter, Prompt Caching) into their own prerendered paths under /cost-optimization, mirroring the Models + Endpoints and Caching per-tab routing. A shared layout renders the header, the experimental banner and the tab bar, deriving the active tab from the pathname; each tab is its own page.tsx that pulls only the props it needs, so deep links and hard-loads to /cost-optimization/compression, /autorouter and /caching resolve to real static HTML with no nginx change. The former CostOptimizationView is removed. The tab bar is rebuilt on the shadcn Tabs primitive and the antd Alert becomes a plain banner, so the page adds no new antd usage.
This commit is contained in:
parent
a507394841
commit
d7e31e73c0
10 changed files with 252 additions and 117 deletions
|
|
@ -1,34 +0,0 @@
|
|||
import { fireEvent, render } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("./UsageTab", () => ({ __esModule: true, default: () => <div data-testid="usage-tab" /> }));
|
||||
vi.mock("./PromptCompressionTab", () => ({ __esModule: true, default: () => <div data-testid="compression-tab" /> }));
|
||||
vi.mock("./AutorouterTab", () => ({ __esModule: true, default: () => <div data-testid="autorouter-tab" /> }));
|
||||
vi.mock("./PromptCachingTab", () => ({ __esModule: true, default: () => <div data-testid="caching-tab" /> }));
|
||||
|
||||
import CostOptimizationView from "./CostOptimizationView";
|
||||
|
||||
const renderView = () => render(<CostOptimizationView accessToken="test-token" userId="u1" userRole="proxy_admin" />);
|
||||
|
||||
describe("CostOptimizationView", () => {
|
||||
it("renders all four cost-optimization tabs", () => {
|
||||
const { getByText } = renderView();
|
||||
|
||||
expect(getByText("Usage")).toBeInTheDocument();
|
||||
expect(getByText("Prompt Compression")).toBeInTheDocument();
|
||||
expect(getByText("Autorouter")).toBeInTheDocument();
|
||||
expect(getByText("Prompt Caching")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("defaults to the Usage tab and switches the active tab on click", () => {
|
||||
const { getByRole } = renderView();
|
||||
|
||||
expect(getByRole("tab", { name: "Usage" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(getByRole("tab", { name: "Prompt Compression" })).toHaveAttribute("aria-selected", "false");
|
||||
|
||||
fireEvent.click(getByRole("tab", { name: "Prompt Compression" }));
|
||||
|
||||
expect(getByRole("tab", { name: "Usage" })).toHaveAttribute("aria-selected", "false");
|
||||
expect(getByRole("tab", { name: "Prompt Compression" })).toHaveAttribute("aria-selected", "true");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,81 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { PiggyBank } from "lucide-react";
|
||||
import { Alert, Tabs } from "antd";
|
||||
|
||||
import UsageTab from "./UsageTab";
|
||||
import PromptCompressionTab from "./PromptCompressionTab";
|
||||
import AutorouterTab from "./AutorouterTab";
|
||||
import PromptCachingTab from "./PromptCachingTab";
|
||||
import { useDailyActivityRange } from "./useDailyActivityRange";
|
||||
|
||||
interface CostOptimizationViewProps {
|
||||
accessToken: string | null;
|
||||
userId: string | null;
|
||||
userRole: string;
|
||||
}
|
||||
|
||||
const CostOptimizationView: React.FC<CostOptimizationViewProps> = ({ accessToken, userId, userRole }) => {
|
||||
const activity = useDailyActivityRange(accessToken, userId, userRole);
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: "usage",
|
||||
label: "Usage",
|
||||
children: <UsageTab accessToken={accessToken} activity={activity} />,
|
||||
},
|
||||
{
|
||||
key: "compression",
|
||||
label: "Prompt Compression",
|
||||
children: <PromptCompressionTab accessToken={accessToken} />,
|
||||
},
|
||||
{
|
||||
key: "autorouter",
|
||||
label: "Autorouter",
|
||||
children: <AutorouterTab accessToken={accessToken} userId={userId} userRole={userRole} />,
|
||||
},
|
||||
{
|
||||
key: "caching",
|
||||
label: "Prompt Caching",
|
||||
children: <PromptCachingTab accessToken={accessToken} activity={activity} />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-6 p-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<PiggyBank className="size-6 text-emerald-600" strokeWidth={1.75} />
|
||||
<h1 className="text-xl font-semibold text-foreground">Cost Optimization</h1>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Track and configure the mechanisms that save you money: prompt compression, prompt caching, and auto routing
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="This is an experimental dashboard"
|
||||
description={
|
||||
<span>
|
||||
Have feedback? Join the discussion{" "}
|
||||
<a
|
||||
href="https://github.com/BerriAI/litellm/discussions/32172"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 underline"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
|
||||
<Tabs defaultActiveKey="usage" items={items} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CostOptimizationView;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import AutorouterTab from "@/app/(dashboard)/cost-optimization/_components/AutorouterTab";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
export default function AutorouterPage() {
|
||||
const { accessToken, userId, userRole } = useAuthorized();
|
||||
return <AutorouterTab accessToken={accessToken} userId={userId} userRole={userRole} />;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import PromptCachingTab from "@/app/(dashboard)/cost-optimization/_components/PromptCachingTab";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
export default function PromptCachingPage() {
|
||||
const { accessToken } = useAuthorized();
|
||||
return <PromptCachingTab accessToken={accessToken} />;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import PromptCompressionTab from "@/app/(dashboard)/cost-optimization/_components/PromptCompressionTab";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
export default function PromptCompressionPage() {
|
||||
const { accessToken } = useAuthorized();
|
||||
return <PromptCompressionTab accessToken={accessToken} />;
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/* @vitest-environment jsdom */
|
||||
import { act, render } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import CostOptimizationLayout from "./layout";
|
||||
|
||||
const { mockPush, navState } = vi.hoisted(() => ({
|
||||
mockPush: vi.fn(),
|
||||
navState: { pathname: "/cost-optimization" },
|
||||
}));
|
||||
vi.mock("next/navigation", () => ({
|
||||
usePathname: () => navState.pathname,
|
||||
useRouter: () => ({ push: mockPush }),
|
||||
}));
|
||||
|
||||
vi.mock("@/components/networking", () => ({ serverRootPath: "" }));
|
||||
|
||||
const renderLayout = () =>
|
||||
render(
|
||||
<CostOptimizationLayout>
|
||||
<div data-testid="tab-content">CHILD</div>
|
||||
</CostOptimizationLayout>,
|
||||
);
|
||||
|
||||
describe("CostOptimizationLayout", () => {
|
||||
beforeEach(() => {
|
||||
navState.pathname = "/cost-optimization";
|
||||
mockPush.mockClear();
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(global as any).ResizeObserver = class {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
};
|
||||
});
|
||||
|
||||
it("renders the tab bar and the active tab's page content", () => {
|
||||
const { getByRole, getByTestId } = renderLayout();
|
||||
expect(getByRole("tab", { name: "Usage" })).toBeInTheDocument();
|
||||
expect(getByRole("tab", { name: "Prompt Compression" })).toBeInTheDocument();
|
||||
expect(getByRole("tab", { name: "Autorouter" })).toBeInTheDocument();
|
||||
expect(getByRole("tab", { name: "Prompt Caching" })).toBeInTheDocument();
|
||||
expect(getByTestId("tab-content")).toHaveTextContent("CHILD");
|
||||
});
|
||||
|
||||
it("marks the base route's Usage tab active", () => {
|
||||
const { getByRole } = renderLayout();
|
||||
expect(getByRole("tab", { name: "Usage" })).toHaveAttribute("aria-selected", "true");
|
||||
expect(getByRole("tab", { name: "Prompt Compression" })).toHaveAttribute("aria-selected", "false");
|
||||
});
|
||||
|
||||
it("navigates to a tab's path when its tab is clicked", async () => {
|
||||
const { getByRole } = renderLayout();
|
||||
await act(async () => {
|
||||
getByRole("tab", { name: "Prompt Compression" }).click();
|
||||
});
|
||||
expect(mockPush).toHaveBeenCalledWith(expect.stringMatching(/\/cost-optimization\/compression\/$/));
|
||||
});
|
||||
|
||||
it("routes the base tab back to the cost-optimization root (no slug)", async () => {
|
||||
navState.pathname = "/cost-optimization/autorouter";
|
||||
const { getByRole } = renderLayout();
|
||||
await act(async () => {
|
||||
getByRole("tab", { name: "Usage" }).click();
|
||||
});
|
||||
expect(mockPush).toHaveBeenCalledWith(expect.stringMatching(/\/cost-optimization\/$/));
|
||||
});
|
||||
|
||||
it("redirects to the base cost-optimization path when the tab slug is unknown", async () => {
|
||||
const replaceMock = vi.fn();
|
||||
const originalLocation = window.location;
|
||||
Object.defineProperty(window, "location", {
|
||||
configurable: true,
|
||||
value: { replace: replaceMock, assign: vi.fn(), href: "http://localhost/", pathname: "/", search: "" },
|
||||
});
|
||||
navState.pathname = "/cost-optimization/bogus";
|
||||
await act(async () => {
|
||||
renderLayout();
|
||||
});
|
||||
expect(replaceMock).toHaveBeenCalledWith(expect.stringMatching(/\/cost-optimization\/$/));
|
||||
Object.defineProperty(window, "location", { configurable: true, value: originalLocation });
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { PiggyBank, Info } from "lucide-react";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
costOptimizationTabHref,
|
||||
slugFromPathname,
|
||||
type CostOptimizationTabSlug,
|
||||
} from "@/app/(dashboard)/cost-optimization/tabRoutes";
|
||||
|
||||
const BASE_TAB_KEY = "usage";
|
||||
|
||||
const ORDERED_KEYS: Array<"" | CostOptimizationTabSlug> = ["", "compression", "autorouter", "caching"];
|
||||
|
||||
const TAB_LABELS: Record<"" | CostOptimizationTabSlug, string> = {
|
||||
"": "Usage",
|
||||
compression: "Prompt Compression",
|
||||
autorouter: "Autorouter",
|
||||
caching: "Prompt Caching",
|
||||
};
|
||||
|
||||
export default function CostOptimizationLayout({ children }: { children: ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
const activeSlug = slugFromPathname(pathname);
|
||||
const isKnownSlug = ORDERED_KEYS.some((slug) => slug === activeSlug);
|
||||
const activeKey = isKnownSlug ? activeSlug || BASE_TAB_KEY : BASE_TAB_KEY;
|
||||
|
||||
useEffect(() => {
|
||||
if (activeSlug !== "" && !isKnownSlug) {
|
||||
window.location.replace(costOptimizationTabHref(""));
|
||||
}
|
||||
}, [activeSlug, isKnownSlug]);
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-6 p-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<PiggyBank className="size-6 text-emerald-600" strokeWidth={1.75} />
|
||||
<h1 className="text-xl font-semibold text-foreground">Cost Optimization</h1>
|
||||
</div>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Track and configure the mechanisms that save you money: prompt compression, prompt caching, and auto routing
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2 rounded-md border border-blue-200 bg-blue-50 p-3 text-sm text-blue-800 dark:border-blue-900 dark:bg-blue-950 dark:text-blue-200">
|
||||
<Info className="mt-0.5 size-4 shrink-0" />
|
||||
<span>
|
||||
This is an experimental dashboard. Have feedback? Join the discussion{" "}
|
||||
<a
|
||||
href="https://github.com/BerriAI/litellm/discussions/32172"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
value={activeKey}
|
||||
onValueChange={(key) => router.push(costOptimizationTabHref(key === BASE_TAB_KEY ? "" : key))}
|
||||
>
|
||||
<TabsList variant="line">
|
||||
{ORDERED_KEYS.map((slug) => (
|
||||
<TabsTrigger key={slug || BASE_TAB_KEY} value={slug || BASE_TAB_KEY}>
|
||||
{TAB_LABELS[slug]}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
|
||||
<div>{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import CostOptimizationView from "./_components/CostOptimizationView";
|
||||
import UsageTab from "./_components/UsageTab";
|
||||
import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized";
|
||||
|
||||
export default function CostOptimizationPage() {
|
||||
const { accessToken, userId, userRole } = useAuthorized();
|
||||
return <CostOptimizationView accessToken={accessToken} userId={userId} userRole={userRole} />;
|
||||
return <UsageTab accessToken={accessToken} userId={userId} userRole={userRole} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
/* @vitest-environment jsdom */
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("@/components/networking", () => ({ serverRootPath: "" }));
|
||||
|
||||
import { COST_OPTIMIZATION_TAB_SLUGS, costOptimizationTabHref, slugFromPathname } from "./tabRoutes";
|
||||
|
||||
describe("slugFromPathname", () => {
|
||||
it("returns empty string for the base path with or without a trailing slash", () => {
|
||||
expect(slugFromPathname("/cost-optimization")).toBe("");
|
||||
expect(slugFromPathname("/cost-optimization/")).toBe("");
|
||||
});
|
||||
|
||||
it("extracts the tab slug from dev and proxy-mounted (/ui) paths", () => {
|
||||
expect(slugFromPathname("/cost-optimization/autorouter")).toBe("autorouter");
|
||||
expect(slugFromPathname("/ui/cost-optimization/compression/")).toBe("compression");
|
||||
});
|
||||
|
||||
it("returns the raw segment for an unknown tab so the layout can redirect to base", () => {
|
||||
expect(slugFromPathname("/ui/cost-optimization/bogus")).toBe("bogus");
|
||||
});
|
||||
|
||||
it("returns empty string when the cost-optimization base segment is not in the path", () => {
|
||||
expect(slugFromPathname("/teams")).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("costOptimizationTabHref", () => {
|
||||
it("builds the trailing-slash base href for the empty slug", () => {
|
||||
expect(costOptimizationTabHref("")).toBe("/ui/cost-optimization/");
|
||||
});
|
||||
|
||||
it("builds a trailing-slash href for every tab slug (required by static export)", () => {
|
||||
for (const slug of COST_OPTIMIZATION_TAB_SLUGS) {
|
||||
expect(costOptimizationTabHref(slug)).toBe(`/ui/cost-optimization/${slug}/`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { migratedHref } from "@/utils/migratedPages";
|
||||
|
||||
export const COST_OPTIMIZATION_BASE_SEGMENT = "cost-optimization";
|
||||
|
||||
export const COST_OPTIMIZATION_TAB_SLUGS = ["compression", "autorouter", "caching"] as const;
|
||||
|
||||
export type CostOptimizationTabSlug = (typeof COST_OPTIMIZATION_TAB_SLUGS)[number];
|
||||
|
||||
export function costOptimizationTabHref(slug: string): string {
|
||||
const base = migratedHref(COST_OPTIMIZATION_BASE_SEGMENT);
|
||||
return slug ? `${base}/${slug}/` : `${base}/`;
|
||||
}
|
||||
|
||||
export function slugFromPathname(pathname: string): string {
|
||||
const parts = pathname.split("/").filter(Boolean);
|
||||
const idx = parts.indexOf(COST_OPTIMIZATION_BASE_SEGMENT);
|
||||
if (idx === -1) {
|
||||
return "";
|
||||
}
|
||||
return parts[idx + 1] ?? "";
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue