feat(log-center): 日志中心 v4 全量组件 — 统一查询面板/时间选择器/统计图表/聚类/数据源管理
This commit is contained in:
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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user