feat(ui): i18n for view_logs batch 4

This commit is contained in:
吹雪 2026-06-10 17:18:52 +09:00
parent 888cc88826
commit 6bb974f1e8
10 changed files with 390 additions and 124 deletions

View file

@ -2,6 +2,7 @@ import moment from "moment";
import { useEffect, 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 { getTimeRangeDisplay } from "./logs_utils";
import type { PaginatedResponse } from "./log_filter_logic";
@ -49,6 +50,7 @@ export function LogsTableToolbar({
onRefetch,
filteredLogs,
}: LogsTableToolbarProps) {
const { t } = useTranslation();
const [quickSelectOpen, setQuickSelectOpen] = useState(false);
const quickSelectRef = useRef<HTMLDivElement>(null);
@ -75,7 +77,7 @@ export function LogsTableToolbar({
<div className="relative w-64 min-w-0 flex-shrink-0">
<input
type="text"
placeholder="Search by Request ID"
placeholder={t("viewLogs.logsTableToolbar.searchPlaceholder")}
className="w-full px-3 py-2 pl-8 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
value={searchTerm}
onChange={(e) => onSearchChange(e.target.value)}
@ -140,7 +142,7 @@ export function LogsTableToolbar({
className={`w-full px-3 py-2 text-left text-sm hover:bg-gray-50 rounded-md ${isCustomDate ? "bg-blue-50 text-blue-600" : ""}`}
onClick={() => onIsCustomDateChange(!isCustomDate)}
>
Custom Range
{t("viewLogs.logsTableToolbar.customRange")}
</button>
</div>
</div>
@ -148,7 +150,7 @@ export function LogsTableToolbar({
</div>
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-gray-900">Live Tail</span>
<span className="text-sm font-medium text-gray-900">{t("viewLogs.logsTableToolbar.liveTail")}</span>
<Switch checked={isLiveTail} defaultChecked={true} onChange={onIsLiveTailChange} />
</div>
@ -157,9 +159,9 @@ export function LogsTableToolbar({
icon={<SyncOutlined spin={isButtonLoading} />}
onClick={onRefetch}
disabled={isButtonLoading}
title="Fetch data"
title={t("viewLogs.logsTableToolbar.fetchData")}
>
{isButtonLoading ? "Fetching" : "Fetch"}
{isButtonLoading ? t("viewLogs.logsTableToolbar.fetching") : t("viewLogs.logsTableToolbar.fetch")}
</Button>
</div>
@ -176,7 +178,7 @@ export function LogsTableToolbar({
className="px-3 py-2 border rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<span className="text-gray-500">to</span>
<span className="text-gray-500">{t("viewLogs.logsTableToolbar.to")}</span>
<div>
<input
type="datetime-local"
@ -194,28 +196,32 @@ export function LogsTableToolbar({
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-700 whitespace-nowrap">
Showing {isLoading ? "..." : filteredLogs ? (currentPage - 1) * pageSize + 1 : 0} -{" "}
{isLoading ? "..." : filteredLogs ? Math.min(currentPage * pageSize, filteredLogs.total) : 0} of{" "}
{isLoading ? "..." : filteredLogs ? filteredLogs.total : 0} results
{t("viewLogs.logsTableToolbar.showing", {
from: isLoading ? "..." : filteredLogs ? (currentPage - 1) * pageSize + 1 : 0,
to: isLoading ? "..." : filteredLogs ? Math.min(currentPage * pageSize, filteredLogs.total) : 0,
total: isLoading ? "..." : filteredLogs ? filteredLogs.total : 0,
})}
</span>
<div className="flex items-center space-x-2">
<span className="text-sm text-gray-700 min-w-[90px]">
Page {isLoading ? "..." : currentPage} of{" "}
{isLoading ? "..." : filteredLogs ? filteredLogs.total_pages : 1}
{t("viewLogs.logsTableToolbar.page", {
current: isLoading ? "..." : currentPage,
total: isLoading ? "..." : filteredLogs ? filteredLogs.total_pages : 1,
})}
</span>
<button
onClick={() => onCurrentPageChange((p: number) => Math.max(1, p - 1))}
disabled={isLoading || currentPage === 1}
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
{t("common.previous")}
</button>
<button
onClick={() => onCurrentPageChange((p: number) => Math.min(filteredLogs.total_pages || 1, p + 1))}
disabled={isLoading || currentPage === (filteredLogs.total_pages || 1)}
className="px-3 py-1 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
{t("common.next")}
</button>
</div>
</div>
@ -224,10 +230,10 @@ export function LogsTableToolbar({
{isLiveTail && currentPage === 1 && (
<div className="mb-4 px-4 py-2 bg-green-50 border border-green-200 rounded-md flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-green-700">Auto-refreshing every 15 seconds</span>
<span className="text-sm text-green-700">{t("viewLogs.logsTableToolbar.autoRefreshing")}</span>
</div>
<button onClick={() => onIsLiveTailChange(false)} className="text-sm text-green-600 hover:text-green-800">
Stop
{t("viewLogs.logsTableToolbar.stop")}
</button>
</div>
)}

View file

@ -2,6 +2,7 @@ import { LogEntry } from "./columns";
import NotificationsManager from "../molecules/notifications_manager";
import { JsonView, defaultStyles } from "react-json-view-lite";
import "react-json-view-lite/dist/index.css";
import { useTranslation } from "react-i18next";
interface RequestResponsePanelProps {
row: {
@ -24,6 +25,7 @@ export function RequestResponsePanel({
getRawRequest,
formattedResponse,
}: RequestResponsePanelProps) {
const { t } = useTranslation();
const copyToClipboard = async (text: string) => {
try {
// Try modern clipboard API first
@ -57,18 +59,18 @@ export function RequestResponsePanel({
const handleCopyRequest = async () => {
const success = await copyToClipboard(JSON.stringify(getRawRequest(), null, 2));
if (success) {
NotificationsManager.success("Request copied to clipboard");
NotificationsManager.success(t("viewLogs.requestResponsePanel.requestCopied"));
} else {
NotificationsManager.fromBackend("Failed to copy request");
NotificationsManager.fromBackend(t("viewLogs.requestResponsePanel.failedToCopyRequest"));
}
};
const handleCopyResponse = async () => {
const success = await copyToClipboard(JSON.stringify(formattedResponse(), null, 2));
if (success) {
NotificationsManager.success("Response copied to clipboard");
NotificationsManager.success(t("viewLogs.requestResponsePanel.responseCopied"));
} else {
NotificationsManager.fromBackend("Failed to copy response");
NotificationsManager.fromBackend(t("viewLogs.requestResponsePanel.failedToCopyResponse"));
}
};
@ -77,8 +79,12 @@ export function RequestResponsePanel({
{/* Request Side */}
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden">
<div className="flex justify-between items-center p-4 border-b">
<h3 className="text-lg font-medium">Request</h3>
<button onClick={handleCopyRequest} className="p-1 hover:bg-gray-200 rounded" title="Copy request">
<h3 className="text-lg font-medium">{t("viewLogs.requestResponsePanel.request")}</h3>
<button
onClick={handleCopyRequest}
className="p-1 hover:bg-gray-200 rounded"
title={t("viewLogs.requestResponsePanel.copyRequest")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
@ -106,13 +112,17 @@ export function RequestResponsePanel({
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden">
<div className="flex justify-between items-center p-4 border-b">
<h3 className="text-lg font-medium">
Response
{hasError && <span className="ml-2 text-sm text-red-600">• HTTP code {errorInfo?.error_code || 400}</span>}
{t("viewLogs.requestResponsePanel.response")}
{hasError && (
<span className="ml-2 text-sm text-red-600">
{t("viewLogs.requestResponsePanel.httpErrorCode", { code: errorInfo?.error_code || 400 })}
</span>
)}
</h3>
<button
onClick={handleCopyResponse}
className="p-1 hover:bg-gray-200 rounded"
title="Copy response"
title={t("viewLogs.requestResponsePanel.copyResponse")}
disabled={!hasResponse && !hasError}
>
<svg
@ -137,7 +147,9 @@ export function RequestResponsePanel({
<JsonView data={formattedResponse()} style={defaultStyles} clickToExpandNode />
</div>
) : (
<div className="text-gray-500 text-sm italic text-center py-4">Response data not available</div>
<div className="text-gray-500 text-sm italic text-center py-4">
{t("viewLogs.requestResponsePanel.responseNotAvailable")}
</div>
)}
</div>
</div>

View file

@ -3,6 +3,7 @@
*/
import { Typography, Table } from "antd";
import { useTranslation } from "react-i18next";
import { ParsedTool, ParameterRow } from "./types";
const { Text } = Typography;
@ -12,6 +13,7 @@ interface FormattedToolViewProps {
}
export function FormattedToolView({ tool }: FormattedToolViewProps) {
const { t } = useTranslation();
// Parse parameters for table display
const parameterRows: ParameterRow[] = Object.entries(tool.parameters?.properties || {}).map(
([name, schema]: [string, any]) => ({
@ -25,7 +27,7 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) {
const columns = [
{
title: "Parameter",
title: t("viewLogs.formattedToolView.colParameter"),
dataIndex: "name",
key: "name",
render: (name: string, record: ParameterRow) => (
@ -36,7 +38,7 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) {
),
},
{
title: "Type",
title: t("viewLogs.formattedToolView.colType"),
dataIndex: "type",
key: "type",
render: (type: string) => (
@ -46,7 +48,7 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) {
),
},
{
title: "Description",
title: t("viewLogs.formattedToolView.colDescription"),
dataIndex: "description",
key: "description",
render: (desc: string) => <Text type="secondary">{desc}</Text>,
@ -80,7 +82,7 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) {
marginBottom: 8,
}}
>
Parameters
{t("viewLogs.formattedToolView.parametersLabel")}
</Text>
<Table dataSource={parameterRows} columns={columns} pagination={false} size="small" bordered />
</div>
@ -97,7 +99,7 @@ export function FormattedToolView({ tool }: FormattedToolViewProps) {
marginBottom: 8,
}}
>
Called With
{t("viewLogs.formattedToolView.calledWithLabel")}
</Text>
<div
style={{

View file

@ -4,6 +4,7 @@
import { useState } from "react";
import { Typography, Radio } from "antd";
import { useTranslation } from "react-i18next";
import { ParsedTool } from "./types";
import { FormattedToolView } from "./FormattedToolView";
import { JsonToolView } from "./JsonToolView";
@ -17,6 +18,7 @@ interface ToolExpandedContentProps {
}
export function ToolExpandedContent({ tool }: ToolExpandedContentProps) {
const { t } = useTranslation();
const [viewMode, setViewMode] = useState<ViewMode>("formatted");
return (
@ -30,11 +32,11 @@ export function ToolExpandedContent({ tool }: ToolExpandedContentProps) {
}}
>
<Text type="secondary" style={{ fontSize: 12 }}>
Description
{t("viewLogs.toolExpandedContent.descriptionLabel")}
</Text>
<Radio.Group size="small" value={viewMode} onChange={(e) => setViewMode(e.target.value)}>
<Radio.Button value="formatted">Formatted</Radio.Button>
<Radio.Button value="json">JSON</Radio.Button>
<Radio.Button value="formatted">{t("viewLogs.toolExpandedContent.viewFormatted")}</Radio.Button>
<Radio.Button value="json">{t("viewLogs.toolExpandedContent.viewJson")}</Radio.Button>
</Radio.Group>
</div>

View file

@ -4,6 +4,7 @@
*/
import { Collapse, Typography } from "antd";
import { useTranslation } from "react-i18next";
import { LogEntry } from "../columns";
import { parseToolsFromLog } from "./utils";
import { ToolItem } from "./ToolItem";
@ -15,6 +16,7 @@ interface ToolsSectionProps {
}
export function ToolsSection({ log }: ToolsSectionProps) {
const { t } = useTranslation();
const tools = parseToolsFromLog(log);
// Don't render if no tools
@ -40,9 +42,9 @@ export function ToolsSection({ log }: ToolsSectionProps) {
key: "1",
label: (
<div style={{ display: "flex", alignItems: "center", gap: 12, flexWrap: "wrap" }}>
<h3 className="text-lg font-medium text-gray-900">Tools</h3>
<h3 className="text-lg font-medium text-gray-900">{t("viewLogs.toolsSection.title")}</h3>
<Text type="secondary" style={{ fontSize: 14 }}>
{totalTools} provided, {calledTools} called
{t("viewLogs.toolsSection.summary", { total: totalTools, called: calledTools })}
</Text>
<Text type="secondary" style={{ fontSize: 14 }}>
• {toolNamePreview}

View file

@ -1,5 +1,6 @@
import React, { useState } from "react";
import { Collapse } from "antd";
import { useTranslation } from "react-i18next";
import { getProviderLogoAndName } from "../provider_info_helpers";
interface VectorStoreContent {
@ -31,6 +32,7 @@ interface VectorStoreViewerProps {
}
export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
const { t } = useTranslation();
const [expandedResults, setExpandedResults] = useState<Record<string, boolean>>({});
if (!data || data.length === 0) {
@ -63,7 +65,7 @@ export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
items={[
{
key: "1",
label: <h3 className="text-lg font-medium text-gray-900">Vector Store Requests</h3>,
label: <h3 className="text-lg font-medium text-gray-900">{t("viewLogs.vectorStoreViewer.title")}</h3>,
children: (
<div className="p-4">
{data.map((request, index) => (
@ -72,15 +74,17 @@ export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex">
<span className="font-medium w-1/3">Query:</span>
<span className="font-medium w-1/3">{t("viewLogs.vectorStoreViewer.labelQuery")}</span>
<span className="font-mono">{request.query}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Vector Store ID:</span>
<span className="font-medium w-1/3">
{t("viewLogs.vectorStoreViewer.labelVectorStoreId")}
</span>
<span className="font-mono">{request.vector_store_id}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Provider:</span>
<span className="font-medium w-1/3">{t("viewLogs.vectorStoreViewer.labelProvider")}</span>
<span className="flex items-center">
{(() => {
const { logo, displayName } = getProviderLogoAndName(request.custom_llm_provider);
@ -96,22 +100,22 @@ export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
</div>
<div className="space-y-2">
<div className="flex">
<span className="font-medium w-1/3">Start Time:</span>
<span className="font-medium w-1/3">{t("viewLogs.vectorStoreViewer.labelStartTime")}</span>
<span>{formatTime(request.start_time)}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">End Time:</span>
<span className="font-medium w-1/3">{t("viewLogs.vectorStoreViewer.labelEndTime")}</span>
<span>{formatTime(request.end_time)}</span>
</div>
<div className="flex">
<span className="font-medium w-1/3">Duration:</span>
<span className="font-medium w-1/3">{t("viewLogs.vectorStoreViewer.labelDuration")}</span>
<span>{calculateDuration(request.start_time, request.end_time)}</span>
</div>
</div>
</div>
</div>
<h4 className="font-medium mb-2">Search Results</h4>
<h4 className="font-medium mb-2">{t("viewLogs.vectorStoreViewer.searchResults")}</h4>
<div className="space-y-2">
{request.vector_store_search_response.data.map((result, resultIndex) => {
const isExpanded = expandedResults[`${index}-${resultIndex}`] || false;
@ -131,9 +135,12 @@ export function VectorStoreViewer({ data }: VectorStoreViewerProps) {
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<div className="flex items-center">
<span className="font-medium mr-2">Result {resultIndex + 1}</span>
<span className="font-medium mr-2">
{t("viewLogs.vectorStoreViewer.resultItem", { number: resultIndex + 1 })}
</span>
<span className="text-gray-500 text-sm">
Score: <span className="font-mono">{result.score.toFixed(4)}</span>
{t("viewLogs.vectorStoreViewer.score")}{" "}
<span className="font-mono">{result.score.toFixed(4)}</span>
</span>
</div>
</div>

View file

@ -4,6 +4,7 @@ import { Table, Tag, Input, Select, Button, Pagination, Spin } from "antd";
import { ReloadOutlined, LoadingOutlined } from "@ant-design/icons";
import type { ColumnsType } from "antd/es/table";
import moment from "moment";
import { useTranslation } from "react-i18next";
import { uiAuditLogsCall } from "../networking";
import { AuditLogEntry } from "./columns";
import { AuditLogDrawer } from "./AuditLogDrawer/AuditLogDrawer";
@ -11,6 +12,14 @@ import DefaultProxyAdminTag from "../common_components/DefaultProxyAdminTag";
const { Search } = Input;
const TABLE_NAME_I18N_KEY: Record<string, string> = {
LiteLLM_VerificationToken: "viewLogs.auditLogs.tableKeys",
LiteLLM_TeamTable: "viewLogs.auditLogs.tableTeams",
LiteLLM_UserTable: "viewLogs.auditLogs.tableUsers",
LiteLLM_OrganizationTable: "viewLogs.auditLogs.tableOrganizations",
LiteLLM_ProxyModelTable: "viewLogs.auditLogs.tableModels",
};
interface AuditLogsProps {
accessToken: string | null;
token: string | null;
@ -23,14 +32,6 @@ interface AuditLogsProps {
const asset_logos_folder = "../ui/assets/";
export const auditLogsPreviewImg = `${asset_logos_folder}audit-logs-preview.png`;
const TABLE_NAME_DISPLAY: Record<string, string> = {
LiteLLM_VerificationToken: "Keys",
LiteLLM_TeamTable: "Teams",
LiteLLM_UserTable: "Users",
LiteLLM_OrganizationTable: "Organizations",
LiteLLM_ProxyModelTable: "Models",
};
const ACTION_COLOR: Record<string, string> = {
created: "green",
updated: "blue",
@ -41,6 +42,7 @@ const ACTION_COLOR: Record<string, string> = {
const PAGE_SIZE = 50;
export default function AuditLogs({ userID, userRole, token, accessToken, isActive, premiumUser }: AuditLogsProps) {
const { t } = useTranslation();
const [page, setPage] = useState(1);
// Filter state
@ -90,7 +92,7 @@ export default function AuditLogs({ userID, userRole, token, accessToken, isActi
const columns: ColumnsType<AuditLogEntry> = [
{
title: "Timestamp",
title: t("viewLogs.auditLogs.colTimestamp"),
dataIndex: "updated_at",
key: "updated_at",
width: 200,
@ -101,7 +103,7 @@ export default function AuditLogs({ userID, userRole, token, accessToken, isActi
),
},
{
title: "Action",
title: t("viewLogs.auditLogs.colAction"),
dataIndex: "action",
key: "action",
width: 100,
@ -112,27 +114,30 @@ export default function AuditLogs({ userID, userRole, token, accessToken, isActi
),
},
{
title: "Table",
title: t("viewLogs.auditLogs.colTable"),
dataIndex: "table_name",
key: "table_name",
width: 130,
render: (val: string) => TABLE_NAME_DISPLAY[val] ?? val,
render: (val: string) => {
const key = TABLE_NAME_I18N_KEY[val];
return key ? t(key) : val;
},
},
{
title: "Object ID",
title: t("viewLogs.auditLogs.colObjectId"),
dataIndex: "object_id",
key: "object_id",
render: (val: string) => <span className="font-mono text-xs">{val}</span>,
},
{
title: "Changed By",
title: t("viewLogs.auditLogs.colChangedBy"),
dataIndex: "changed_by",
key: "changed_by",
width: 200,
render: (val: string) => <DefaultProxyAdminTag userId={val} />,
},
{
title: "API Key (Hash)",
title: t("viewLogs.auditLogs.colApiKeyHash"),
dataIndex: "changed_by_api_key",
key: "changed_by_api_key",
width: 140,
@ -143,16 +148,14 @@ export default function AuditLogs({ userID, userRole, token, accessToken, isActi
if (!premiumUser) {
return (
<div style={{ textAlign: "center", marginTop: "20px" }}>
<h1 style={{ display: "block", marginBottom: "10px" }}>✨ Enterprise Feature.</h1>
<p style={{ display: "block", marginBottom: "10px" }}>
This is a LiteLLM Enterprise feature, and requires a valid key to use.
</p>
<h1 style={{ display: "block", marginBottom: "10px" }}>✨ {t("viewLogs.auditLogs.enterpriseTitle")}</h1>
<p style={{ display: "block", marginBottom: "10px" }}>{t("viewLogs.auditLogs.enterpriseDesc")}</p>
<p style={{ display: "block", marginBottom: "20px", fontStyle: "italic" }}>
Here&apos;s a preview of what Audit Logs offer:
{t("viewLogs.auditLogs.previewText")}
</p>
<img
src={auditLogsPreviewImg}
alt="Audit Logs Preview"
alt={t("viewLogs.auditLogs.previewAlt")}
style={{
maxWidth: "100%",
maxHeight: "700px",
@ -177,13 +180,13 @@ export default function AuditLogs({ userID, userRole, token, accessToken, isActi
{/* Header */}
<div className="border-b px-6 py-4">
<div className="flex items-center justify-between mb-4">
<h1 className="text-xl font-semibold">Audit Logs</h1>
<h1 className="text-xl font-semibold">{t("viewLogs.auditLogs.pageTitle")}</h1>
</div>
{/* Filters + pagination on same row */}
<div className="flex flex-wrap items-center gap-3">
<Search
placeholder="Object ID"
placeholder={t("viewLogs.auditLogs.placeholderObjectId")}
allowClear
style={{ width: 200 }}
onSearch={(val) => {
@ -198,7 +201,7 @@ export default function AuditLogs({ userID, userRole, token, accessToken, isActi
}}
/>
<Search
placeholder="Changed By"
placeholder={t("viewLogs.auditLogs.placeholderChangedBy")}
allowClear
style={{ width: 180 }}
onSearch={(val) => {
@ -213,7 +216,7 @@ export default function AuditLogs({ userID, userRole, token, accessToken, isActi
}}
/>
<Search
placeholder="Team ID"
placeholder={t("viewLogs.auditLogs.placeholderTeamId")}
allowClear
style={{ width: 180 }}
onSearch={(val) => {
@ -228,7 +231,7 @@ export default function AuditLogs({ userID, userRole, token, accessToken, isActi
}}
/>
<Search
placeholder="Key Hash"
placeholder={t("viewLogs.auditLogs.placeholderKeyHash")}
allowClear
style={{ width: 180 }}
onSearch={(val) => {
@ -243,14 +246,14 @@ export default function AuditLogs({ userID, userRole, token, accessToken, isActi
}}
/>
<Select
placeholder="All Actions"
placeholder={t("viewLogs.auditLogs.allActions")}
allowClear
style={{ width: 140 }}
options={[
{ label: "Created", value: "created" },
{ label: "Updated", value: "updated" },
{ label: "Deleted", value: "deleted" },
{ label: "Rotated", value: "rotated" },
{ label: t("viewLogs.auditLogs.actionCreated"), value: "created" },
{ label: t("viewLogs.auditLogs.actionUpdated"), value: "updated" },
{ label: t("viewLogs.auditLogs.actionDeleted"), value: "deleted" },
{ label: t("viewLogs.auditLogs.actionRotated"), value: "rotated" },
]}
onChange={(val) => {
setAction(val);
@ -258,15 +261,15 @@ export default function AuditLogs({ userID, userRole, token, accessToken, isActi
}}
/>
<Select
placeholder="All Tables"
placeholder={t("viewLogs.auditLogs.allTables")}
allowClear
style={{ width: 150 }}
options={[
{ label: "Keys", value: "LiteLLM_VerificationToken" },
{ label: "Teams", value: "LiteLLM_TeamTable" },
{ label: "Users", value: "LiteLLM_UserTable" },
{ label: "Organizations", value: "LiteLLM_OrganizationTable" },
{ label: "Models", value: "LiteLLM_ProxyModelTable" },
{ label: t("viewLogs.auditLogs.tableKeys"), value: "LiteLLM_VerificationToken" },
{ label: t("viewLogs.auditLogs.tableTeams"), value: "LiteLLM_TeamTable" },
{ label: t("viewLogs.auditLogs.tableUsers"), value: "LiteLLM_UserTable" },
{ label: t("viewLogs.auditLogs.tableOrganizations"), value: "LiteLLM_OrganizationTable" },
{ label: t("viewLogs.auditLogs.tableModels"), value: "LiteLLM_ProxyModelTable" },
]}
onChange={(val) => {
setTableName(val);
@ -285,7 +288,7 @@ export default function AuditLogs({ userID, userRole, token, accessToken, isActi
current={page}
pageSize={PAGE_SIZE}
total={total}
showTotal={(t) => `${t} total`}
showTotal={(n) => t("viewLogs.auditLogs.showTotal", { total: n })}
showSizeChanger={false}
size="small"
onChange={(p) => setPage(p)}

View file

@ -3,6 +3,7 @@ import type { ColumnDef } from "@tanstack/react-table";
import { Badge, Button } from "@tremor/react";
import { Tooltip } from "antd";
import React, { useState } from "react";
import i18n from "@/lib/i18n";
import { getProviderLogoAndName } from "../provider_info_helpers";
import { TableHeaderSortDropdown } from "../common_components/TableHeaderSortDropdown/TableHeaderSortDropdown";
import { TimeCell } from "./time_cell";
@ -110,19 +111,19 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
header: sortProps
? () => (
<SortableHeader
label="Time"
label={i18n.t("viewLogs.columns.colTime")}
field="startTime"
sortBy={sortProps.sortBy}
sortOrder={sortProps.sortOrder}
onSortChange={sortProps.onSortChange}
/>
)
: "Time",
: i18n.t("viewLogs.columns.colTime"),
accessorKey: "startTime",
cell: (info: any) => <TimeCell utcTime={info.getValue()} />,
},
{
header: "Type",
header: i18n.t("viewLogs.columns.colType"),
id: "type",
cell: (info: any) => {
const row = info.row.original;
@ -166,7 +167,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
},
},
{
header: "Status",
header: i18n.t("viewLogs.columns.colStatus"),
accessorKey: "metadata.status",
cell: (info: any) => {
const status = info.getValue() || "Success";
@ -178,13 +179,13 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
isSuccess ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"
}`}
>
{isSuccess ? "Success" : "Failure"}
{isSuccess ? i18n.t("viewLogs.columns.statusSuccess") : i18n.t("viewLogs.columns.statusFailure")}
</span>
);
},
},
{
header: "Session ID",
header: i18n.t("viewLogs.columns.colSessionId"),
accessorKey: "session_id",
cell: (info: any) => {
const value = String(info.getValue() || "");
@ -205,7 +206,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
},
{
header: "Request ID",
header: i18n.t("viewLogs.columns.colRequestId"),
accessorKey: "request_id",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "")}>
@ -217,14 +218,14 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
header: sortProps
? () => (
<SortableHeader
label="Cost"
label={i18n.t("viewLogs.columns.colCost")}
field="spend"
sortBy={sortProps.sortBy}
sortOrder={sortProps.sortOrder}
onSortChange={sortProps.onSortChange}
/>
)
: "Cost",
: i18n.t("viewLogs.columns.colCost"),
accessorKey: "spend",
cell: (info: any) => {
const row = info.row.original;
@ -238,7 +239,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
</Tooltip>
{mcpCount > 0 && mcpSpend > 0 && (
<span className="text-[10px] text-amber-600">
incl. {getSpendString(mcpSpend)} from {mcpCount} MCP
{i18n.t("viewLogs.columns.mcpCostNote", { spend: getSpendString(mcpSpend), count: mcpCount })}
</span>
)}
</div>
@ -249,14 +250,14 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
header: sortProps
? () => (
<SortableHeader
label="Duration (s)"
label={i18n.t("viewLogs.columns.colDuration")}
field="request_duration_ms"
sortBy={sortProps.sortBy}
sortOrder={sortProps.sortOrder}
onSortChange={sortProps.onSortChange}
/>
)
: "Duration (s)",
: i18n.t("viewLogs.columns.colDuration"),
accessorKey: "request_duration_ms",
cell: (info: any) => {
const ms = info.getValue();
@ -273,14 +274,14 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
header: sortProps
? () => (
<SortableHeader
label="TTFT (s)"
label={i18n.t("viewLogs.columns.colTtft")}
field="ttft_ms"
sortBy={sortProps.sortBy}
sortOrder={sortProps.sortOrder}
onSortChange={sortProps.onSortChange}
/>
)
: "TTFT (s)",
: i18n.t("viewLogs.columns.colTtft"),
accessorKey: "completionStartTime",
cell: (info: any) => {
const row = info.row.original;
@ -299,7 +300,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
},
},
{
header: "Team Name",
header: i18n.t("viewLogs.columns.colTeamName"),
accessorKey: "metadata.user_api_key_team_alias",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "-")}>
@ -308,7 +309,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
),
},
{
header: "Key Hash",
header: i18n.t("viewLogs.columns.colKeyHash"),
accessorKey: "metadata.user_api_key",
cell: (info: any) => {
const value = String(info.getValue() || "-");
@ -327,7 +328,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
},
},
{
header: "Key Name",
header: i18n.t("viewLogs.columns.colKeyName"),
accessorKey: "metadata.user_api_key_alias",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "-")}>
@ -339,14 +340,14 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
header: sortProps
? () => (
<SortableHeader
label="Model"
label={i18n.t("viewLogs.columns.colModel")}
field="model"
sortBy={sortProps.sortBy}
sortOrder={sortProps.sortOrder}
onSortChange={sortProps.onSortChange}
/>
)
: "Model",
: i18n.t("viewLogs.columns.colModel"),
accessorKey: "model",
cell: (info: any) => {
const row = info.row.original;
@ -376,14 +377,14 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
header: sortProps
? () => (
<SortableHeader
label="Tokens"
label={i18n.t("viewLogs.columns.colTokens")}
field="total_tokens"
sortBy={sortProps.sortBy}
sortOrder={sortProps.sortOrder}
onSortChange={sortProps.onSortChange}
/>
)
: "Tokens",
: i18n.t("viewLogs.columns.colTokens"),
accessorKey: "total_tokens",
cell: (info: any) => {
const row = info.row.original;
@ -398,7 +399,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
},
},
{
header: "Internal User",
header: i18n.t("viewLogs.columns.colInternalUser"),
accessorKey: "user",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "-")}>
@ -407,7 +408,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
),
},
{
header: "End User",
header: i18n.t("viewLogs.columns.colEndUser"),
accessorKey: "end_user",
cell: (info: any) => (
<Tooltip title={String(info.getValue() || "-")}>
@ -417,7 +418,7 @@ export const createColumns = (sortProps?: LogsSortProps): ColumnDef<LogEntry>[]
},
{
header: "Tags",
header: i18n.t("viewLogs.columns.colTags"),
accessorKey: "request_tags",
cell: (info: any) => {
const tags = info.getValue();
@ -483,11 +484,11 @@ export const RequestResponsePanel = ({ request, response }: { request: any; resp
<div className="grid grid-cols-2 gap-4 mt-4">
<div className="rounded-lg border border-gray-200 bg-gray-50">
<div className="flex justify-between items-center p-3 border-b border-gray-200">
<h3 className="text-sm font-medium">Request</h3>
<h3 className="text-sm font-medium">{i18n.t("viewLogs.requestResponsePanel.request")}</h3>
<button
onClick={() => copyToClipboard(requestStr)}
className="p-1 hover:bg-gray-200 rounded"
title="Copy request"
title={i18n.t("viewLogs.requestResponsePanel.copyRequest")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -510,11 +511,11 @@ export const RequestResponsePanel = ({ request, response }: { request: any; resp
<div className="rounded-lg border border-gray-200 bg-gray-50">
<div className="flex justify-between items-center p-3 border-b border-gray-200">
<h3 className="text-sm font-medium">Response</h3>
<h3 className="text-sm font-medium">{i18n.t("viewLogs.requestResponsePanel.response")}</h3>
<button
onClick={() => copyToClipboard(responseStr)}
className="p-1 hover:bg-gray-200 rounded"
title="Copy response"
title={i18n.t("viewLogs.requestResponsePanel.copyResponse")}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -550,7 +551,8 @@ const CollapsibleJsonCell = ({ jsonData }: { jsonData: any }) => {
return (
<div>
<button onClick={() => setIsExpanded(!isExpanded)} className="text-blue-500 hover:text-blue-700 text-xs">
{isExpanded ? "Hide JSON" : "Show JSON"} ({Object.keys(jsonData).length} fields)
{isExpanded ? i18n.t("viewLogs.columns.hideJson") : i18n.t("viewLogs.columns.showJson")} (
{Object.keys(jsonData).length} fields)
</button>
{isExpanded && (
<pre className="mt-2 p-2 bg-gray-50 border rounded text-xs overflow-auto max-h-60">{jsonString}</pre>
@ -596,7 +598,7 @@ export const auditLogColumns: ColumnDef<AuditLogEntry>[] = [
<button
onClick={toggleHandler}
style={{ cursor: "pointer" }}
aria-label={localExpanded ? "Collapse row" : "Expand row"}
aria-label={localExpanded ? i18n.t("viewLogs.columns.collapseRow") : i18n.t("viewLogs.columns.expandRow")}
className="w-6 h-6 flex items-center justify-center focus:outline-none"
>
<svg
@ -617,31 +619,31 @@ export const auditLogColumns: ColumnDef<AuditLogEntry>[] = [
},
},
{
header: "Timestamp",
header: i18n.t("viewLogs.columns.colTime"),
accessorKey: "updated_at",
cell: (info: any) => <TimeCell utcTime={info.getValue()} />,
},
{
header: "Table Name",
header: i18n.t("viewLogs.columns.colTableName"),
accessorKey: "table_name",
cell: (info: any) => {
const tableName = info.getValue();
let displayValue = tableName;
switch (tableName) {
case "LiteLLM_VerificationToken":
displayValue = "Keys";
displayValue = i18n.t("viewLogs.columns.auditTableKeys");
break;
case "LiteLLM_TeamTable":
displayValue = "Teams";
displayValue = i18n.t("viewLogs.columns.auditTableTeams");
break;
case "LiteLLM_OrganizationTable":
displayValue = "Organizations";
displayValue = i18n.t("viewLogs.columns.auditTableOrganizations");
break;
case "LiteLLM_UserTable":
displayValue = "Users";
displayValue = i18n.t("viewLogs.columns.auditTableUsers");
break;
case "LiteLLM_ProxyModelTable":
displayValue = "Models";
displayValue = i18n.t("viewLogs.columns.auditTableModels");
break;
default:
displayValue = tableName;
@ -650,12 +652,12 @@ export const auditLogColumns: ColumnDef<AuditLogEntry>[] = [
},
},
{
header: "Action",
header: i18n.t("viewLogs.columns.colAction"),
accessorKey: "action",
cell: (info: any) => <span>{getActionBadge(info.getValue())}</span>,
},
{
header: "Changed By",
header: i18n.t("viewLogs.columns.colChangedBy"),
accessorKey: "changed_by",
cell: (info: any) => {
const changedBy = info.row.original.changed_by;
@ -677,7 +679,7 @@ export const auditLogColumns: ColumnDef<AuditLogEntry>[] = [
},
},
{
header: "Affected Item ID",
header: i18n.t("viewLogs.columns.colAffectedItemId"),
accessorKey: "object_id",
cell: (props) => {
const ObjectIdDisplay = () => {
@ -697,7 +699,7 @@ export const auditLogColumns: ColumnDef<AuditLogEntry>[] = [
};
return (
<Tooltip title={copied ? "Copied!" : String(objectId)}>
<Tooltip title={copied ? i18n.t("viewLogs.columns.copied") : String(objectId)}>
<span className="max-w-[20ch] truncate block cursor-pointer hover:text-blue-600" onClick={handleCopy}>
{String(objectId)}
</span>

View file

@ -2264,6 +2264,121 @@
},
"tokenFlow": {
"breakdown": "{{total}} ({{prompt}} prompt tokens + {{completion}} completion tokens)"
},
"logsTableToolbar": {
"searchPlaceholder": "Search by Request ID",
"customRange": "Custom Range",
"liveTail": "Live Tail",
"fetchData": "Fetch data",
"fetching": "Fetching",
"fetch": "Fetch",
"to": "to",
"showing": "Showing {{from}} - {{to}} of {{total}} results",
"page": "Page {{current}} of {{total}}",
"autoRefreshing": "Auto-refreshing every 15 seconds",
"stop": "Stop"
},
"requestResponsePanel": {
"requestCopied": "Request copied to clipboard",
"failedToCopyRequest": "Failed to copy request",
"responseCopied": "Response copied to clipboard",
"failedToCopyResponse": "Failed to copy response",
"request": "Request",
"response": "Response",
"copyRequest": "Copy request",
"copyResponse": "Copy response",
"httpErrorCode": "• HTTP code {{code}}",
"responseNotAvailable": "Response data not available"
},
"formattedToolView": {
"colParameter": "Parameter",
"colType": "Type",
"colDescription": "Description",
"parametersLabel": "Parameters",
"calledWithLabel": "Called With"
},
"toolExpandedContent": {
"descriptionLabel": "Description",
"viewFormatted": "Formatted",
"viewJson": "JSON"
},
"toolsSection": {
"title": "Tools",
"summary": "{{total}} provided, {{called}} called"
},
"vectorStoreViewer": {
"title": "Vector Store Requests",
"labelQuery": "Query:",
"labelVectorStoreId": "Vector Store ID:",
"labelProvider": "Provider:",
"labelStartTime": "Start Time:",
"labelEndTime": "End Time:",
"labelDuration": "Duration:",
"searchResults": "Search Results",
"resultItem": "Result {{number}}",
"score": "Score:"
},
"auditLogs": {
"enterpriseTitle": "Enterprise Feature.",
"enterpriseDesc": "This is a LiteLLM Enterprise feature, and requires a valid key to use.",
"previewText": "Here's a preview of what Audit Logs offer:",
"previewAlt": "Audit Logs Preview",
"pageTitle": "Audit Logs",
"placeholderObjectId": "Object ID",
"placeholderChangedBy": "Changed By",
"placeholderTeamId": "Team ID",
"placeholderKeyHash": "Key Hash",
"allActions": "All Actions",
"allTables": "All Tables",
"tableKeys": "Keys",
"tableTeams": "Teams",
"tableUsers": "Users",
"tableOrganizations": "Organizations",
"tableModels": "Models",
"actionCreated": "Created",
"actionUpdated": "Updated",
"actionDeleted": "Deleted",
"actionRotated": "Rotated",
"showTotal": "{{total}} total",
"colTimestamp": "Timestamp",
"colAction": "Action",
"colTable": "Table",
"colObjectId": "Object ID",
"colChangedBy": "Changed By",
"colApiKeyHash": "API Key (Hash)"
},
"columns": {
"colTime": "Time",
"colType": "Type",
"colStatus": "Status",
"colSessionId": "Session ID",
"colRequestId": "Request ID",
"colCost": "Cost",
"colDuration": "Duration (s)",
"colTtft": "TTFT (s)",
"colTeamName": "Team Name",
"colKeyHash": "Key Hash",
"colKeyName": "Key Name",
"colModel": "Model",
"colTokens": "Tokens",
"colInternalUser": "Internal User",
"colEndUser": "End User",
"colTags": "Tags",
"statusSuccess": "Success",
"statusFailure": "Failure",
"mcpCostNote": "incl. {{spend}} from {{count}} MCP",
"colTableName": "Table Name",
"colAffectedItemId": "Affected Item ID",
"auditTableKeys": "Keys",
"auditTableTeams": "Teams",
"auditTableOrganizations": "Organizations",
"auditTableUsers": "Users",
"auditTableModels": "Models",
"expandRow": "Expand row",
"collapseRow": "Collapse row",
"hideJson": "Hide JSON",
"showJson": "Show JSON",
"copied": "Copied!"
}
}
}

View file

@ -2264,6 +2264,121 @@
},
"tokenFlow": {
"breakdown": "{{total}}({{prompt}} 提示词 Token + {{completion}} 补全 Token)"
},
"logsTableToolbar": {
"searchPlaceholder": "按请求 ID 搜索",
"customRange": "自定义范围",
"liveTail": "实时追踪",
"fetchData": "获取数据",
"fetching": "获取中",
"fetch": "获取",
"to": "至",
"showing": "显示第 {{from}} - {{to}} 条,共 {{total}} 条",
"page": "第 {{current}} 页,共 {{total}} 页",
"autoRefreshing": "每 15 秒自动刷新",
"stop": "停止"
},
"requestResponsePanel": {
"requestCopied": "请求已复制到剪贴板",
"failedToCopyRequest": "复制请求失败",
"responseCopied": "响应已复制到剪贴板",
"failedToCopyResponse": "复制响应失败",
"request": "请求",
"response": "响应",
"copyRequest": "复制请求",
"copyResponse": "复制响应",
"httpErrorCode": "• HTTP 错误码 {{code}}",
"responseNotAvailable": "响应数据不可用"
},
"formattedToolView": {
"colParameter": "参数",
"colType": "类型",
"colDescription": "描述",
"parametersLabel": "参数",
"calledWithLabel": "调用参数"
},
"toolExpandedContent": {
"descriptionLabel": "描述",
"viewFormatted": "格式化",
"viewJson": "JSON"
},
"toolsSection": {
"title": "工具",
"summary": "共 {{total}} 个,已调用 {{called}} 个"
},
"vectorStoreViewer": {
"title": "向量存储请求",
"labelQuery": "查询:",
"labelVectorStoreId": "向量存储 ID:",
"labelProvider": "提供商:",
"labelStartTime": "开始时间:",
"labelEndTime": "结束时间:",
"labelDuration": "耗时:",
"searchResults": "搜索结果",
"resultItem": "结果 {{number}}",
"score": "得分:"
},
"auditLogs": {
"enterpriseTitle": "企业版功能。",
"enterpriseDesc": "这是 LiteLLM 企业版功能,需要有效的许可证密钥方可使用。",
"previewText": "以下是审计日志功能的预览:",
"previewAlt": "审计日志预览",
"pageTitle": "审计日志",
"placeholderObjectId": "对象 ID",
"placeholderChangedBy": "操作人",
"placeholderTeamId": "团队 ID",
"placeholderKeyHash": "密钥哈希",
"allActions": "所有操作",
"allTables": "所有表",
"tableKeys": "密钥",
"tableTeams": "团队",
"tableUsers": "用户",
"tableOrganizations": "组织",
"tableModels": "模型",
"actionCreated": "创建",
"actionUpdated": "更新",
"actionDeleted": "删除",
"actionRotated": "轮换",
"showTotal": "共 {{total}} 条",
"colTimestamp": "时间戳",
"colAction": "操作",
"colTable": "表",
"colObjectId": "对象 ID",
"colChangedBy": "操作人",
"colApiKeyHash": "API 密钥(哈希)"
},
"columns": {
"colTime": "时间",
"colType": "类型",
"colStatus": "状态",
"colSessionId": "会话 ID",
"colRequestId": "请求 ID",
"colCost": "费用",
"colDuration": "耗时(秒)",
"colTtft": "TTFT(秒)",
"colTeamName": "团队名称",
"colKeyHash": "密钥哈希",
"colKeyName": "密钥名称",
"colModel": "模型",
"colTokens": "Token",
"colInternalUser": "内部用户",
"colEndUser": "终端用户",
"colTags": "标签",
"statusSuccess": "成功",
"statusFailure": "失败",
"mcpCostNote": "含 {{count}} 个 MCP 花费 {{spend}}",
"colTableName": "表名称",
"colAffectedItemId": "受影响项目 ID",
"auditTableKeys": "密钥",
"auditTableTeams": "团队",
"auditTableOrganizations": "组织",
"auditTableUsers": "用户",
"auditTableModels": "模型",
"expandRow": "展开行",
"collapseRow": "折叠行",
"hideJson": "隐藏 JSON",
"showJson": "显示 JSON",
"copied": "已复制!"
}
}
}