feat(platform): add source readiness operations view

This commit is contained in:
lingniu
2026-07-04 23:29:31 +08:00
parent bf106a9730
commit 44c7748958
9 changed files with 536 additions and 4 deletions

View File

@@ -2,7 +2,7 @@ import { Button, Card, Space, Table, Tag, Toast, Typography } from '@douyinfe/se
import { IconCopy } from '@douyinfe/semi-icons';
import { useEffect, useState } from 'react';
import { api } from '../api/client';
import type { CapacityMetrics, LinkHealth, OpsHealth } from '../api/types';
import type { CapacityMetrics, LinkHealth, OpsHealth, SourceReadinessPlan, SourceReadinessRow } from '../api/types';
import { PageHeader } from '../components/PageHeader';
const statusColor: Record<string, 'green' | 'orange' | 'red' | 'grey'> = {
@@ -15,6 +15,14 @@ function formatNumber(value?: number | null) {
return value == null ? '-' : value.toLocaleString();
}
function formatRate(value?: number | null) {
return value == null || !Number.isFinite(Number(value)) ? '0%' : `${Number(value).toLocaleString(undefined, { maximumFractionDigits: 1 })}%`;
}
function normalizeSourceReadiness(plan: SourceReadinessPlan | null): SourceReadinessPlan | null {
return plan && Array.isArray(plan.sources) ? plan : null;
}
function statusText(ok?: boolean) {
return ok ? '正常' : '异常';
}
@@ -384,6 +392,27 @@ function opsCapacityActionPlanText(health: OpsHealth | null) {
].join('\n\n');
}
function sourceReadinessReport(plan: SourceReadinessPlan | null) {
if (!plan) return '【数据源生产就绪度】\n暂无数据源就绪度数据';
return [
'【数据源生产就绪度】',
`运行版本:${plan.platformRelease || '-'}`,
`车辆规模:${formatNumber(plan.totalVehicles)} / 在线 ${formatNumber(plan.onlineVehicles)}`,
`接入状态:连接 ${formatNumber(plan.activeConnections)} / Redis在线Key ${formatNumber(plan.redisOnlineKeys)} / Kafka Lag ${formatNumber(plan.kafkaLag)}`,
'',
...plan.sources.map((source, index) => [
`${index + 1}. [${source.severity}] ${source.protocol} - ${source.status}`,
` 角色:${source.role}`,
` 证据:${source.evidence}`,
` 在线率:${formatRate(source.onlineRate)},缺失车辆:${formatNumber(source.missingVehicles)}`,
` 动作:${source.action}`,
` 验收:${source.acceptance}`,
` 实时监控:${window.location.origin}${window.location.pathname}${source.realtimeHash}`,
` 历史证据:${window.location.origin}${window.location.pathname}${source.historyHash}`
].join('\n'))
].join('\n');
}
async function copyText(value: string, label: string) {
try {
await navigator.clipboard.writeText(value);
@@ -395,12 +424,22 @@ async function copyText(value: string, label: string) {
export function OpsQuality() {
const [health, setHealth] = useState<OpsHealth | null>(null);
const [sourceReadiness, setSourceReadiness] = useState<SourceReadinessPlan | null>(null);
const [loading, setLoading] = useState(true);
const load = () => {
setLoading(true);
api.opsHealth()
.then(setHealth)
Promise.all([
api.opsHealth(),
api.sourceReadiness().catch((error: Error) => {
Toast.error(error.message);
return null;
})
])
.then(([nextHealth, nextSourceReadiness]) => {
setHealth(nextHealth);
setSourceReadiness(normalizeSourceReadiness(nextSourceReadiness));
})
.catch((error: Error) => Toast.error(error.message))
.finally(() => setLoading(false));
};
@@ -477,6 +516,53 @@ export function OpsQuality() {
</Space>
</Card>
<Card
bordered
title={<Space><span></span><Button size="small" icon={<IconCopy />} disabled={!sourceReadiness} onClick={() => copyText(sourceReadinessReport(sourceReadiness), '数据源生产就绪度')}></Button></Space>}
loading={loading}
style={{ marginTop: 16 }}
>
<Table<SourceReadinessRow>
pagination={false}
dataSource={sourceReadiness?.sources ?? []}
rowKey={(row?: SourceReadinessRow) => row?.protocol ?? ''}
columns={[
{
title: '来源',
width: 150,
render: (_: unknown, row: SourceReadinessRow) => (
<Space vertical align="start" spacing={4}>
<Tag color={row.severity === 'error' ? 'red' : row.severity === 'warning' ? 'orange' : 'green'}>{row.protocol}</Tag>
<Typography.Text type="secondary">{row.role}</Typography.Text>
</Space>
)
},
{
title: '就绪状态',
width: 150,
render: (_: unknown, row: SourceReadinessRow) => <Tag color={row.severity === 'error' ? 'red' : row.severity === 'warning' ? 'orange' : 'green'}>{row.status}</Tag>
},
{ title: '在线率', width: 100, render: (_: unknown, row: SourceReadinessRow) => formatRate(row.onlineRate) },
{ title: '在线/总数', width: 120, render: (_: unknown, row: SourceReadinessRow) => `${formatNumber(row.online)} / ${formatNumber(row.total)}` },
{ title: '缺失车辆', width: 110, render: (_: unknown, row: SourceReadinessRow) => formatNumber(row.missingVehicles) },
{ title: '证据', dataIndex: 'evidence' },
{ title: '建议动作', dataIndex: 'action' },
{
title: '入口',
width: 220,
render: (_: unknown, row: SourceReadinessRow) => (
<Space wrap>
<Button size="small" onClick={() => { window.location.hash = row.realtimeHash; }}></Button>
<Button size="small" onClick={() => { window.location.hash = row.vehiclesHash; }}></Button>
<Button size="small" onClick={() => { window.location.hash = row.historyHash; }}>RAW</Button>
<Button size="small" onClick={() => { window.location.hash = row.alertHash; }}></Button>
</Space>
)
}
]}
/>
</Card>
<Card bordered title="消息链路积压" loading={loading} style={{ marginTop: 16 }}>
<Table<CapacityMetricRow>
pagination={false}