Files
aiops/frontend/src/components/monitor/log-center/LogHistogram.tsx
T
attacker 72bfb0e12d fix(i18n): 回退到稳定的 32% 覆盖率状态 (tsc + Vite 构建通过)
批量转换脚本引入的语法错误已回退,保留稳定的 i18n 转换:
- 翻译词典: 22 namespaces, 3600+ keys
- 已接入: 145+ 文件 (Dashboard/Login/K8s 主页面/Monitor/System/Deploy)
- 语言切换器在 Header 中可用 (🇨🇳/🇺🇸)
- tsc 零错误, Vite 构建成功
2026-07-03 02:02:29 +08:00

143 lines
5.8 KiB
TypeScript

// LogHistogram.tsx — Log Volume 时间分布面积图 // Logan.Li
import { useMemo } from 'react';
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import type { LogEngine } from './types-v3';
interface LogHistogramProps {
data: { timestamp: string; count: number }[];
engine: LogEngine;
onBarClick?: (start: string, end: string) => void;
loading?: boolean;
totalBytes?: number; // 新增:处理字节数 // Logan.Li
lineLimitReached?: boolean; // 新增:是否达到行限制
coverage?: string; // 新增:时间覆盖率
}
const ENGINE_COLORS: Record<LogEngine, { fill: string; stroke: string }> = {
loki: { fill: 'url(#gradientLoki)', stroke: '#06b6d4' },
elasticsearch: { fill: 'url(#gradientES)', stroke: '#22c55e' },
sls: { fill: 'url(#gradientSLS)', stroke: '#f97316' },
};
function formatTimeLabel(ts: string): string {
const parts = ts.split(':');
return parts.length >= 2 ? `${parts[0].slice(-2)}:${parts[1]}` : ts.slice(-5);
}
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} KB`;
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
}
export function LogHistogram({ data, engine, onBarClick, loading, totalBytes, lineLimitReached, coverage }: LogHistogramProps) {
const colors = ENGINE_COLORS[engine];
const chartData = useMemo(() => {
return data.map(d => ({ ...d, label: formatTimeLabel(d.timestamp) }));
}, [data]);
if (loading) {
return (
<div className="rounded-lg border animate-pulse" style={{ background: 'var(--dg-bg-secondary)', borderColor: 'var(--dg-border)' }}>
<div className="h-[140px] flex items-center justify-center">
<span className="text-xs" style={{ color: 'var(--dg-text-muted)' }}>Loading log volume...</span>
</div>
</div>
);
}
if (chartData.length === 0) {
return (
<div className="rounded-lg border flex items-center justify-center" style={{ background: 'var(--dg-bg-secondary)', borderColor: 'var(--dg-border)', minHeight: '100px' }}>
<span className="text-xs" style={{ color: 'var(--dg-text-muted)' }}>Enter a query to display volume</span>
</div>
);
}
return (
<div>
{/* 统计提示行 */}
{(lineLimitReached || coverage || totalBytes) && (
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 mb-2 text-[11px]" style={{ color: 'var(--dg-text-muted)' }}>
{lineLimitReached && (
<span>
<span style={{ color: '#f59e0b' }}>Line limit reached</span>
{coverage && `, received logs cover ${coverage}`}
</span>
)}
{totalBytes !== undefined && (
<span>Total bytes processed: <span className="font-medium" style={{ color: 'var(--dg-text-secondary)' }}>{formatBytes(totalBytes)}</span></span>
)}
</div>
)}
<div className="h-[140px] rounded-lg border" style={{ background: 'var(--dg-bg-secondary)', borderColor: 'var(--dg-border)' }}>
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData} margin={{ top: 8, right: 8, bottom: 0, left: 0 }}>
<defs>
<linearGradient id="gradientLoki" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#06b6d4" stopOpacity={0.3} />
<stop offset="100%" stopColor="#06b6d4" stopOpacity={0.02} />
</linearGradient>
<linearGradient id="gradientES" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#22c55e" stopOpacity={0.3} />
<stop offset="100%" stopColor="#22c55e" stopOpacity={0.02} />
</linearGradient>
<linearGradient id="gradientSLS" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="#f97316" stopOpacity={0.3} />
<stop offset="100%" stopColor="#f97316" stopOpacity={0.02} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="var(--dg-border)" vertical={false} />
<XAxis
dataKey="label"
tick={{ fontSize: 10, fill: 'var(--dg-text-muted)' }}
axisLine={{ stroke: 'var(--dg-border)' }}
tickLine={false}
interval="preserveStartEnd"
/>
<YAxis
tick={{ fontSize: 10, fill: 'var(--dg-text-muted)' }}
axisLine={false}
tickLine={false}
width={35}
/>
<Tooltip
contentStyle={{
background: 'var(--dg-bg-secondary)',
border: '1px solid var(--dg-border)',
borderRadius: '6px',
fontSize: '11px',
color: 'var(--dg-text-primary)',
}}
formatter={(value: number) => [`${value} 条`, 'logs']}
labelFormatter={(label: string) => {
const item = chartData.find(d => d.label === label);
return item ? item.timestamp : label;
}}
/>
<Area
type="monotone"
dataKey="count"
stroke={colors.stroke}
fill={colors.fill}
strokeWidth={1.5}
dot={false}
activeDot={{ r: 3, stroke: colors.stroke, strokeWidth: 1 }}
onClick={(entry: any) => {
if (onBarClick && entry?.timestamp) {
const start = entry.timestamp;
const end = start.slice(0, 13) + ':59:59';
onBarClick(start, end);
}
}}
style={{ cursor: onBarClick ? 'pointer' : 'default' }}
/>
</AreaChart>
</ResponsiveContainer>
</div>
</div>
);
}