refine Semi UI operations health hierarchy
This commit is contained in:
@@ -53,7 +53,7 @@ function seedSession() {
|
||||
test('renders reconciliation queue, loads evidence on demand and records review conclusion', async () => {
|
||||
seedSession();
|
||||
mocks.opsHealth.mockResolvedValue({
|
||||
linkHealth: [{ name: 'MySQL', status: 'ok', detail: '主库连接正常' }], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5,
|
||||
linkHealth: [{ name: 'MySQL realtime', status: 'ok', detail: '主库连接正常' }], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5,
|
||||
tdengineWritable: true, mysqlWritable: true,
|
||||
runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false }
|
||||
});
|
||||
@@ -179,7 +179,7 @@ test('renders only the compact mobile reconciliation surface and defers secondar
|
||||
test('reconciles service identities with bound and identity-required vehicles', async () => {
|
||||
seedSession();
|
||||
mocks.opsHealth.mockResolvedValue({
|
||||
linkHealth: [{ name: 'MySQL', status: 'ok', detail: '主库连接正常' }], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5,
|
||||
linkHealth: [{ name: 'MySQL realtime', status: 'ok', detail: '主库连接正常' }], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5,
|
||||
tdengineWritable: true, mysqlWritable: true,
|
||||
runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false }
|
||||
});
|
||||
@@ -205,6 +205,8 @@ test('reconciles service identities with bound and identity-required vehicles',
|
||||
expect(liveOptions.refetchOnWindowFocus).toBe(true);
|
||||
}
|
||||
expect(screen.getByText('车辆总数 / 在线')).toBeInTheDocument();
|
||||
expect(screen.getByText('协议就绪 / 总数')).toBeInTheDocument();
|
||||
expect(screen.getByText('全部协议达到当前口径')).toBeInTheDocument();
|
||||
expect(screen.getByText('已绑定 1024 · 待绑定 11')).toBeInTheDocument();
|
||||
expect(screen.getByRole('heading', { name: '运行概览', level: 5 })).toBeInTheDocument();
|
||||
expect(screen.getByText('车辆总数 / 在线').closest('.v2-ops-metric')).toBeInTheDocument();
|
||||
@@ -215,7 +217,7 @@ test('reconciles service identities with bound and identity-required vehicles',
|
||||
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')).toHaveTextContent('MySQL主库连接正常');
|
||||
expect(document.querySelector('.v2-ops-link-card')).toHaveTextContent('MySQL 实时连接MySQL realtime主库连接正常');
|
||||
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');
|
||||
@@ -236,6 +238,42 @@ test('reconciles service identities with bound and identity-required vehicles',
|
||||
expect(screen.queryByText('统一车辆视角')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('includes protocol readiness in the global health verdict and prioritizes its evidence', async () => {
|
||||
seedSession();
|
||||
mocks.opsHealth.mockResolvedValue({
|
||||
linkHealth: [{ name: 'MySQL realtime', status: 'ok', detail: '主库连接正常' }], kafkaLag: 0, activeConnections: 10, capacityFindings: [], redisOnlineKeys: 5,
|
||||
tdengineWritable: true, mysqlWritable: true,
|
||||
runtime: { platformRelease: 'test-release', dataMode: 'production', requestTimeoutMs: 5000, amapSecurityProxyEnabled: true, amapSecurityCodeExposed: false }
|
||||
});
|
||||
mocks.sourceReadiness.mockResolvedValue({
|
||||
totalVehicles: 1024, boundVehicles: 1024, identityRequiredVehicles: 11, onlineVehicles: 234,
|
||||
sources: [
|
||||
{
|
||||
protocol: 'GB32960', role: '仪表盘里程与整车状态', total: 494, online: 123, severity: 'warning',
|
||||
status: '覆盖不足', evidence: '在线覆盖偏低', action: '核对接入范围', acceptance: '缺失车辆下降'
|
||||
},
|
||||
{
|
||||
protocol: 'YUTONG_MQTT', role: '宇通补充来源', total: 50, online: 12, severity: 'error',
|
||||
status: '链路异常', evidence: '投影停滞', action: '恢复投影与回补', acceptance: '日里程恢复'
|
||||
}
|
||||
]
|
||||
});
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||
renderOperations(client, ['/operations?view=health']);
|
||||
|
||||
expect(await screen.findByText('需要处理')).toBeInTheDocument();
|
||||
expect(screen.getByText('协议覆盖 2 项 · 链路运行 0 项')).toBeInTheDocument();
|
||||
expect(screen.getByText('0 / 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('2 个协议需要处置')).toBeInTheDocument();
|
||||
expect(within(screen.getByRole('navigation', { name: '运维质量视图' })).getByText('2 项关注')).toBeInTheDocument();
|
||||
const sourcePanel = screen.getByRole('heading', { name: '协议来源就绪度', level: 5 }).closest('.v2-ops-sources');
|
||||
const infrastructureGrid = document.querySelector('.v2-ops-grid');
|
||||
expect(sourcePanel).toHaveClass('has-attention');
|
||||
expect(sourcePanel).not.toBeNull();
|
||||
expect(infrastructureGrid).not.toBeNull();
|
||||
expect(Boolean(sourcePanel!.compareDocumentPosition(infrastructureGrid!) & Node.DOCUMENT_POSITION_FOLLOWING)).toBe(true);
|
||||
});
|
||||
|
||||
test('keeps health evidence visible when source readiness fails and retries that section', async () => {
|
||||
seedSession();
|
||||
mocks.opsHealth.mockResolvedValue({
|
||||
@@ -252,6 +290,8 @@ test('keeps health evidence visible when source readiness fails and retries that
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: '全局健康' }));
|
||||
expect(await screen.findByText('来源就绪度暂时不可用')).toBeInTheDocument();
|
||||
expect(screen.getByText('协议来源证据不可用')).toBeInTheDocument();
|
||||
expect(screen.getByText('证据缺失')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('运维质量操作')).toHaveTextContent('上海时间 · 版本 test-release');
|
||||
expect(screen.getByRole('heading', { name: '数据链路', level: 5 })).toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { name: /重试/ }));
|
||||
|
||||
@@ -24,6 +24,20 @@ function statusLabel(status: string) {
|
||||
return { ok: '正常', warning: '关注', error: '异常' }[status] ?? status;
|
||||
}
|
||||
|
||||
const opsLinkLabels: Record<string, string> = {
|
||||
'MySQL realtime': 'MySQL 实时连接',
|
||||
vehicle_realtime_snapshot: '车辆实时快照',
|
||||
vehicle_realtime_location: '车辆实时位置',
|
||||
'TDengine raw_frames': 'TDengine 原始帧',
|
||||
'Capacity check': '容量与消费积压',
|
||||
'Redis online keys': 'Redis 在线状态',
|
||||
'Alert Kafka stream': '告警消费链路'
|
||||
};
|
||||
|
||||
function opsLinkLabel(name: string) {
|
||||
return opsLinkLabels[name] ?? name;
|
||||
}
|
||||
|
||||
function HealthTag({ status, children }: { status: string; children?: string }) {
|
||||
const color = status === 'ok' ? 'green' : status === 'warning' ? 'orange' : status === 'error' ? 'red' : 'grey';
|
||||
return <Tag className={`v2-ops-health-tag is-${status}`} color={color} type="light" size="small"><i />{children ?? statusLabel(status)}</Tag>;
|
||||
@@ -345,8 +359,34 @@ export default function OperationsPage() {
|
||||
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';
|
||||
const infrastructureIssueCount = linkIssueCount + runtimeIssueCount + capacityIssueCount + Number((data?.kafkaLag ?? 0) > 0);
|
||||
const sourceIssueCount = sources?.sources.filter((item) => item.severity !== 'ok').length ?? 0;
|
||||
const sourceErrorCount = sources?.sources.filter((item) => item.severity === 'error').length ?? 0;
|
||||
const sourceHealthyCount = sources?.sources.filter((item) => item.severity === 'ok').length ?? 0;
|
||||
const readinessUnavailable = readiness.isError;
|
||||
const healthIssueCount = infrastructureIssueCount + sourceIssueCount + Number(readinessUnavailable);
|
||||
const healthEvidencePending = !data || readiness.isPending;
|
||||
const overallStatus = healthEvidencePending
|
||||
? 'unknown'
|
||||
: readinessUnavailable || sourceErrorCount > 0
|
||||
? 'error'
|
||||
: healthIssueCount > 0
|
||||
? 'warning'
|
||||
: 'ok';
|
||||
const overallValue = healthEvidencePending
|
||||
? '读取中'
|
||||
: readinessUnavailable
|
||||
? '证据缺失'
|
||||
: healthIssueCount > 0
|
||||
? '需要处理'
|
||||
: '运行正常';
|
||||
const overallDetail = healthEvidencePending
|
||||
? '等待运行与协议覆盖证据'
|
||||
: readinessUnavailable
|
||||
? '协议来源就绪度暂不可用'
|
||||
: healthIssueCount > 0
|
||||
? `协议覆盖 ${sourceIssueCount} 项 · 链路运行 ${infrastructureIssueCount} 项`
|
||||
: '链路、写入与协议覆盖均正常';
|
||||
const sortedLinks = useMemo(() => [...(data?.linkHealth ?? [])].sort((left, right) => {
|
||||
const priority = { error: 0, warning: 1, ok: 2 } as Record<string, number>;
|
||||
return (priority[left.status] ?? 3) - (priority[right.status] ?? 3);
|
||||
@@ -365,8 +405,8 @@ export default function OperationsPage() {
|
||||
? { description: '按需读取单车来源', status: '精确诊断', color: 'cyan' as const }
|
||||
: {
|
||||
description: '15 秒刷新运行证据',
|
||||
status: !data ? '读取中' : healthIssueCount > 0 ? `${healthIssueCount} 项关注` : '运行正常',
|
||||
color: !data ? 'grey' as const : healthIssueCount > 0 ? 'orange' as const : 'green' as const
|
||||
status: healthEvidencePending ? '读取中' : readinessUnavailable ? '协议证据缺失' : healthIssueCount > 0 ? `${healthIssueCount} 项关注` : '运行正常',
|
||||
color: healthEvidencePending ? 'grey' as const : readinessUnavailable || sourceErrorCount > 0 ? 'red' as const : healthIssueCount > 0 ? 'orange' as const : 'green' as const
|
||||
};
|
||||
return <div className={`v2-ops-page is-${workspace}`}>
|
||||
<WorkspaceCommandBar
|
||||
@@ -410,14 +450,29 @@ export default function OperationsPage() {
|
||||
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={overallValue} detail={overallDetail} tone={healthEvidencePending ? 'neutral' : readinessUnavailable || sourceErrorCount > 0 ? 'danger' : healthIssueCount > 0 ? 'warning' : 'success'} />
|
||||
<OpsMetric
|
||||
label="协议就绪 / 总数"
|
||||
value={sources?.sources.length ? `${sourceHealthyCount} / ${sources.sources.length}` : readinessUnavailable ? '不可用' : '—'}
|
||||
detail={readinessUnavailable ? '来源证据读取失败' : sourceIssueCount > 0 ? `${sourceIssueCount} 个协议需要处置` : sources?.sources.length ? '全部协议达到当前口径' : '等待协议覆盖证据'}
|
||||
tone={readinessUnavailable || sourceErrorCount > 0 ? 'danger' : sourceIssueCount > 0 ? 'warning' : sources?.sources.length ? 'success' : 'neutral'}
|
||||
/>
|
||||
<OpsMetric label="车辆总数 / 在线" value={sources ? `${sources.totalVehicles} / ${sources.onlineVehicles}` : '—'} detail={sources ? `已绑定 ${sources.boundVehicles} · 待绑定 ${sources.identityRequiredVehicles}` : '档案与快照并集'} />
|
||||
<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="在线状态快照" />
|
||||
</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">{sortedLinks.length ? sortedLinks.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-sources${sourceIssueCount || readinessUnavailable ? ' has-attention' : ''}`} bodyStyle={{ padding: 0 }}><WorkspacePanelHeader title="协议来源就绪度" description="优先展示覆盖不足的协议,并给出处置动作与验收口径" meta={<HealthTag status={readinessUnavailable ? 'error' : sourceIssueCount > 0 ? 'warning' : sources ? 'ok' : 'unknown'}>{readinessUnavailable ? '证据不可用' : sources ? sourceIssueCount > 0 ? `${sourceIssueCount} 个协议需关注` : `${sources.sources.length} 个协议正常` : '正在读取'}</HealthTag>} />{sources ? sortedSources.length ? <CardGroup className="v2-ops-source-list" type="grid" spacing={0}>{sortedSources.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="当前响应没有可展示的协议来源就绪度。" /> : readinessUnavailable ? <Empty className="v2-ops-source-empty is-error" title="协议来源证据不可用" description="运行证据仍可查看;请使用上方重试恢复协议覆盖判断。" /> : <div className="v2-ops-source-loading" role="status"><Spin size="middle" tip="正在读取协议来源就绪度" /></div>}</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">{sortedLinks.length ? sortedLinks.map((item) => {
|
||||
const label = opsLinkLabel(item.name);
|
||||
return <article className={`v2-ops-link-card is-${item.status}`} key={item.name} role="listitem"><span className="v2-ops-link-status"><i /><span><strong>{label}</strong>{label !== item.name ? <small>{item.name}</small> : null}</span></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> },
|
||||
@@ -425,13 +480,6 @@ export default function OperationsPage() {
|
||||
{ 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="在线覆盖、当前证据、处置动作与验收口径" meta={<Tag color="blue" type="light" size="small">{sources ? `${sources.sources.length} 个协议` : '正在读取'}</Tag>} />{sources ? sortedSources.length ? <CardGroup className="v2-ops-source-list" type="grid" spacing={0}>{sortedSources.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>
|
||||
</> : null}
|
||||
</section>
|
||||
</div>;
|
||||
|
||||
@@ -15460,3 +15460,55 @@
|
||||
padding-inline: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Semi UI operations health attention hierarchy.
|
||||
* Protocol readiness is part of the global verdict, so its actionable
|
||||
* evidence now appears before healthy infrastructure detail.
|
||||
*/
|
||||
.v2-ops-sources.has-attention.semi-card {
|
||||
border-color: #e8d2a6;
|
||||
box-shadow: 0 9px 28px rgba(127, 88, 23, .075);
|
||||
}
|
||||
|
||||
.v2-ops-sources.has-attention > .semi-card-body > .v2-workspace-panel-header {
|
||||
background: linear-gradient(96deg, #fffaf0 0%, #fffdf9 54%, #fff 100%);
|
||||
}
|
||||
|
||||
.v2-ops-link-status > span {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.v2-ops-link-status > span > strong,
|
||||
.v2-ops-link-status > span > small {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.v2-ops-link-status > span > small {
|
||||
color: #91a0b2;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.v2-ops-source-empty.is-error.semi-empty {
|
||||
background: #fff9f8;
|
||||
}
|
||||
|
||||
.v2-ops-source-empty.is-error .semi-empty-title {
|
||||
color: #b94a45;
|
||||
}
|
||||
|
||||
@media (max-width: 680px) {
|
||||
.v2-ops-sources.has-attention.semi-card {
|
||||
box-shadow: 0 6px 20px rgba(127, 88, 23, .07);
|
||||
}
|
||||
|
||||
.v2-ops-link-status > span > small {
|
||||
font-size: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user