feat(log-center): 日志中心 v4 全量组件 — 统一查询面板/时间选择器/统计图表/聚类/数据源管理

This commit is contained in:
attacker committed 2026-06-27 11:41:50 +08:00
1 parent 880cf050fe
commit bdf3c2daf4
30 files changed
+4276 -136

No files matched your search

@@ -0,0 +1,142 @@
// 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>
);
}