From da416036e4ab13f415b62a40287e9ac6db380769 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Sat, 11 Apr 2026 15:38:00 +0800 Subject: [PATCH] refactor(usage): compact request log table with merged cache/multiplier columns and centered layout --- src/components/usage/RequestLogTable.tsx | 587 +++++++---------------- 1 file changed, 180 insertions(+), 407 deletions(-) diff --git a/src/components/usage/RequestLogTable.tsx b/src/components/usage/RequestLogTable.tsx index fe485ec20..2767d6f17 100644 --- a/src/components/usage/RequestLogTable.tsx +++ b/src/components/usage/RequestLogTable.tsx @@ -17,10 +17,9 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { useRequestLogs, usageKeys } from "@/lib/query/usage"; -import { useQueryClient } from "@tanstack/react-query"; -import type { LogFilters } from "@/types/usage"; -import { ChevronLeft, ChevronRight, RefreshCw, Search, X } from "lucide-react"; +import { useRequestLogs } from "@/lib/query/usage"; +import type { LogFilters, UsageRangeSelection } from "@/types/usage"; +import { Calendar, ChevronLeft, ChevronRight, Search, X } from "lucide-react"; import { fmtInt, fmtUsd, @@ -29,38 +28,25 @@ import { } from "./format"; interface RequestLogTableProps { + range: UsageRangeSelection; + rangeLabel: string; appType?: string; refreshIntervalMs: number; } -const ONE_DAY_SECONDS = 24 * 60 * 60; -const MAX_FIXED_RANGE_SECONDS = 30 * ONE_DAY_SECONDS; - -type TimeMode = "rolling" | "fixed"; - export function RequestLogTable({ + range, + rangeLabel, appType: dashboardAppType, refreshIntervalMs, }: RequestLogTableProps) { const { t, i18n } = useTranslation(); - const queryClient = useQueryClient(); - - const getRollingRange = () => { - const now = Math.floor(Date.now() / 1000); - const oneDayAgo = now - ONE_DAY_SECONDS; - return { startDate: oneDayAgo, endDate: now }; - }; - - const [appliedTimeMode, setAppliedTimeMode] = useState("rolling"); - const [draftTimeMode, setDraftTimeMode] = useState("rolling"); const [appliedFilters, setAppliedFilters] = useState({}); const [draftFilters, setDraftFilters] = useState({}); const [page, setPage] = useState(0); const pageSize = 20; - const [validationError, setValidationError] = useState(null); - // When dashboard-level app filter is active (not "all"), override the local appType filter const dashboardAppTypeActive = dashboardAppType && dashboardAppType !== "all"; const effectiveFilters: LogFilters = dashboardAppTypeActive ? { ...appliedFilters, appType: dashboardAppType } @@ -68,8 +54,7 @@ export function RequestLogTable({ const { data: result, isLoading } = useRequestLogs({ filters: effectiveFilters, - timeMode: appliedTimeMode, - rollingWindowSeconds: ONE_DAY_SECONDS, + range, page, pageSize, options: { @@ -82,122 +67,43 @@ export function RequestLogTable({ const totalPages = Math.ceil(total / pageSize); const handleSearch = () => { - setValidationError(null); - - if (draftTimeMode === "fixed") { - const start = draftFilters.startDate; - const end = draftFilters.endDate; - - if (typeof start !== "number" || typeof end !== "number") { - setValidationError( - t("usage.invalidTimeRange", "请选择完整的开始/结束时间"), - ); - return; - } - - if (start > end) { - setValidationError( - t("usage.invalidTimeRangeOrder", "开始时间不能晚于结束时间"), - ); - return; - } - - if (end - start > MAX_FIXED_RANGE_SECONDS) { - setValidationError( - t("usage.timeRangeTooLarge", "时间范围过大,请缩小范围"), - ); - return; - } - } - - setAppliedTimeMode(draftTimeMode); - setAppliedFilters((prev) => { - const next = { ...prev, ...draftFilters }; - if (draftTimeMode === "rolling") { - delete next.startDate; - delete next.endDate; - } - return next; - }); + setAppliedFilters(draftFilters); setPage(0); }; const handleReset = () => { - setValidationError(null); - setAppliedTimeMode("rolling"); - setDraftTimeMode("rolling"); setDraftFilters({}); setAppliedFilters({}); setPage(0); }; - const handleRefresh = () => { - const key = { - timeMode: appliedTimeMode, - rollingWindowSeconds: - appliedTimeMode === "rolling" ? ONE_DAY_SECONDS : undefined, - appType: appliedFilters.appType, - providerName: appliedFilters.providerName, - model: appliedFilters.model, - statusCode: appliedFilters.statusCode, - startDate: - appliedTimeMode === "fixed" ? appliedFilters.startDate : undefined, - endDate: appliedTimeMode === "fixed" ? appliedFilters.endDate : undefined, - }; - - queryClient.invalidateQueries({ - queryKey: usageKeys.logs(key, page, pageSize), - }); - }; - - // 将 Unix 时间戳转换为本地时间的 datetime-local 格式 - const timestampToLocalDatetime = (timestamp: number): string => { - const date = new Date(timestamp * 1000); - const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, "0"); - const day = String(date.getDate()).padStart(2, "0"); - const hours = String(date.getHours()).padStart(2, "0"); - const minutes = String(date.getMinutes()).padStart(2, "0"); - return `${year}-${month}-${day}T${hours}:${minutes}`; - }; - - // 将 datetime-local 格式转换为 Unix 时间戳 - const localDatetimeToTimestamp = (datetime: string): number | undefined => { - if (!datetime) return undefined; - // 验证格式是否完整 (YYYY-MM-DDTHH:mm) - if (datetime.length < 16) return undefined; - const timestamp = new Date(datetime).getTime(); - // 验证是否为有效日期 - if (isNaN(timestamp)) return undefined; - return Math.floor(timestamp / 1000); - }; - const language = i18n.resolvedLanguage || i18n.language || "en"; const locale = getLocaleFromLanguage(language); - const rollingRangeForDisplay = - draftTimeMode === "rolling" ? getRollingRange() : null; - return (
- {/* 筛选栏 */} -
-
+
+
+ {/* App type */} + {/* App type */} + {/* Status code */} -
-
- - - setDraftFilters({ - ...draftFilters, - providerName: e.target.value || undefined, - }) - } - /> -
+ {/* Provider search */} +
+ + + setDraftFilters({ + ...draftFilters, + providerName: e.target.value || undefined, + }) + } + onKeyDown={(e) => { + if (e.key === "Enter") handleSearch(); + }} + /> +
+ + {/* Model search */} +
setDraftFilters({ @@ -260,89 +176,40 @@ export function RequestLogTable({ model: e.target.value || undefined, }) } - /> -
-
- -
-
- {t("usage.timeRange")}: - { - const timestamp = localDatetimeToTimestamp(e.target.value); - setDraftTimeMode("fixed"); - setDraftFilters({ - ...draftFilters, - startDate: timestamp, - }); - }} - /> - - - { - const timestamp = localDatetimeToTimestamp(e.target.value); - setDraftTimeMode("fixed"); - setDraftFilters({ - ...draftFilters, - endDate: timestamp, - }); + onKeyDown={(e) => { + if (e.key === "Enter") handleSearch(); }} />
-
- - - + {/* Time range badge */} +
+ + + {rangeLabel} +
-
- {validationError && ( -
{validationError}
- )} + {/* Search & Reset (icon-only) */} + + +
{isLoading ? ( @@ -353,40 +220,31 @@ export function RequestLogTable({ - + {t("usage.time")} - + {t("usage.provider")} - + {t("usage.billingModel")} - + {t("usage.inputTokens")} - + {t("usage.outputTokens")} - - {t("usage.cacheReadTokens")} - - - {t("usage.cacheCreationTokens")} - - - {t("usage.multiplier")} - - + {t("usage.totalCost")} - + {t("usage.timingInfo")} - + {t("usage.status")} - + {t("usage.source", { defaultValue: "Source" })} @@ -395,7 +253,7 @@ export function RequestLogTable({ {logs.length === 0 ? ( {t("usage.noData")} @@ -404,140 +262,96 @@ export function RequestLogTable({ ) : ( logs.map((log) => ( - - {new Date(log.createdAt * 1000).toLocaleString(locale)} + + {new Date(log.createdAt * 1000).toLocaleString(locale, { + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + })} - + {log.providerName || t("usage.unknownProvider")} - +
- {log.model} + {log.requestModel && + log.requestModel !== log.model ? ( + + {log.requestModel} + + {" → "} + {log.model} + + + ) : ( + log.model + )}
- {log.requestModel && log.requestModel !== log.model && ( -
- ← {log.requestModel} + + +
+ {fmtInt(log.inputTokens, locale)} +
+ {(log.cacheReadTokens > 0 || + log.cacheCreationTokens > 0) && ( +
+ {[ + log.cacheReadTokens > 0 && + `R${fmtInt(log.cacheReadTokens, locale)}`, + log.cacheCreationTokens > 0 && + `W${fmtInt(log.cacheCreationTokens, locale)}`, + ] + .filter(Boolean) + .join("·")}
)}
- - {fmtInt(log.inputTokens, locale)} - - + {fmtInt(log.outputTokens, locale)} - - {fmtInt(log.cacheReadTokens, locale)} + +
+ {fmtUsd(log.totalCostUsd, 4)} +
+ {parseFiniteNumber(log.costMultiplier) != null && + parseFiniteNumber(log.costMultiplier) !== 1 && ( +
+ × + {parseFiniteNumber(log.costMultiplier)?.toFixed( + 2, + )} +
+ )}
- - {fmtInt(log.cacheCreationTokens, locale)} - - - {(parseFiniteNumber(log.costMultiplier) ?? 1) !== 1 ? ( - - ×{log.costMultiplier} + + {(log.latencyMs / 1000).toFixed(1)}s + {log.firstTokenMs != null && ( + + /{(log.firstTokenMs / 1000).toFixed(1)}s - ) : ( - ×1 )} - - {fmtUsd(log.totalCostUsd, 6)} - - -
- {(() => { - const durationMs = - typeof log.durationMs === "number" - ? log.durationMs - : log.latencyMs; - const durationSec = durationMs / 1000; - const durationColor = Number.isFinite(durationSec) - ? durationSec <= 5 - ? "bg-green-100 text-green-800" - : durationSec <= 120 - ? "bg-orange-100 text-orange-800" - : "bg-red-200 text-red-900" - : "bg-gray-100 text-gray-700"; - return ( - - {Number.isFinite(durationSec) - ? `${Math.round(durationSec)}s` - : "--"} - - ); - })()} - {log.isStreaming && - log.firstTokenMs != null && - (() => { - const firstSec = log.firstTokenMs / 1000; - const firstColor = Number.isFinite(firstSec) - ? firstSec <= 5 - ? "bg-green-100 text-green-800" - : firstSec <= 120 - ? "bg-orange-100 text-orange-800" - : "bg-red-200 text-red-900" - : "bg-gray-100 text-gray-700"; - return ( - - {Number.isFinite(firstSec) - ? `${firstSec.toFixed(1)}s` - : "--"} - - ); - })()} - - {log.isStreaming - ? t("usage.stream") - : t("usage.nonStream")} - -
-
- + = 200 && log.statusCode < 300 - ? "bg-green-100 text-green-800" - : "bg-red-100 text-red-800" - }`} + ? "text-green-600" + : "text-red-600" + } > {log.statusCode} - - {log.dataSource && log.dataSource !== "proxy" ? ( - - {t(`usage.dataSource.${log.dataSource}`, { - defaultValue: log.dataSource, - })} - - ) : ( - - {t("usage.dataSource.proxy", { - defaultValue: "Proxy", - })} - - )} + + {log.dataSource || "proxy"} )) @@ -546,71 +360,30 @@ export function RequestLogTable({
- {/* 分页控件 */} - {total > 0 && ( -
- - {t("usage.totalRecords", { total })} +
+ {t("usage.totalRecords", { total })} +
+ + + {page + 1} / {Math.max(totalPages, 1)} -
- - {/* 页码按钮 */} - {(() => { - const pages: (number | string)[] = []; - if (totalPages <= 7) { - for (let i = 0; i < totalPages; i++) pages.push(i); - } else { - pages.push(0); - if (page > 2) pages.push("..."); - for ( - let i = Math.max(1, page - 1); - i <= Math.min(totalPages - 2, page + 1); - i++ - ) { - pages.push(i); - } - if (page < totalPages - 3) pages.push("..."); - pages.push(totalPages - 1); - } - return pages.map((p, idx) => - typeof p === "string" ? ( - - ... - - ) : ( - - ), - ); - })()} - -
+
- )} +
)}