批量转换脚本引入的语法错误已回退,保留稳定的 i18n 转换: - 翻译词典: 22 namespaces, 3600+ keys - 已接入: 145+ 文件 (Dashboard/Login/K8s 主页面/Monitor/System/Deploy) - 语言切换器在 Header 中可用 (🇨🇳/🇺🇸) - tsc 零错误, Vite 构建成功
143 lines
5.8 KiB
TypeScript
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>
|
|
);
|
|
}
|