mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-11 01:03:51 +08:00
🐛 fix(redis): 修复监控图表提示数据错位
- 使用唯一时间戳作为监控图表横轴数据键 - 统一格式化刻度与提示时间并覆盖跨分钟回归 Fixes #689
This commit is contained in:
@@ -183,6 +183,40 @@ describe('RedisMonitor polling', () => {
|
||||
expect(redisApi.RedisGetServerInfo).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('uses unique chart x values when samples share the same second across minutes', async () => {
|
||||
vi.setSystemTime(new Date('2026-07-27T10:00:22'));
|
||||
|
||||
await act(async () => {
|
||||
renderer = create(renderMonitor(true));
|
||||
});
|
||||
await act(async () => {
|
||||
renderer!.root.findAllByType('button')[0].props.onClick();
|
||||
});
|
||||
|
||||
vi.setSystemTime(new Date('2026-07-27T10:01:22'));
|
||||
await act(async () => {
|
||||
renderer!.root.findAllByType('button')[1].props.onClick();
|
||||
});
|
||||
|
||||
const charts = renderer!.root.findAll(node => Array.isArray(node.props.data));
|
||||
expect(charts).toHaveLength(4);
|
||||
|
||||
const points = charts[0].props.data as Array<{ timestamp?: number }>;
|
||||
expect(points).toHaveLength(2);
|
||||
expect(new Set(points.map(point => point.timestamp))).toHaveLength(2);
|
||||
|
||||
const xAxes = renderer!.root.findAll(node => node.props.dataKey === 'timestamp');
|
||||
expect(xAxes).toHaveLength(4);
|
||||
expect(xAxes.every(node => node.props.type === 'number')).toBe(true);
|
||||
|
||||
const labels = points.map(point => xAxes[0].props.tickFormatter(point.timestamp));
|
||||
expect(labels[0]).not.toBe(labels[1]);
|
||||
|
||||
const tooltips = renderer!.root.findAll(node => typeof node.props.labelFormatter === 'function');
|
||||
expect(tooltips).toHaveLength(4);
|
||||
expect(tooltips[0].props.labelFormatter(points[1].timestamp)).toBe(labels[1]);
|
||||
});
|
||||
|
||||
it('discards a pending manual refresh after deactivation and lets a new generation run', async () => {
|
||||
const staleRefresh = createDeferred<ReturnType<typeof serverInfoResponse>>();
|
||||
redisApi.RedisGetServerInfo
|
||||
|
||||
@@ -27,7 +27,7 @@ interface RedisMonitorProps {
|
||||
|
||||
// Data point for charts
|
||||
interface MetricPoint {
|
||||
time: string;
|
||||
timestamp: number;
|
||||
qps: number;
|
||||
memory: number; // in MB
|
||||
memory_rss: number; // in MB
|
||||
@@ -40,6 +40,17 @@ interface MetricPoint {
|
||||
|
||||
const MAX_HISTORY_POINTS = 60; // Keep up to 60 data points
|
||||
const POLL_INTERVAL_MS = 2000;
|
||||
const formatMetricTimestamp = (timestamp: unknown) => {
|
||||
const value = Number(timestamp);
|
||||
if (!Number.isFinite(value)) return '';
|
||||
|
||||
return new Date(value).toLocaleTimeString([], {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
const RedisMonitor: React.FC<RedisMonitorProps> = ({ connectionId, redisDB, isActive = true }) => {
|
||||
const connections = useStore(state => state.connections);
|
||||
@@ -94,8 +105,7 @@ const RedisMonitor: React.FC<RedisMonitorProps> = ({ connectionId, redisDB, isAc
|
||||
const infoMap = res.data as Record<string, string>;
|
||||
setCurrentInfo(infoMap);
|
||||
|
||||
const now = new Date();
|
||||
const timeStr = now.toLocaleTimeString([], { hour12: false, second: '2-digit' });
|
||||
const timestamp = Date.now();
|
||||
|
||||
// Parse values
|
||||
const qps = parseInt(infoMap['instantaneous_ops_per_sec'] || '0', 10);
|
||||
@@ -118,7 +128,7 @@ const RedisMonitor: React.FC<RedisMonitorProps> = ({ connectionId, redisDB, isAc
|
||||
});
|
||||
|
||||
const point: MetricPoint = {
|
||||
time: timeStr,
|
||||
timestamp,
|
||||
qps,
|
||||
memory: parseFloat((memBytes / 1024 / 1024).toFixed(2)),
|
||||
memory_rss: parseFloat((memRssBytes / 1024 / 1024).toFixed(2)),
|
||||
@@ -334,11 +344,22 @@ const RedisMonitor: React.FC<RedisMonitorProps> = ({ connectionId, redisDB, isAc
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={chartGridColor} />
|
||||
<XAxis dataKey="time" tick={{ fill: chartTextColor, fontSize: 12 }} axisLine={false} tickLine={false} minTickGap={20} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
type="number"
|
||||
scale="time"
|
||||
domain={['dataMin', 'dataMax']}
|
||||
tickFormatter={formatMetricTimestamp}
|
||||
tick={{ fill: chartTextColor, fontSize: 12 }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
minTickGap={20}
|
||||
/>
|
||||
<YAxis tick={{ fill: chartTextColor, fontSize: 12 }} axisLine={false} tickLine={false} />
|
||||
<RechartsTooltip
|
||||
contentStyle={{ backgroundColor: cardBgColor, border: `1px solid ${chartGridColor}`, borderRadius: 6 }}
|
||||
itemStyle={{ fontWeight: 600 }}
|
||||
labelFormatter={formatMetricTimestamp}
|
||||
/>
|
||||
<Area type="monotone" dataKey="qps" name="QPS" stroke="#52c41a" strokeWidth={2} fillOpacity={1} fill="url(#colorQps)" isAnimationActive={false} />
|
||||
</AreaChart>
|
||||
@@ -356,12 +377,23 @@ const RedisMonitor: React.FC<RedisMonitorProps> = ({ connectionId, redisDB, isAc
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={history} margin={{ top: 10, right: 10, left: 10, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={chartGridColor} />
|
||||
<XAxis dataKey="time" tick={{ fill: chartTextColor, fontSize: 12 }} axisLine={false} tickLine={false} minTickGap={20} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
type="number"
|
||||
scale="time"
|
||||
domain={['dataMin', 'dataMax']}
|
||||
tickFormatter={formatMetricTimestamp}
|
||||
tick={{ fill: chartTextColor, fontSize: 12 }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
minTickGap={20}
|
||||
/>
|
||||
<YAxis tick={{ fill: chartTextColor, fontSize: 12 }} axisLine={false} tickLine={false} domain={['auto', 'auto']} />
|
||||
<RechartsTooltip
|
||||
contentStyle={{ backgroundColor: cardBgColor, border: `1px solid ${chartGridColor}`, borderRadius: 6 }}
|
||||
itemStyle={{ fontWeight: 600 }}
|
||||
formatter={(value: any) => [`${value} MB`]}
|
||||
labelFormatter={formatMetricTimestamp}
|
||||
/>
|
||||
<Legend verticalAlign="top" height={36}/>
|
||||
<Line type="monotone" dataKey="memory" name={tr('redis_monitor.series.used_memory')} stroke="#eb2f96" strokeWidth={2} dot={false} isAnimationActive={false} />
|
||||
@@ -383,12 +415,23 @@ const RedisMonitor: React.FC<RedisMonitorProps> = ({ connectionId, redisDB, isAc
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={history} margin={{ top: 10, right: 10, left: 10, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={chartGridColor} />
|
||||
<XAxis dataKey="time" tick={{ fill: chartTextColor, fontSize: 12 }} axisLine={false} tickLine={false} minTickGap={20} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
type="number"
|
||||
scale="time"
|
||||
domain={['dataMin', 'dataMax']}
|
||||
tickFormatter={formatMetricTimestamp}
|
||||
tick={{ fill: chartTextColor, fontSize: 12 }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
minTickGap={20}
|
||||
/>
|
||||
<YAxis tick={{ fill: chartTextColor, fontSize: 12 }} axisLine={false} tickLine={false} />
|
||||
<RechartsTooltip
|
||||
contentStyle={{ backgroundColor: cardBgColor, border: `1px solid ${chartGridColor}`, borderRadius: 6 }}
|
||||
itemStyle={{ fontWeight: 600 }}
|
||||
formatter={(value: any) => [`${value} s`]}
|
||||
labelFormatter={formatMetricTimestamp}
|
||||
/>
|
||||
<Legend verticalAlign="top" height={36}/>
|
||||
<Line type="monotone" dataKey="cpuSys" name={tr('redis_monitor.series.system')} stroke="#cf1322" strokeWidth={2} dot={false} isAnimationActive={false} />
|
||||
@@ -408,12 +451,23 @@ const RedisMonitor: React.FC<RedisMonitorProps> = ({ connectionId, redisDB, isAc
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={history} margin={{ top: 10, right: 10, left: 10, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke={chartGridColor} />
|
||||
<XAxis dataKey="time" tick={{ fill: chartTextColor, fontSize: 12 }} axisLine={false} tickLine={false} minTickGap={20} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
type="number"
|
||||
scale="time"
|
||||
domain={['dataMin', 'dataMax']}
|
||||
tickFormatter={formatMetricTimestamp}
|
||||
tick={{ fill: chartTextColor, fontSize: 12 }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
minTickGap={20}
|
||||
/>
|
||||
<YAxis yAxisId="left" tick={{ fill: chartTextColor, fontSize: 12 }} axisLine={false} tickLine={false} />
|
||||
<YAxis yAxisId="right" orientation="right" tick={{ fill: chartTextColor, fontSize: 12 }} axisLine={false} tickLine={false} />
|
||||
<RechartsTooltip
|
||||
contentStyle={{ backgroundColor: cardBgColor, border: `1px solid ${chartGridColor}`, borderRadius: 6 }}
|
||||
itemStyle={{ fontWeight: 600 }}
|
||||
labelFormatter={formatMetricTimestamp}
|
||||
/>
|
||||
<Legend verticalAlign="top" height={36}/>
|
||||
<Line yAxisId="left" type="stepAfter" dataKey="clients" name={tr('redis_monitor.series.clients')} stroke="#1677ff" strokeWidth={2} dot={false} isAnimationActive={false} />
|
||||
|
||||
Reference in New Issue
Block a user