feat(platform): add source readiness operations view
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user