refine Semi UI operations health workspace
This commit is contained in:
@@ -170,17 +170,23 @@ test('reconciles service identities with bound and identity-required vehicles',
|
||||
}
|
||||
expect(screen.getByText('服务身份 / 在线')).toBeInTheDocument();
|
||||
expect(screen.getByText('已绑定 1024 · 待绑定 11')).toBeInTheDocument();
|
||||
expect(screen.getByText('服务身份 / 在线').closest('.semi-card')).toHaveClass('v2-ops-kpi-card');
|
||||
expect(screen.getByRole('heading', { name: '运行概览', level: 5 })).toBeInTheDocument();
|
||||
expect(screen.getByText('服务身份 / 在线').closest('.v2-ops-metric')).toBeInTheDocument();
|
||||
expect(screen.getByRole('list', { name: '运行健康指标' })).toHaveClass('v2-ops-metric-rail');
|
||||
expect(screen.getByText('全部正常')).toBeInTheDocument();
|
||||
expect(screen.getByText('运行时安全').closest('.semi-card')).toHaveClass('v2-ops-runtime');
|
||||
expect(screen.getByText('协议来源就绪度').closest('.semi-card')).toHaveClass('v2-ops-sources');
|
||||
expect(screen.getByRole('heading', { name: '数据链路', level: 5 })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: '运行时安全', level: 5 })).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: '协议来源就绪度', level: 5 })).toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-ops-link-card.semi-card')).toHaveTextContent('MySQL主库连接正常');
|
||||
expect(document.querySelector('.v2-ops-link-card')).toHaveTextContent('MySQL主库连接正常');
|
||||
expect(document.querySelector('.v2-ops-link-card.semi-card')).not.toBeInTheDocument();
|
||||
expect(document.querySelector('.v2-ops-runtime-descriptions.semi-descriptions')).toBeInTheDocument();
|
||||
expect(screen.getByText('容量检查通过').closest('.semi-empty')).toHaveClass('v2-ops-capacity-empty');
|
||||
expect(document.querySelector('.v2-ops-source-list.semi-card-group')).toBeInTheDocument();
|
||||
expect(screen.getByText('GB32960').closest('.semi-card')).toHaveClass('v2-ops-source-card');
|
||||
expect(screen.getByLabelText('GB32960 在线覆盖率')).toBeInTheDocument();
|
||||
expect(screen.getByText('22.9% 在线')).toBeInTheDocument();
|
||||
expect(screen.getByText('验收标准').closest('.semi-card')).toHaveClass('v2-ops-source-card');
|
||||
expect(screen.queryByText('单车多来源诊断')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('tab', { name: '单车诊断' }));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IconRefresh, IconSearch, IconTickCircle } from '@douyinfe/semi-icons';
|
||||
import { Button, Card, CardGroup, Checkbox, Descriptions, Empty, Input, SideSheet, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { Button, Card, CardGroup, Checkbox, Descriptions, Empty, Input, Progress, SideSheet, Spin, Table, Tag, Typography } from '@douyinfe/semi-ui';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { FormEvent, useDeferredValue, useEffect, useState } from 'react';
|
||||
import { api } from '../../api/client';
|
||||
@@ -36,6 +36,24 @@ function number(value?: number, digits = 1) {
|
||||
return value == null || !Number.isFinite(value) ? '—' : value.toLocaleString('zh-CN', { maximumFractionDigits: digits });
|
||||
}
|
||||
|
||||
function OpsMetric({ label, value, detail, tone = 'neutral' }: {
|
||||
label: string;
|
||||
value: string;
|
||||
detail: string;
|
||||
tone?: 'neutral' | 'success' | 'warning' | 'danger';
|
||||
}) {
|
||||
return <article className={`v2-ops-metric is-${tone}`} role="listitem">
|
||||
<small>{label}</small>
|
||||
<strong>{value}</strong>
|
||||
<span>{detail}</span>
|
||||
</article>;
|
||||
}
|
||||
|
||||
function sourceOnlineRate(online: number, total: number, onlineRate?: number) {
|
||||
if (onlineRate != null && Number.isFinite(onlineRate)) return Math.max(0, Math.min(100, onlineRate));
|
||||
return total > 0 ? Math.max(0, Math.min(100, (online / total) * 100)) : 0;
|
||||
}
|
||||
|
||||
function SourcePolicyEditor({ vin, source, diagnostic, editable, onSaved }: {
|
||||
vin: string;
|
||||
source: VehicleLocationSourceEvidence;
|
||||
@@ -316,6 +334,11 @@ export default function OperationsPage() {
|
||||
const readiness = useQuery({ queryKey: ['ops-source-readiness-v2'], queryFn: ({ signal }) => api.sourceReadiness(signal), refetchInterval: 30_000, staleTime: 15_000, gcTime: QUERY_MEMORY.summaryGcTime, ...LIVE_QUERY_POLICY });
|
||||
const data = health.data; const sources = readiness.data;
|
||||
const refresh = () => Promise.all([health.refetch(), readiness.refetch()]);
|
||||
const linkIssueCount = data?.linkHealth.filter((item) => item.status !== 'ok').length ?? 0;
|
||||
const runtimeIssueCount = data ? Number(!data.mysqlWritable) + Number(!data.tdengineWritable) + Number(data.runtime.dataMode !== 'production') : 0;
|
||||
const capacityIssueCount = data?.capacityFindings.length ?? 0;
|
||||
const healthIssueCount = linkIssueCount + runtimeIssueCount + capacityIssueCount + Number((data?.kafkaLag ?? 0) > 0);
|
||||
const overallStatus = !data ? 'unknown' : healthIssueCount > 0 ? 'warning' : 'ok';
|
||||
return <div className="v2-ops-page">
|
||||
<PageHeader
|
||||
title="运维质量"
|
||||
@@ -343,24 +366,35 @@ export default function OperationsPage() {
|
||||
{workspace === 'health' ? <>
|
||||
{health.isError ? <InlineError message={health.error.message} onRetry={refresh} /> : null}
|
||||
{readiness.isError ? <InlineError message={readiness.error instanceof Error ? readiness.error.message : '协议来源就绪度读取失败'} onRetry={() => readiness.refetch()} /> : null}
|
||||
<section className="v2-ops-kpis">
|
||||
<Card className="v2-ops-kpi-card" bodyStyle={{ padding: 0 }}><small>运行模式</small><strong>{data?.runtime.dataMode === 'production' ? '生产数据' : '待确认'}</strong><HealthTag status={data?.runtime.dataMode === 'production' ? 'ok' : 'error'}>{data?.runtime.platformRelease ? '版本已注入' : '缺少版本'}</HealthTag></Card>
|
||||
<Card className="v2-ops-kpi-card" bodyStyle={{ padding: 0 }}><small>活跃连接</small><strong>{data?.activeConnections?.toLocaleString('zh-CN') ?? '—'}</strong><span>网关实时连接</span></Card>
|
||||
<Card className="v2-ops-kpi-card" bodyStyle={{ padding: 0 }}><small>Kafka Lag</small><strong>{data?.kafkaLag?.toLocaleString('zh-CN') ?? '—'}</strong><HealthTag status={data?.kafkaLag === 0 ? 'ok' : 'warning'}>{data?.kafkaLag === 0 ? '已回零' : '需检查'}</HealthTag></Card>
|
||||
<Card className="v2-ops-kpi-card" bodyStyle={{ padding: 0 }}><small>Redis 在线 Key</small><strong>{data?.redisOnlineKeys?.toLocaleString('zh-CN') ?? '—'}</strong><span>在线状态快照</span></Card>
|
||||
<Card className="v2-ops-kpi-card" bodyStyle={{ padding: 0 }}><small>服务身份 / 在线</small><strong>{sources ? `${sources.totalVehicles} / ${sources.onlineVehicles}` : '—'}</strong><span>{sources ? `已绑定 ${sources.boundVehicles} · 待绑定 ${sources.identityRequiredVehicles}` : '档案与快照并集'}</span></Card>
|
||||
</section>
|
||||
<div className="v2-ops-grid"><Card className="v2-ops-panel v2-ops-links" bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="数据链路" meta="15 秒自动刷新" /><div className="v2-ops-link-list">{data?.linkHealth.length ? data.linkHealth.map((item) => <Card className={`v2-ops-link-card is-${item.status}`} key={item.name} bodyStyle={{ padding: 0 }}><span className="v2-ops-link-status"><i /><strong>{item.name}</strong></span><p>{item.detail || '无补充信息'}</p><HealthTag status={item.status} /></Card>) : <Empty className="v2-ops-link-empty" title={data ? '暂无链路探针' : '正在读取链路状态'} description={data ? '当前响应没有可展示的数据链路证据。' : '全局健康数据返回后将在这里更新。'} />}</div></Card>
|
||||
<Card className="v2-ops-panel v2-ops-runtime" bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="运行时安全" /><Descriptions className="v2-ops-runtime-descriptions" align="left" size="small" data={[
|
||||
<Card className="v2-ops-overview" bodyStyle={{ padding: 0 }}>
|
||||
<WorkspacePanelHeader
|
||||
title="运行概览"
|
||||
description="关键链路、基础设施与车辆覆盖的同一健康快照"
|
||||
meta={<span className="v2-ops-overview-meta"><HealthTag status={overallStatus}>{!data ? '正在读取' : healthIssueCount > 0 ? `${healthIssueCount} 项需关注` : '全部正常'}</HealthTag><Typography.Text type="tertiary">15 秒自动刷新</Typography.Text></span>}
|
||||
/>
|
||||
<section className="v2-ops-metric-rail" role="list" aria-label="运行健康指标">
|
||||
<OpsMetric label="整体状态" value={!data ? '读取中' : healthIssueCount > 0 ? '需要处理' : '运行正常'} detail={!data ? '等待服务端健康证据' : healthIssueCount > 0 ? `${healthIssueCount} 项证据需要关注` : '链路与写入探针均正常'} tone={!data ? 'neutral' : healthIssueCount > 0 ? 'warning' : 'success'} />
|
||||
<OpsMetric label="活跃连接" value={data?.activeConnections?.toLocaleString('zh-CN') ?? '—'} detail="网关实时连接" />
|
||||
<OpsMetric label="Kafka Lag" value={data?.kafkaLag?.toLocaleString('zh-CN') ?? '—'} detail={data?.kafkaLag === 0 ? '消费积压已回零' : data ? '存在待消费消息' : '等待消费证据'} tone={data?.kafkaLag === 0 ? 'success' : data ? 'warning' : 'neutral'} />
|
||||
<OpsMetric label="Redis 在线 Key" value={data?.redisOnlineKeys?.toLocaleString('zh-CN') ?? '—'} detail="在线状态快照" />
|
||||
<OpsMetric label="服务身份 / 在线" value={sources ? `${sources.totalVehicles} / ${sources.onlineVehicles}` : '—'} detail={sources ? `已绑定 ${sources.boundVehicles} · 待绑定 ${sources.identityRequiredVehicles}` : '档案与快照并集'} />
|
||||
</section>
|
||||
</Card>
|
||||
<div className="v2-ops-grid"><Card className="v2-ops-panel v2-ops-links" bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="数据链路" description="按链路查看当前状态与服务端诊断证据" meta={<HealthTag status={!data ? 'unknown' : linkIssueCount > 0 ? 'warning' : 'ok'}>{!data ? '读取中' : linkIssueCount > 0 ? `${linkIssueCount} 条异常` : '链路正常'}</HealthTag>} /><div className="v2-ops-link-list" role="list">{data?.linkHealth.length ? data.linkHealth.map((item) => <article className={`v2-ops-link-card is-${item.status}`} key={item.name} role="listitem"><span className="v2-ops-link-status"><i /><strong>{item.name}</strong></span><p>{item.detail || '无补充信息'}</p><HealthTag status={item.status} /></article>) : <Empty className="v2-ops-link-empty" title={data ? '暂无链路探针' : '正在读取链路状态'} description={data ? '当前响应没有可展示的数据链路证据。' : '全局健康数据返回后将在这里更新。'} />}</div></Card>
|
||||
<Card className="v2-ops-panel v2-ops-runtime" bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="运行时安全" description="生产模式、写入探针与代理配置" meta={<HealthTag status={!data ? 'unknown' : runtimeIssueCount + capacityIssueCount > 0 ? 'warning' : 'ok'}>{!data ? '读取中' : runtimeIssueCount + capacityIssueCount > 0 ? `${runtimeIssueCount + capacityIssueCount} 项关注` : '配置正常'}</HealthTag>} /><Descriptions className="v2-ops-runtime-descriptions" align="left" size="small" data={[
|
||||
{ key: '生产数据模式', value: <HealthTag status={data?.runtime.dataMode === 'production' ? 'ok' : 'error'}>{data?.runtime.dataMode === 'production' ? '已启用' : '未启用'}</HealthTag> },
|
||||
{ key: 'MySQL 写探针', value: <HealthTag status={data?.mysqlWritable ? 'ok' : 'error'}>{data?.mysqlWritable ? '正常' : '异常'}</HealthTag> },
|
||||
{ key: 'TDengine 写探针', value: <HealthTag status={data?.tdengineWritable ? 'ok' : 'error'}>{data?.tdengineWritable ? '正常' : '异常'}</HealthTag> },
|
||||
{ key: '请求超时', value: `${data?.runtime.requestTimeoutMs ?? '—'} ms` },
|
||||
{ key: '高德安全代理', value: <HealthTag status={data?.runtime.amapSecurityProxyEnabled && !data?.runtime.amapSecurityCodeExposed ? 'ok' : 'warning'}>{data?.runtime.amapSecurityProxyEnabled ? '服务端代理' : '未启用'}</HealthTag> }
|
||||
]} />{data?.capacityFindings?.length ? <div className="v2-ops-capacity-findings">{data.capacityFindings.map((item) => <Card className="v2-ops-capacity-finding" key={item}><HealthTag status="warning">待处理</HealthTag><p>{item}</p></Card>)}</div> : <Empty className="v2-ops-capacity-empty" image={<IconTickCircle />} title="容量检查通过" description="当前没有需要处理的容量风险。" />}</Card></div>
|
||||
<Card className="v2-ops-panel v2-ops-sources" bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="协议来源就绪度" description="验收口径与处置建议" />{sources ? sources.sources.length ? <CardGroup className="v2-ops-source-list" type="grid" spacing={0}>{sources.sources.map((source) => <Card className={`v2-ops-source-card is-${source.severity}`} key={source.protocol} title={<span className="v2-ops-source-title"><Tag color="blue" type="light" size="small">{source.protocol}</Tag><small>{source.role}</small></span>} headerExtraContent={<HealthTag status={source.severity}>{`${source.online} / ${source.total} 在线`}</HealthTag>} headerLine>
|
||||
<Card className="v2-ops-panel v2-ops-sources" bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="协议来源就绪度" description="在线覆盖、当前证据、处置动作与验收口径" meta={<Tag color="blue" type="light" size="small">{sources ? `${sources.sources.length} 个协议` : '正在读取'}</Tag>} />{sources ? sources.sources.length ? <CardGroup className="v2-ops-source-list" type="grid" spacing={0}>{sources.sources.map((source) => {
|
||||
const onlineRate = sourceOnlineRate(source.online, source.total, source.onlineRate);
|
||||
return <Card className={`v2-ops-source-card is-${source.severity}`} key={source.protocol} title={<span className="v2-ops-source-title"><Tag color="blue" type="light" size="small">{source.protocol}</Tag><small>{source.role}</small></span>} headerExtraContent={<HealthTag status={source.severity}>{source.status || statusLabel(source.severity)}</HealthTag>} headerLine>
|
||||
<div className="v2-ops-source-coverage"><span><strong>在线覆盖</strong><b>{source.online.toLocaleString('zh-CN')} / {source.total.toLocaleString('zh-CN')}</b></span><Progress aria-label={`${source.protocol} 在线覆盖率`} percent={onlineRate} showInfo={false} stroke="var(--v2-blue)" /><small>{number(onlineRate)}% 在线{source.missingVehicles ? ` · ${source.missingVehicles.toLocaleString('zh-CN')} 辆待恢复` : ''}</small></div>
|
||||
<div className="v2-ops-source-evidence"><strong>当前证据</strong><p>{source.evidence}</p></div><div className="v2-ops-source-action"><strong>建议动作</strong><p>{source.action}</p></div><footer><Tag color="green" type="light" size="small">验收标准</Tag><span>{source.acceptance}</span></footer>
|
||||
</Card>)}</CardGroup> : <Empty className="v2-ops-source-empty" title="暂无协议来源" description="当前响应没有可展示的协议来源就绪度。" /> : <div className="v2-ops-source-loading" role="status"><Spin size="middle" tip="正在读取协议来源就绪度" /></div>}</Card>
|
||||
</Card>;
|
||||
})}</CardGroup> : <Empty className="v2-ops-source-empty" title="暂无协议来源" description="当前响应没有可展示的协议来源就绪度。" /> : <div className="v2-ops-source-loading" role="status"><Spin size="middle" tip="正在读取协议来源就绪度" /></div>}</Card>
|
||||
</> : null}
|
||||
</section>
|
||||
</div>;
|
||||
|
||||
Reference in New Issue
Block a user