feat(platform): separate ops quality as evidence layer
This commit is contained in:
@@ -75,6 +75,33 @@ type OpsActionItem = {
|
|||||||
acceptance: string;
|
acceptance: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const customerEvidencePaths = [
|
||||||
|
{
|
||||||
|
title: '车辆不在线',
|
||||||
|
action: '看实时写入',
|
||||||
|
evidence: 'Redis 在线 Key、快速写入 pending、NATS ACK',
|
||||||
|
detail: '解释客户看到车辆离线时,是否是实时帧、在线 TTL 或快速写入链路中断。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '轨迹缺数据',
|
||||||
|
action: '看历史消费',
|
||||||
|
evidence: 'Kafka Lag、历史批次、历史行待写',
|
||||||
|
detail: '解释轨迹回放或历史明细缺失时,数据是否卡在 Kafka、TDengine 或批量写入。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '无法导出',
|
||||||
|
action: '看存储/API',
|
||||||
|
evidence: 'TDengine、MySQL、请求超时、运行版本',
|
||||||
|
detail: '解释客户导出失败、查询慢或字段缺失时,核心存储和 API 是否可用。'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '地图不可用',
|
||||||
|
action: '看高德配置',
|
||||||
|
evidence: 'Web JS Key、服务端 API Key、安全代理',
|
||||||
|
detail: '解释实时地图、轨迹回放和地理能力不可用时,地图配置是否完整且安全。'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
const emptyCapacityMetrics: CapacityMetrics = {
|
const emptyCapacityMetrics: CapacityMetrics = {
|
||||||
activeConnections: 0,
|
activeConnections: 0,
|
||||||
kafkaLag: 0,
|
kafkaLag: 0,
|
||||||
@@ -457,8 +484,8 @@ export function OpsQuality() {
|
|||||||
return (
|
return (
|
||||||
<div className="vp-page">
|
<div className="vp-page">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="运维质量"
|
title="运维证据层"
|
||||||
description="集中查看网关、消息队列、缓存、时序库、关系库、地图配置和运行版本状态"
|
description="客户主流程外的链路、容量、存储和地图配置证据,用来解释车辆实时、轨迹、统计、导出或告警为何不可用。"
|
||||||
actions={(
|
actions={(
|
||||||
<Space>
|
<Space>
|
||||||
<Button size="small" icon={<IconCopy />} aria-label="复制容量交接" disabled={!health} onClick={() => copyText(opsCapacityHandoffText(health), '运维容量交接')}>复制容量交接</Button>
|
<Button size="small" icon={<IconCopy />} aria-label="复制容量交接" disabled={!health} onClick={() => copyText(opsCapacityHandoffText(health), '运维容量交接')}>复制容量交接</Button>
|
||||||
@@ -468,6 +495,32 @@ export function OpsQuality() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Card bordered className="vp-ops-evidence-layer-card">
|
||||||
|
<div className="vp-ops-evidence-layer-head">
|
||||||
|
<div>
|
||||||
|
<Typography.Title heading={5} style={{ margin: 0 }}>客户问题解释路径</Typography.Title>
|
||||||
|
<Typography.Text type="secondary">运维证据只在客户问题需要解释时打开,默认不参与车辆服务主流程。</Typography.Text>
|
||||||
|
</div>
|
||||||
|
<Tag color="blue">证据层</Tag>
|
||||||
|
</div>
|
||||||
|
<div className="vp-ops-evidence-path-grid">
|
||||||
|
{customerEvidencePaths.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.title}
|
||||||
|
type="button"
|
||||||
|
className="vp-ops-evidence-path-item"
|
||||||
|
aria-label={`客户问题解释路径 ${item.title} ${item.action}`}
|
||||||
|
onClick={() => Toast.info(item.evidence)}
|
||||||
|
>
|
||||||
|
<span className="vp-ops-evidence-path-title">{item.title}</span>
|
||||||
|
<span className="vp-ops-evidence-path-action">{item.action}</span>
|
||||||
|
<span className="vp-ops-evidence-path-evidence">{item.evidence}</span>
|
||||||
|
<span className="vp-ops-evidence-path-detail">{item.detail}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div className="vp-kpi-grid">
|
<div className="vp-kpi-grid">
|
||||||
<Card bordered loading={loading}>
|
<Card bordered loading={loading}>
|
||||||
<div className="vp-kpi-value">{formatNumber(health?.kafkaLag)}</div>
|
<div className="vp-kpi-value">{formatNumber(health?.kafkaLag)}</div>
|
||||||
|
|||||||
@@ -5624,6 +5624,69 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
line-height: 32px;
|
line-height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vp-ops-evidence-layer-card {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-ops-evidence-layer-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-ops-evidence-path-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-ops-evidence-path-item {
|
||||||
|
min-height: 168px;
|
||||||
|
padding: 14px;
|
||||||
|
border: 1px solid var(--vp-border);
|
||||||
|
border-radius: var(--vp-radius);
|
||||||
|
background: #fbfcff;
|
||||||
|
color: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-ops-evidence-path-item:hover {
|
||||||
|
border-color: var(--vp-primary);
|
||||||
|
box-shadow: 0 10px 24px rgba(21, 101, 216, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-ops-evidence-path-title {
|
||||||
|
color: var(--vp-text);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-ops-evidence-path-action {
|
||||||
|
color: var(--vp-primary);
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-ops-evidence-path-evidence {
|
||||||
|
color: var(--vp-text);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vp-ops-evidence-path-detail {
|
||||||
|
color: var(--vp-text-muted);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.vp-risk-runbook {
|
.vp-risk-runbook {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
@@ -9865,6 +9928,7 @@ button.vp-realtime-command-item:focus-visible {
|
|||||||
.vp-notification-reachability-grid,
|
.vp-notification-reachability-grid,
|
||||||
.vp-sla-summary,
|
.vp-sla-summary,
|
||||||
.vp-risk-grid,
|
.vp-risk-grid,
|
||||||
|
.vp-ops-evidence-path-grid,
|
||||||
.vp-ops-action-grid,
|
.vp-ops-action-grid,
|
||||||
.vp-alert-ops-grid,
|
.vp-alert-ops-grid,
|
||||||
.vp-stat-workspace,
|
.vp-stat-workspace,
|
||||||
|
|||||||
@@ -4391,7 +4391,7 @@ test('renders notification rules as a standalone operations page', async () => {
|
|||||||
expect(window.location.hash).toBe('#/detail?keyword=VIN-RULES-001');
|
expect(window.location.hash).toBe('#/detail?keyword=VIN-RULES-001');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('renders ops quality as a standalone runtime health page', async () => {
|
test('renders ops quality as a separated evidence layer for customer vehicle service', async () => {
|
||||||
window.history.replaceState(null, '', '/#/ops-quality');
|
window.history.replaceState(null, '', '/#/ops-quality');
|
||||||
const writeText = vi.fn(() => Promise.resolve());
|
const writeText = vi.fn(() => Promise.resolve());
|
||||||
Object.defineProperty(navigator, 'clipboard', {
|
Object.defineProperty(navigator, 'clipboard', {
|
||||||
@@ -4512,7 +4512,14 @@ test('renders ops quality as a standalone runtime health page', async () => {
|
|||||||
|
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
expect(await screen.findByRole('heading', { name: '运维质量' })).toBeInTheDocument();
|
expect(await screen.findByRole('heading', { name: '运维证据层' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('客户主流程外的链路、容量、存储和地图配置证据,用来解释车辆实时、轨迹、统计、导出或告警为何不可用。')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('客户问题解释路径')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: '客户问题解释路径 车辆不在线 看实时写入' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: '客户问题解释路径 轨迹缺数据 看历史消费' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: '客户问题解释路径 无法导出 看存储/API' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: '客户问题解释路径 地图不可用 看高德配置' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('运维证据只在客户问题需要解释时打开,默认不参与车辆服务主流程。')).toBeInTheDocument();
|
||||||
expect(screen.getByText('platform-ops-test')).toBeInTheDocument();
|
expect(screen.getByText('platform-ops-test')).toBeInTheDocument();
|
||||||
expect(screen.getAllByText('42').length).toBeGreaterThanOrEqual(1);
|
expect(screen.getAllByText('42').length).toBeGreaterThanOrEqual(1);
|
||||||
expect(screen.getAllByText('1,234').length).toBeGreaterThanOrEqual(1);
|
expect(screen.getAllByText('1,234').length).toBeGreaterThanOrEqual(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user