diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.integration.test.tsx index 78280e32eed..51dfe9fda90 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.integration.test.tsx @@ -24,12 +24,18 @@ describe("useScopedDailyActivityRange wiring", () => { ); global.fetch = mockFetch; - renderHook(() => useScopedDailyActivityRange("sk-token", { userId: "u1", apiKey: "hash-abc" })); + const activity = { + dateValue: { from: new Date(2026, 7, 1), to: new Date(2026, 7, 10) }, + onDateChange: vi.fn(), + }; + renderHook(() => useScopedDailyActivityRange("sk-token", { userId: "u1", apiKey: "hash-abc" }, activity)); await waitFor(() => expect(mockFetch).toHaveBeenCalled()); const url = String(mockFetch.mock.calls[0][0]); expect(url).toContain("user_id=u1"); expect(url).toContain("api_key=hash-abc"); + expect(url).toContain("start_date=2026-08-01"); + expect(url).toContain("end_date=2026-08-10"); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts index 234144f4423..3435b57dbc8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts @@ -37,7 +37,9 @@ export interface DailyActivityScope { apiKey?: string | null; } -export const useActivityDateRange = (): Pick => { +export type ActivityDateRange = Pick; + +export const useActivityDateRange = (): ActivityDateRange => { const initialFrom = useMemo(() => new Date(new Date().getTime() - THIRTY_DAYS_MS), []); const initialTo = useMemo(() => new Date(), []); const [dateValue, setDateValue] = useState({ from: initialFrom, to: initialTo }); @@ -47,9 +49,8 @@ export const useActivityDateRange = (): Pick { - const { dateValue, onDateChange } = useActivityDateRange(); - const startTime = dateValue.from ?? null; const endTime = dateValue.to ?? null; const { userId, apiKey = null } = scope; @@ -82,7 +83,7 @@ export const useDailyActivityRange = ( accessToken: string | null, userId: string | null, userRole: string, -): DailyActivityRange => - useScopedDailyActivityRange(accessToken, { - userId: spendScopeUserId(userRole, userId), - }); +): DailyActivityRange => { + const dateRange = useActivityDateRange(); + return useScopedDailyActivityRange(accessToken, { userId: spendScopeUserId(userRole, userId) }, dateRange); +}; diff --git a/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.integration.test.tsx b/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.integration.test.tsx index 69d2f1b896b..8cfd9d1941e 100644 --- a/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.integration.test.tsx @@ -75,7 +75,11 @@ describe("KeyAutoRouterUsageTab", () => { }); it("renders this key's spend, baseline, savings and per-router filter", async () => { - renderWithProviders(); + const activity = { + dateValue: { from: new Date(2025, 0, 1), to: new Date(2025, 0, 31) }, + onDateChange: vi.fn(), + }; + renderWithProviders(); expect(await screen.findByText("$8.75")).toBeInTheDocument(); expect(screen.getByText("Actual auto-router spend")).toBeInTheDocument(); @@ -88,5 +92,7 @@ describe("KeyAutoRouterUsageTab", () => { const benchmarkUrl = new URL(requestedUrls().find((url) => url.includes("/auto_router/benchmarks")) ?? ""); expect(benchmarkUrl.searchParams.get("api_key")).toBe("key-hash-1"); + expect(benchmarkUrl.searchParams.get("start_date")).toBe("2025-01-01"); + expect(benchmarkUrl.searchParams.get("end_date")).toBe("2025-01-31"); }); }); diff --git a/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.tsx b/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.tsx index a2a7b9079a5..9e1cee1ec2e 100644 --- a/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeyAutoRouterUsageTab.tsx @@ -3,15 +3,16 @@ import React from "react"; import { AutoRouterUsageView } from "@/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab"; -import { useActivityDateRange } from "@/app/(dashboard)/cost-optimization/_components/useDailyActivityRange"; +import type { ActivityDateRange } from "@/app/(dashboard)/cost-optimization/_components/useDailyActivityRange"; interface KeyAutoRouterUsageTabProps { accessToken: string | null; keyToken: string; + activity: ActivityDateRange; } -const KeyAutoRouterUsageTab: React.FC = ({ accessToken, keyToken }) => ( - +const KeyAutoRouterUsageTab: React.FC = ({ accessToken, keyToken, activity }) => ( + ); export default KeyAutoRouterUsageTab; diff --git a/ui/litellm-dashboard/src/components/templates/KeySavingsTab.integration.test.tsx b/ui/litellm-dashboard/src/components/templates/KeySavingsTab.integration.test.tsx index 6c7d65ee566..385c3967d02 100644 --- a/ui/litellm-dashboard/src/components/templates/KeySavingsTab.integration.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeySavingsTab.integration.test.tsx @@ -46,6 +46,11 @@ const mockActivity = ( const scopedRange = () => vi.spyOn(useScopedDailyActivityRangeModule, "useScopedDailyActivityRange"); +const activity = { + dateValue: { from: new Date(2025, 0, 1), to: new Date(2025, 0, 31) }, + onDateChange: vi.fn(), +}; + const renderTab = (props: Partial> = {}) => render( > = keyToken="key-abc123" userId="user-123" userRole="Internal User" + activity={activity} {...props} />, ); @@ -114,7 +120,7 @@ describe("KeySavingsTab", () => { renderTab({ userId: "user-456", userRole: "Internal User" }); - expect(hook).toHaveBeenCalledWith("test-token", { userId: "user-456", apiKey: "key-abc123" }); + expect(hook).toHaveBeenCalledWith("test-token", { userId: "user-456", apiKey: "key-abc123" }, activity); expect(screen.getByTestId("key-savings-scope-note")).toHaveTextContent("Showing your own requests"); }); @@ -123,7 +129,7 @@ describe("KeySavingsTab", () => { renderTab({ userId: "admin-123", userRole: "Admin" }); - expect(hook).toHaveBeenCalledWith("test-token", { userId: null, apiKey: "key-abc123" }); + expect(hook).toHaveBeenCalledWith("test-token", { userId: null, apiKey: "key-abc123" }, activity); expect(screen.queryByTestId("key-savings-scope-note")).not.toBeInTheDocument(); }); @@ -132,7 +138,7 @@ describe("KeySavingsTab", () => { renderTab({ userId: "org-admin-1", userRole: "Org Admin" }); - expect(hook).toHaveBeenCalledWith("test-token", { userId: "org-admin-1", apiKey: "key-abc123" }); + expect(hook).toHaveBeenCalledWith("test-token", { userId: "org-admin-1", apiKey: "key-abc123" }, activity); expect(screen.getByTestId("key-savings-scope-note")).toHaveTextContent("Showing your own requests"); }); }); diff --git a/ui/litellm-dashboard/src/components/templates/KeySavingsTab.tsx b/ui/litellm-dashboard/src/components/templates/KeySavingsTab.tsx index 967c3c7eff0..c33529eb042 100644 --- a/ui/litellm-dashboard/src/components/templates/KeySavingsTab.tsx +++ b/ui/litellm-dashboard/src/components/templates/KeySavingsTab.tsx @@ -22,7 +22,10 @@ import { usd, withStartAnchor, } from "@/app/(dashboard)/cost-optimization/_components/costOptimizationUtils"; -import { useScopedDailyActivityRange } from "@/app/(dashboard)/cost-optimization/_components/useDailyActivityRange"; +import { + useScopedDailyActivityRange, + type ActivityDateRange, +} from "@/app/(dashboard)/cost-optimization/_components/useDailyActivityRange"; interface KeySavingsTabProps { accessToken: string | null; @@ -30,19 +33,19 @@ interface KeySavingsTabProps { keyToken: string; userId: string | null; userRole: string; + activity: ActivityDateRange; } -const KeySavingsTab: React.FC = ({ accessToken, keyToken, userId, userRole }) => { +const KeySavingsTab: React.FC = ({ accessToken, keyToken, userId, userRole, activity }) => { // Proxy admins read the whole key. For anyone else the endpoint applies the caller's own user_id // alongside the key filter, so the figures cover only that viewer's requests on this key -- said // plainly in the scope note below rather than left to be misread as the key's total. const readsWholeKey = hasProxyWideSpendView(userRole); - const activity = useScopedDailyActivityRange(accessToken, { - userId: spendScopeUserId(userRole, userId), - apiKey: keyToken, - }); - - const { dateValue, onDateChange, results, loading, isFetchingMore } = activity; + const { dateValue, onDateChange, results, loading, isFetchingMore } = useScopedDailyActivityRange( + accessToken, + { userId: spendScopeUserId(userRole, userId), apiKey: keyToken }, + activity, + ); const startTime = dateValue.from ?? null; const endTime = dateValue.to ?? null; diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx index feee1cee9e6..175e57f2f44 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.test.tsx @@ -26,8 +26,28 @@ vi.mock("./key_edit_view", () => ({ }, })); +import type { ActivityDateRange } from "@/app/(dashboard)/cost-optimization/_components/useDailyActivityRange"; + +const AnalyticsDateControl = ({ activity, keyToken }: { activity: ActivityDateRange; keyToken: string }) => ( +
+ {keyToken} + {activity.dateValue.from?.toISOString()} + {[1, 10].map((day) => ( + + ))} +
+); + vi.mock("./KeyAutoRouterUsageTab", () => ({ - default: ({ keyToken }: { keyToken: string }) =>
{keyToken}
, + default: (props: React.ComponentProps) => , +})); +vi.mock("./KeySavingsTab", () => ({ + default: (props: React.ComponentProps) => , })); vi.mock("@/app/(dashboard)/hooks/useTeams", () => ({ @@ -189,6 +209,22 @@ describe("KeyInfoView", () => { expect(screen.getByTestId("key-auto-router-usage")).toHaveTextContent("test-token-123"); }); + it("preserves dates in both directions across unmounted analytics panels", async () => { + vi.mocked(useAuthorized).mockReturnValue({ ...baseUseAuthorizedMock, userRole: "Admin" }); + renderWithProviders( {}} keyId="test-key-id" teams={[]} />); + + expect(screen.queryByLabelText("Selected dates")).not.toBeInTheDocument(); + await userEvent.click(screen.getByRole("tab", { name: "Savings" })); + await userEvent.click(screen.getByRole("button", { name: "Select August 1" })); + await userEvent.click(screen.getByRole("tab", { name: "Auto-router usage" })); + expect(screen.getByLabelText("Selected dates")).toHaveTextContent("2026-08-01T00:00:00.000Z"); + await userEvent.click(screen.getByRole("button", { name: "Select August 10" })); + await userEvent.click(screen.getByRole("tab", { name: "Settings" })); + expect(screen.queryByLabelText("Selected dates")).not.toBeInTheDocument(); + await userEvent.click(screen.getByRole("tab", { name: "Savings" })); + expect(screen.getByLabelText("Selected dates")).toHaveTextContent("2026-08-10T00:00:00.000Z"); + }); + it("does not offer the admin-only auto-router usage tab to an internal user", () => { vi.mocked(useAuthorized).mockReturnValue({ ...baseUseAuthorizedMock, userRole: "Internal User" }); renderWithProviders( {}} keyId="test-key-id" teams={[]} />); diff --git a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx index 86bbfd73410..f5c682a2ee0 100644 --- a/ui/litellm-dashboard/src/components/templates/key_info_view.tsx +++ b/ui/litellm-dashboard/src/components/templates/key_info_view.tsx @@ -17,6 +17,7 @@ import { BadgeLink } from "@/components/shared/BadgeLink"; import { KeyInfoHeader } from "./KeyInfoHeader"; import KeySavingsTab from "./KeySavingsTab"; import KeyAutoRouterUsageTab from "./KeyAutoRouterUsageTab"; +import { useActivityDateRange } from "@/app/(dashboard)/cost-optimization/_components/useDailyActivityRange"; import { useEffect, useState } from "react"; import { hasProxyWideSpendView, @@ -87,6 +88,7 @@ export default function KeyInfoView({ backButtonText = "Back to Keys", }: KeyInfoViewProps) { const { accessToken, userId: userID, userRole, premiumUser } = useAuthorized(); + const activityDateRange = useActivityDateRange(); const queryClient = useQueryClient(); const canEditGuardrails = premiumUser || (userRole != null && rolesWithWriteAccess.includes(userRole)); const { teams: teamsData } = useTeams(); @@ -772,12 +774,17 @@ export default function KeyInfoView({ keyToken={currentKeyData.token} userId={userID} userRole={userRole} + activity={activityDateRange} /> {hasProxyWideSpendView(userRole) && ( - + )}