feat(platform): add ops capacity handoff
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Button, Card, Space, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui';
|
||||
import { IconCopy } from '@douyinfe/semi-icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { api } from '../api/client';
|
||||
import type { CapacityMetrics, LinkHealth, OpsHealth } from '../api/types';
|
||||
@@ -226,6 +227,44 @@ function capacityRisk(finding: string): CapacityRisk {
|
||||
};
|
||||
}
|
||||
|
||||
function opsCapacityHandoffText(health: OpsHealth | null) {
|
||||
const metrics = getCapacityMetrics(health);
|
||||
const risks = (health?.capacityFindings ?? []).map(capacityRisk);
|
||||
const unhealthyLinks = (health?.linkHealth ?? []).filter((item) => item.status !== 'ok');
|
||||
const runtime = health?.runtime;
|
||||
return [
|
||||
'【车辆数据中台运维容量交接】',
|
||||
`运行版本:${runtime?.platformRelease || '-'}`,
|
||||
`Kafka Lag:${formatNumber(metrics.kafkaLag)}`,
|
||||
`活跃连接:${formatNumber(metrics.activeConnections)}`,
|
||||
`Redis 在线 Key:${formatNumber(health?.redisOnlineKeys)}`,
|
||||
`NATS桥接:consumer pending ${formatNumber(metrics.bridgeConsumerPending)} / ack pending ${formatNumber(metrics.bridgeAckPending)} / batch pending ${formatNumber(metrics.bridgeBatchPendingMessages)}`,
|
||||
`快速写入:consumer pending ${formatNumber(metrics.fastWriterConsumerPending)} / ack pending ${formatNumber(metrics.fastWriterAckPending)} / batch pending ${formatNumber(metrics.fastWriterBatchPending)}`,
|
||||
`历史写入:batch pending ${formatNumber(metrics.historyBatchPending)} / rows pending ${formatNumber(metrics.historyRowsPending)}`,
|
||||
`存储状态:TDengine ${statusText(health?.tdengineWritable)} / MySQL ${statusText(health?.mysqlWritable)}`,
|
||||
`地图配置:Web JS ${runtime?.amapWebJsConfigured ? '已配置' : '未配置'} / API ${runtime?.amapApiConfigured ? '已配置' : '未配置'} / ${amapSecurityText(health)}`,
|
||||
`异常链路:${unhealthyLinks.length > 0 ? unhealthyLinks.map((item) => `${item.name}=${item.status}(${item.detail || '-'})`).join(';') : '正常'}`,
|
||||
'',
|
||||
'容量发现:',
|
||||
...(risks.length > 0 ? risks.map((risk, index) => `${index + 1}. [${risk.category}] ${risk.finding} -> ${risk.action}`) : ['暂无容量风险']),
|
||||
'',
|
||||
'处置顺序:',
|
||||
'1. 先看 Kafka Lag 与 NATS ACK pending,确认消息是否卡在桥接或下游消费',
|
||||
'2. 再看快速写入 pending,确认 Redis/TDengine 实时链路是否影响在线和实时数据',
|
||||
'3. 再看历史写入 pending,确认轨迹、RAW、统计是否滞后',
|
||||
'4. 最后核对 MySQL、TDengine、Redis 和地图配置,确认中台查询体验是否受影响'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
async function copyText(value: string, label: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
Toast.success(`已复制${label}`);
|
||||
} catch {
|
||||
Toast.error(`复制${label}失败`);
|
||||
}
|
||||
}
|
||||
|
||||
export function OpsQuality() {
|
||||
const [health, setHealth] = useState<OpsHealth | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -252,7 +291,12 @@ export function OpsQuality() {
|
||||
<PageHeader
|
||||
title="运维质量"
|
||||
description="集中查看网关、消息队列、缓存、时序库、关系库、地图配置和运行版本状态"
|
||||
actions={<Button size="small" loading={loading} onClick={load}>刷新</Button>}
|
||||
actions={(
|
||||
<Space>
|
||||
<Button size="small" icon={<IconCopy />} aria-label="复制容量交接" disabled={!health} onClick={() => copyText(opsCapacityHandoffText(health), '运维容量交接')}>复制容量交接</Button>
|
||||
<Button size="small" loading={loading} onClick={load}>刷新</Button>
|
||||
</Space>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="vp-kpi-grid">
|
||||
|
||||
@@ -3497,6 +3497,11 @@ test('renders notification rules as a standalone operations page', async () => {
|
||||
|
||||
test('renders ops quality as a standalone runtime health page', async () => {
|
||||
window.history.replaceState(null, '', '/#/ops-quality');
|
||||
const writeText = vi.fn(() => Promise.resolve());
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
configurable: true,
|
||||
value: { writeText }
|
||||
});
|
||||
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||
const path = String(input);
|
||||
if (path.includes('/api/ops/health')) {
|
||||
@@ -3579,6 +3584,14 @@ test('renders ops quality as a standalone runtime health page', async () => {
|
||||
expect(screen.getAllByText('NATS 桥接待消费').length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByText('桥接处置顺序')).toBeInTheDocument();
|
||||
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/ops/health'), undefined);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '复制容量交接' }));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('【车辆数据中台运维容量交接】'));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('运行版本:platform-ops-test'));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('Kafka Lag:42'));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('NATS桥接:consumer pending 1,482,047 / ack pending 4,000'));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('存储状态:TDengine 正常 / MySQL 异常'));
|
||||
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('1. 先看 Kafka Lag 与 NATS ACK pending'));
|
||||
});
|
||||
|
||||
test('copies notification text from quality priority queue', async () => {
|
||||
|
||||
Reference in New Issue
Block a user