mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-15 09:12:18 +00:00
fix(ui): share key analytics date range
This commit is contained in:
parent
9c17332a6a
commit
b1d0c6ea26
8 changed files with 91 additions and 25 deletions
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -37,7 +37,9 @@ export interface DailyActivityScope {
|
|||
apiKey?: string | null;
|
||||
}
|
||||
|
||||
export const useActivityDateRange = (): Pick<DailyActivityRange, "dateValue" | "onDateChange"> => {
|
||||
export type ActivityDateRange = Pick<DailyActivityRange, "dateValue" | "onDateChange">;
|
||||
|
||||
export const useActivityDateRange = (): ActivityDateRange => {
|
||||
const initialFrom = useMemo(() => new Date(new Date().getTime() - THIRTY_DAYS_MS), []);
|
||||
const initialTo = useMemo(() => new Date(), []);
|
||||
const [dateValue, setDateValue] = useState<DateRange>({ from: initialFrom, to: initialTo });
|
||||
|
|
@ -47,9 +49,8 @@ export const useActivityDateRange = (): Pick<DailyActivityRange, "dateValue" | "
|
|||
export const useScopedDailyActivityRange = (
|
||||
accessToken: string | null,
|
||||
scope: DailyActivityScope,
|
||||
{ dateValue, onDateChange }: ActivityDateRange,
|
||||
): DailyActivityRange => {
|
||||
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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -75,7 +75,11 @@ describe("KeyAutoRouterUsageTab", () => {
|
|||
});
|
||||
|
||||
it("renders this key's spend, baseline, savings and per-router filter", async () => {
|
||||
renderWithProviders(<KeyAutoRouterUsageTab accessToken="test-token" keyToken="key-hash-1" />);
|
||||
const activity = {
|
||||
dateValue: { from: new Date(2025, 0, 1), to: new Date(2025, 0, 31) },
|
||||
onDateChange: vi.fn(),
|
||||
};
|
||||
renderWithProviders(<KeyAutoRouterUsageTab accessToken="test-token" keyToken="key-hash-1" activity={activity} />);
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<KeyAutoRouterUsageTabProps> = ({ accessToken, keyToken }) => (
|
||||
<AutoRouterUsageView accessToken={accessToken} activity={useActivityDateRange()} apiKey={keyToken} />
|
||||
const KeyAutoRouterUsageTab: React.FC<KeyAutoRouterUsageTabProps> = ({ accessToken, keyToken, activity }) => (
|
||||
<AutoRouterUsageView accessToken={accessToken} activity={activity} apiKey={keyToken} />
|
||||
);
|
||||
|
||||
export default KeyAutoRouterUsageTab;
|
||||
|
|
|
|||
|
|
@ -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<React.ComponentProps<typeof KeySavingsTab>> = {}) =>
|
||||
render(
|
||||
<KeySavingsTab
|
||||
|
|
@ -53,6 +58,7 @@ const renderTab = (props: Partial<React.ComponentProps<typeof KeySavingsTab>> =
|
|||
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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<KeySavingsTabProps> = ({ accessToken, keyToken, userId, userRole }) => {
|
||||
const KeySavingsTab: React.FC<KeySavingsTabProps> = ({ 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }) => (
|
||||
<div>
|
||||
<span data-testid="key-auto-router-usage">{keyToken}</span>
|
||||
<output aria-label="Selected dates">{activity.dateValue.from?.toISOString()}</output>
|
||||
{[1, 10].map((day) => (
|
||||
<button
|
||||
key={day}
|
||||
onClick={() => activity.onDateChange({ from: new Date(Date.UTC(2026, 7, day)), to: new Date(2026, 7, 20) })}
|
||||
>
|
||||
Select August {day}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
vi.mock("./KeyAutoRouterUsageTab", () => ({
|
||||
default: ({ keyToken }: { keyToken: string }) => <div data-testid="key-auto-router-usage">{keyToken}</div>,
|
||||
default: (props: React.ComponentProps<typeof AnalyticsDateControl>) => <AnalyticsDateControl {...props} />,
|
||||
}));
|
||||
vi.mock("./KeySavingsTab", () => ({
|
||||
default: (props: React.ComponentProps<typeof AnalyticsDateControl>) => <AnalyticsDateControl {...props} />,
|
||||
}));
|
||||
|
||||
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(<KeyInfoView keyData={MOCK_KEY_DATA} onClose={() => {}} 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(<KeyInfoView keyData={MOCK_KEY_DATA} onClose={() => {}} keyId="test-key-id" teams={[]} />);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{hasProxyWideSpendView(userRole) && (
|
||||
<TabsContent value="auto-router-usage">
|
||||
<KeyAutoRouterUsageTab accessToken={accessToken} keyToken={currentKeyData.token} />
|
||||
<KeyAutoRouterUsageTab
|
||||
accessToken={accessToken}
|
||||
keyToken={currentKeyData.token}
|
||||
activity={activityDateRange}
|
||||
/>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue