fix(ui): share key analytics date range

This commit is contained in:
Tin Chi Lo 2026-09-07 11:14:25 -07:00
parent 9c17332a6a
commit b1d0c6ea26
8 changed files with 91 additions and 25 deletions

View file

@ -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");
});
});

View file

@ -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);
};

View file

@ -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");
});
});

View file

@ -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;

View file

@ -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");
});
});

View file

@ -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;

View file

@ -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={[]} />);

View file

@ -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>
)}