feat(ui): i18n for view_logs constants and filter options

This commit is contained in:
吹雪 2026-06-10 17:24:46 +09:00
parent 6bb974f1e8
commit 95af75657c
6 changed files with 147 additions and 55 deletions

View file

@ -1,9 +1,9 @@
import moment from "moment";
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import { SyncOutlined } from "@ant-design/icons";
import { Button, Switch } from "antd";
import { useTranslation } from "react-i18next";
import { QUICK_SELECT_OPTIONS } from "./constants";
import { getQuickSelectOptions } from "./constants";
import { getTimeRangeDisplay } from "./logs_utils";
import type { PaginatedResponse } from "./log_filter_logic";
@ -51,6 +51,7 @@ export function LogsTableToolbar({
filteredLogs,
}: LogsTableToolbarProps) {
const { t } = useTranslation();
const quickSelectOptions = useMemo(() => getQuickSelectOptions(t), [t]);
const [quickSelectOpen, setQuickSelectOpen] = useState(false);
const quickSelectRef = useRef<HTMLDivElement>(null);
@ -64,7 +65,7 @@ export function LogsTableToolbar({
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const selectedOption = QUICK_SELECT_OPTIONS.find(
const selectedOption = quickSelectOptions.find(
(option) => option.value === selectedTimeInterval.value && option.unit === selectedTimeInterval.unit,
);
const displayLabel = isCustomDate ? getTimeRangeDisplay(isCustomDate, startTime, endTime) : selectedOption?.label;
@ -117,7 +118,7 @@ export function LogsTableToolbar({
{quickSelectOpen && (
<div className="absolute left-0 mt-2 w-64 bg-white rounded-lg shadow-lg border p-2 z-50">
<div className="space-y-1">
{QUICK_SELECT_OPTIONS.map((option) => (
{quickSelectOptions.map((option) => (
<button
key={option.label}
className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-50 rounded-md ${displayLabel === option.label ? "bg-blue-50 text-blue-600" : ""}`}

View file

@ -1,15 +1,17 @@
export const ERROR_CODE_OPTIONS: { label: string; value: string }[] = [
{ label: "400 - Bad Request", value: "400" },
{ label: "401 - Invalid Authentication", value: "401" },
{ label: "403 - Permission Denied", value: "403" },
{ label: "404 - Not Found", value: "404" },
{ label: "408 - Request Timeout", value: "408" },
{ label: "422 - Unprocessable Entity", value: "422" },
{ label: "429 - Rate Limited", value: "429" },
{ label: "500 - Internal Server Error", value: "500" },
{ label: "502 - Bad Gateway", value: "502" },
{ label: "503 - Service Unavailable", value: "503" },
{ label: "529 - Overloaded", value: "529" },
import type { TFunction } from "i18next";
export const getErrorCodeOptions = (t: TFunction): { label: string; value: string }[] => [
{ label: t("viewLogs.constants.errorCode400"), value: "400" },
{ label: t("viewLogs.constants.errorCode401"), value: "401" },
{ label: t("viewLogs.constants.errorCode403"), value: "403" },
{ label: t("viewLogs.constants.errorCode404"), value: "404" },
{ label: t("viewLogs.constants.errorCode408"), value: "408" },
{ label: t("viewLogs.constants.errorCode422"), value: "422" },
{ label: t("viewLogs.constants.errorCode429"), value: "429" },
{ label: t("viewLogs.constants.errorCode500"), value: "500" },
{ label: t("viewLogs.constants.errorCode502"), value: "502" },
{ label: t("viewLogs.constants.errorCode503"), value: "503" },
{ label: t("viewLogs.constants.errorCode529"), value: "529" },
];
/** Call types that represent MCP tool invocations (shared across columns, index, drawer). */
@ -18,11 +20,11 @@ export const MCP_CALL_TYPES = ["call_mcp_tool", "list_mcp_tools"];
/** Call types that represent agent/A2A requests (e.g. asend_message). */
export const AGENT_CALL_TYPES = ["asend_message"];
export const QUICK_SELECT_OPTIONS: { label: string; value: number; unit: string }[] = [
{ label: "Last Minute", value: 1, unit: "minutes" },
{ label: "Last 15 Minutes", value: 15, unit: "minutes" },
{ label: "Last Hour", value: 1, unit: "hours" },
{ label: "Last 4 Hours", value: 4, unit: "hours" },
{ label: "Last 24 Hours", value: 24, unit: "hours" },
{ label: "Last 7 Days", value: 7, unit: "days" },
export const getQuickSelectOptions = (t: TFunction): { label: string; value: number; unit: string }[] => [
{ label: t("viewLogs.constants.quickSelectLastMinute"), value: 1, unit: "minutes" },
{ label: t("viewLogs.constants.quickSelectLast15Minutes"), value: 15, unit: "minutes" },
{ label: t("viewLogs.constants.quickSelectLastHour"), value: 1, unit: "hours" },
{ label: t("viewLogs.constants.quickSelectLast4Hours"), value: 4, unit: "hours" },
{ label: t("viewLogs.constants.quickSelectLast24Hours"), value: 24, unit: "hours" },
{ label: t("viewLogs.constants.quickSelectLast7Days"), value: 7, unit: "days" },
];

View file

@ -1,35 +1,36 @@
import type { TFunction } from "i18next";
import FilterTeamDropdown from "../common_components/FilterTeamDropdown";
import { PaginatedKeyAliasSelect } from "../KeyAliasSelect/PaginatedKeyAliasSelect/PaginatedKeyAliasSelect";
import { PaginatedModelSelect } from "../ModelSelect/PaginatedModelSelect/PaginatedModelSelect";
import { FilterOption } from "../molecules/filter";
import { allEndUsersCall } from "../networking";
import { ERROR_CODE_OPTIONS } from "./constants";
import { getErrorCodeOptions } from "./constants";
import { FILTER_KEYS } from "./log_filter_logic";
export function getLogFilterOptions(accessToken: string): FilterOption[] {
export function getLogFilterOptions(accessToken: string, t: TFunction): FilterOption[] {
return [
{
name: "Team ID",
label: "Team ID",
name: FILTER_KEYS.TEAM_ID,
label: t("viewLogs.filterOptions.teamIdLabel"),
customComponent: FilterTeamDropdown,
},
{
name: "Status",
label: "Status",
name: FILTER_KEYS.STATUS,
label: t("viewLogs.filterOptions.statusLabel"),
isSearchable: false,
options: [
{ label: "Success", value: "success" },
{ label: "Failure", value: "failure" },
{ label: t("viewLogs.filterOptions.statusSuccess"), value: "success" },
{ label: t("viewLogs.filterOptions.statusFailure"), value: "failure" },
],
},
{
name: "Key Alias",
label: "Key Alias",
name: FILTER_KEYS.KEY_ALIAS,
label: t("viewLogs.filterOptions.keyAliasLabel"),
customComponent: PaginatedKeyAliasSelect,
},
{
name: "End User",
label: "End User",
name: FILTER_KEYS.END_USER,
label: t("viewLogs.filterOptions.endUserLabel"),
isSearchable: true,
searchFn: async (searchText: string) => {
const data = await allEndUsersCall(accessToken);
@ -39,38 +40,42 @@ export function getLogFilterOptions(accessToken: string): FilterOption[] {
},
},
{
name: "Error Code",
label: "Error Code",
name: FILTER_KEYS.ERROR_CODE,
label: t("viewLogs.filterOptions.errorCodeLabel"),
isSearchable: true,
searchFn: async (searchText: string) => {
if (!searchText) return ERROR_CODE_OPTIONS;
const errorCodeOptions = getErrorCodeOptions(t);
if (!searchText) return errorCodeOptions;
const lower = searchText.toLowerCase();
const filtered = ERROR_CODE_OPTIONS.filter((opt) => opt.label.toLowerCase().includes(lower));
const isExactValue = ERROR_CODE_OPTIONS.some((opt) => opt.value === searchText.trim());
const filtered = errorCodeOptions.filter((opt) => opt.label.toLowerCase().includes(lower));
const isExactValue = errorCodeOptions.some((opt) => opt.value === searchText.trim());
if (!isExactValue && searchText.trim()) {
filtered.push({ label: `Use custom code: ${searchText.trim()}`, value: searchText.trim() });
filtered.push({
label: t("viewLogs.filterOptions.customCodeLabel", { code: searchText.trim() }),
value: searchText.trim(),
});
}
return filtered;
},
},
{
name: "Error Message",
label: "Error Message",
name: FILTER_KEYS.ERROR_MESSAGE,
label: t("viewLogs.filterOptions.errorMessageLabel"),
isSearchable: false,
},
{
name: "Key Hash",
label: "Key Hash",
name: FILTER_KEYS.KEY_HASH,
label: t("viewLogs.filterOptions.keyHashLabel"),
isSearchable: false,
},
{
name: "Model",
label: "Model",
name: FILTER_KEYS.MODEL,
label: t("viewLogs.filterOptions.modelLabel"),
customComponent: PaginatedModelSelect,
},
{
name: FILTER_KEYS.PUBLIC_MODEL_OR_SEARCH_TOOL,
label: "Public model / search tool",
label: t("viewLogs.filterOptions.publicModelLabel"),
isSearchable: false,
},
];

View file

@ -1,5 +1,6 @@
import moment from "moment";
import { useCallback, useDeferredValue, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Tab, TabGroup, TabList, TabPanel, TabPanels } from "@tremor/react";
import { internalUserRoles } from "../../utils/roles";
import DeletedKeysPage from "../DeletedKeysPage/DeletedKeysPage";
@ -27,6 +28,7 @@ interface SpendLogsTableProps {
}
export default function SpendLogsTable({ accessToken, token, userRole, userID, premiumUser }: SpendLogsTableProps) {
const { t } = useTranslation();
const [searchTerm, setSearchTerm] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const [pageSize] = useState(50);
@ -237,15 +239,15 @@ export default function SpendLogsTable({ accessToken, token, userRole, userID, p
<div className="w-full max-w-screen p-6 overflow-x-hidden box-border">
<TabGroup defaultIndex={0} onIndexChange={(index) => setActiveTab(index === 0 ? "request logs" : "audit logs")}>
<TabList>
<Tab>Request Logs</Tab>
<Tab>Audit Logs</Tab>
<Tab>Deleted Keys</Tab>
<Tab>Deleted Teams</Tab>
<Tab>{t("viewLogs.index.tabRequestLogs")}</Tab>
<Tab>{t("viewLogs.index.tabAuditLogs")}</Tab>
<Tab>{t("viewLogs.index.tabDeletedKeys")}</Tab>
<Tab>{t("viewLogs.index.tabDeletedTeams")}</Tab>
</TabList>
<TabPanels>
<TabPanel>
<div className="flex items-center justify-between mb-4">
<h1 className="text-xl font-semibold">Request Logs</h1>
<h1 className="text-xl font-semibold">{t("viewLogs.index.requestLogsTitle")}</h1>
</div>
{selectedKeyInfo && selectedKeyIdInfoView && selectedKeyInfo.api_key === selectedKeyIdInfoView ? (
<KeyInfoView
@ -253,12 +255,12 @@ export default function SpendLogsTable({ accessToken, token, userRole, userID, p
keyData={selectedKeyInfo}
teams={allTeams ?? []}
onClose={() => setSelectedKeyIdInfoView(null)}
backButtonText="Back to Logs"
backButtonText={t("viewLogs.index.backToLogs")}
/>
) : (
<>
<FilterComponent
options={getLogFilterOptions(accessToken)}
options={getLogFilterOptions(accessToken, t)}
onApplyFilters={handleFilterChange}
onResetFilters={handleFilterReset}
/>

View file

@ -2265,6 +2265,47 @@
"tokenFlow": {
"breakdown": "{{total}} ({{prompt}} prompt tokens + {{completion}} completion tokens)"
},
"constants": {
"errorCode400": "400 - Bad Request",
"errorCode401": "401 - Invalid Authentication",
"errorCode403": "403 - Permission Denied",
"errorCode404": "404 - Not Found",
"errorCode408": "408 - Request Timeout",
"errorCode422": "422 - Unprocessable Entity",
"errorCode429": "429 - Rate Limited",
"errorCode500": "500 - Internal Server Error",
"errorCode502": "502 - Bad Gateway",
"errorCode503": "503 - Service Unavailable",
"errorCode529": "529 - Overloaded",
"quickSelectLastMinute": "Last Minute",
"quickSelectLast15Minutes": "Last 15 Minutes",
"quickSelectLastHour": "Last Hour",
"quickSelectLast4Hours": "Last 4 Hours",
"quickSelectLast24Hours": "Last 24 Hours",
"quickSelectLast7Days": "Last 7 Days"
},
"filterOptions": {
"teamIdLabel": "Team ID",
"statusLabel": "Status",
"statusSuccess": "Success",
"statusFailure": "Failure",
"keyAliasLabel": "Key Alias",
"endUserLabel": "End User",
"errorCodeLabel": "Error Code",
"customCodeLabel": "Use custom code: {{code}}",
"errorMessageLabel": "Error Message",
"keyHashLabel": "Key Hash",
"modelLabel": "Model",
"publicModelLabel": "Public model / search tool"
},
"index": {
"tabRequestLogs": "Request Logs",
"tabAuditLogs": "Audit Logs",
"tabDeletedKeys": "Deleted Keys",
"tabDeletedTeams": "Deleted Teams",
"requestLogsTitle": "Request Logs",
"backToLogs": "Back to Logs"
},
"logsTableToolbar": {
"searchPlaceholder": "Search by Request ID",
"customRange": "Custom Range",

View file

@ -2265,6 +2265,47 @@
"tokenFlow": {
"breakdown": "{{total}}({{prompt}} 提示词 Token + {{completion}} 补全 Token)"
},
"constants": {
"errorCode400": "400 - 错误请求",
"errorCode401": "401 - 身份验证无效",
"errorCode403": "403 - 权限被拒绝",
"errorCode404": "404 - 未找到",
"errorCode408": "408 - 请求超时",
"errorCode422": "422 - 无法处理的实体",
"errorCode429": "429 - 速率受限",
"errorCode500": "500 - 内部服务器错误",
"errorCode502": "502 - 错误网关",
"errorCode503": "503 - 服务不可用",
"errorCode529": "529 - 过载",
"quickSelectLastMinute": "最近 1 分钟",
"quickSelectLast15Minutes": "最近 15 分钟",
"quickSelectLastHour": "最近 1 小时",
"quickSelectLast4Hours": "最近 4 小时",
"quickSelectLast24Hours": "最近 24 小时",
"quickSelectLast7Days": "最近 7 天"
},
"filterOptions": {
"teamIdLabel": "团队 ID",
"statusLabel": "状态",
"statusSuccess": "成功",
"statusFailure": "失败",
"keyAliasLabel": "密钥别名",
"endUserLabel": "终端用户",
"errorCodeLabel": "错误码",
"customCodeLabel": "使用自定义错误码:{{code}}",
"errorMessageLabel": "错误消息",
"keyHashLabel": "密钥哈希",
"modelLabel": "模型",
"publicModelLabel": "公共模型 / 搜索工具"
},
"index": {
"tabRequestLogs": "请求日志",
"tabAuditLogs": "审计日志",
"tabDeletedKeys": "已删除密钥",
"tabDeletedTeams": "已删除团队",
"requestLogsTitle": "请求日志",
"backToLogs": "返回日志"
},
"logsTableToolbar": {
"searchPlaceholder": "按请求 ID 搜索",
"customRange": "自定义范围",