diff --git a/frontend/src/components/RedisMonitor.polling.test.tsx b/frontend/src/components/RedisMonitor.polling.test.tsx index 4f45e138..089491da 100644 --- a/frontend/src/components/RedisMonitor.polling.test.tsx +++ b/frontend/src/components/RedisMonitor.polling.test.tsx @@ -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>(); redisApi.RedisGetServerInfo diff --git a/frontend/src/components/RedisMonitor.tsx b/frontend/src/components/RedisMonitor.tsx index 059c3bf5..272a30e1 100644 --- a/frontend/src/components/RedisMonitor.tsx +++ b/frontend/src/components/RedisMonitor.tsx @@ -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 = ({ connectionId, redisDB, isActive = true }) => { const connections = useStore(state => state.connections); @@ -94,8 +105,7 @@ const RedisMonitor: React.FC = ({ connectionId, redisDB, isAc const infoMap = res.data as Record; 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 = ({ 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 = ({ connectionId, redisDB, isAc - + @@ -356,12 +377,23 @@ const RedisMonitor: React.FC = ({ connectionId, redisDB, isAc - + [`${value} MB`]} + labelFormatter={formatMetricTimestamp} /> @@ -383,12 +415,23 @@ const RedisMonitor: React.FC = ({ connectionId, redisDB, isAc - + [`${value} s`]} + labelFormatter={formatMetricTimestamp} /> @@ -408,12 +451,23 @@ const RedisMonitor: React.FC = ({ connectionId, redisDB, isAc - +