diff --git a/pages/api-key-usage/ApiKeyUsagePage.tsx b/pages/api-key-usage/ApiKeyUsagePage.tsx index 843f86b5..8f54d7d5 100644 --- a/pages/api-key-usage/ApiKeyUsagePage.tsx +++ b/pages/api-key-usage/ApiKeyUsagePage.tsx @@ -12,8 +12,13 @@ import { TextInput } from "@code-proxy/ui"; import { ThemeToggleButton } from "@code-proxy/ui"; import type { SearchableCheckboxMultiSelectOption } from "@code-proxy/ui"; import type { TimeRange } from "@features/monitor-widgets/monitor-constants"; +import { LogContentModal } from "@features/log-content-viewer"; import { ModelTag } from "@features/model-tags"; -import { fetchPublicLogs, fetchPublicUsageSummary } from "../api-key-lookup/api"; +import { + fetchPublicLogContent, + fetchPublicLogs, + fetchPublicUsageSummary, +} from "../api-key-lookup/api"; import { LookupResultsToolbar, type ApiKeyLookupTab, @@ -205,6 +210,11 @@ export function ApiKeyUsagePage() { const [selectedModels, setSelectedModels] = useState>(null); const [selectedStatuses, setSelectedStatuses] = useState>(null); + const [contentModal, setContentModal] = useState<{ + id: number; + model: string; + tab: "input" | "output"; + } | null>(null); const abortControllerRef = useRef(null); const fetchIdRef = useRef(0); @@ -216,13 +226,28 @@ export function ApiKeyUsagePage() { const summaryAbortControllerRef = useRef(null); const summaryFetchIdRef = useRef(0); + const handleContentClick = useCallback( + (id: number, tab: "input" | "output", model: string) => setContentModal({ id, tab, model }), + [], + ); + const fetchLogContentPart = useCallback( + (id: number, part: "input" | "output", options?: { signal?: AbortSignal }) => + fetchPublicLogContent({ + id, + apiKey: queriedKey, + part, + signal: options?.signal, + }), + [queriedKey], + ); + const logColumns = useMemo( () => - buildRequestLogsColumns((key) => t(key), undefined, undefined, { + buildRequestLogsColumns((key) => t(key), handleContentClick, undefined, { identityColumn: "none", hideChannel: true, }), - [t], + [handleContentClick, t], ); const modelOptions = useMemo(() => { @@ -293,6 +318,7 @@ export function ApiKeyUsagePage() { }); setSelectedModels(null); setSelectedStatuses(null); + setContentModal(null); setApiKeyName(""); setQuotaLimits(null); setQuotaScopes([]); @@ -637,6 +663,15 @@ export function ApiKeyUsagePage() { )} + setContentModal(null)} + fetchPartFn={fetchLogContentPart} + /> + ({ + fetchPublicLogContent: vi.fn(), fetchPublicLogs: vi.fn(), fetchPublicUsageSummary: vi.fn(), })); vi.mock("../../api-key-lookup/api", () => ({ + fetchPublicLogContent: mocks.fetchPublicLogContent, fetchPublicLogs: mocks.fetchPublicLogs, fetchPublicUsageSummary: mocks.fetchPublicUsageSummary, })); +vi.mock("@features/log-content-viewer", () => ({ + LogContentModal: ({ + open, + logId, + initialTab, + fetchPartFn, + }: { + open: boolean; + logId: number | null; + initialTab?: "input" | "output"; + fetchPartFn?: (id: number, part: "input" | "output") => Promise; + }) => + open && logId ? ( + + ) : null, +})); + vi.mock("../../api-key-lookup/components/QuickImportTabContent", () => ({ QuickImportTabContent: ({ apiKey }: { apiKey: string }) => (
quick-import:{apiKey}
@@ -37,8 +62,15 @@ describe("ApiKeyUsagePage", () => { await i18n.changeLanguage("zh-CN"); window.history.replaceState({}, "", "/manage/apikey-usage"); window.localStorage.clear(); + mocks.fetchPublicLogContent.mockReset(); mocks.fetchPublicLogs.mockReset(); mocks.fetchPublicUsageSummary.mockReset(); + mocks.fetchPublicLogContent.mockResolvedValue({ + id: 1, + model: "gpt-test", + part: "input", + content: "hello", + }); mocks.fetchPublicLogs.mockResolvedValue({ items: [ { @@ -52,7 +84,7 @@ describe("ApiKeyUsagePage", () => { cached_tokens: 0, total_tokens: 3, cost: 0, - has_content: false, + has_content: true, }, ], total: 1, @@ -102,7 +134,9 @@ describe("ApiKeyUsagePage", () => { expect(screen.getByText("快捷导入")).toBeInTheDocument(); expect(screen.getByText("张军宝")).toBeInTheDocument(); expect(screen.queryByText("使用统计")).not.toBeInTheDocument(); - expect(screen.queryByRole("columnheader", { name: /KEY 名称|Key name/i })).not.toBeInTheDocument(); + expect( + screen.queryByRole("columnheader", { name: /KEY 名称|Key name/i }), + ).not.toBeInTheDocument(); await waitFor(() => { expect(mocks.fetchPublicUsageSummary).toHaveBeenCalledWith( expect.objectContaining({ apiKey: "sk-demo" }), @@ -140,4 +174,18 @@ describe("ApiKeyUsagePage", () => { expect(await screen.findByText("张军宝")).toBeInTheDocument(); expect(screen.queryByTestId("apikey-usage-empty")).not.toBeInTheDocument(); }); + + test("loads key-scoped request content from the logs table", async () => { + renderPage(); + + await userEvent.type(screen.getByPlaceholderText(/输入 API 密钥|Enter API Key/i), "sk-demo"); + await userEvent.click(screen.getByTestId("apikey-usage-submit")); + + await userEvent.click(await screen.findByTitle(/查看输入|View input/i)); + await userEvent.click(await screen.findByTestId("load-log-content")); + + expect(mocks.fetchPublicLogContent).toHaveBeenCalledWith( + expect.objectContaining({ id: 1, apiKey: "sk-demo", part: "input" }), + ); + }); });