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 { Button, Card, Space, Table, Tag, Toast, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import { IconCopy } from '@douyinfe/semi-icons';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
import type { CapacityMetrics, LinkHealth, OpsHealth } from '../api/types';
|
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() {
|
export function OpsQuality() {
|
||||||
const [health, setHealth] = useState<OpsHealth | null>(null);
|
const [health, setHealth] = useState<OpsHealth | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -252,7 +291,12 @@ export function OpsQuality() {
|
|||||||
<PageHeader
|
<PageHeader
|
||||||
title="运维质量"
|
title="运维质量"
|
||||||
description="集中查看网关、消息队列、缓存、时序库、关系库、地图配置和运行版本状态"
|
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">
|
<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 () => {
|
test('renders ops quality as a standalone runtime health page', async () => {
|
||||||
window.history.replaceState(null, '', '/#/ops-quality');
|
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 fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async (input) => {
|
||||||
const path = String(input);
|
const path = String(input);
|
||||||
if (path.includes('/api/ops/health')) {
|
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.getAllByText('NATS 桥接待消费').length).toBeGreaterThanOrEqual(1);
|
||||||
expect(screen.getByText('桥接处置顺序')).toBeInTheDocument();
|
expect(screen.getByText('桥接处置顺序')).toBeInTheDocument();
|
||||||
expect(fetchMock).toHaveBeenCalledWith(expect.stringContaining('/api/ops/health'), undefined);
|
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 () => {
|
test('copies notification text from quality priority queue', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user