feat(node-detail): 添加主状态码、Top Domain 和已恢复事件统计信息

This commit is contained in:
ryan
2026-03-14 12:24:08 +08:00
parent 7ff642ca7f
commit 0ad26e3904
2 changed files with 112 additions and 7 deletions
@@ -76,11 +76,13 @@ function RiskSignal({
value,
tone,
hint,
href,
}: {
label: string;
value: number;
tone: 'danger' | 'warning' | 'info' | 'success';
hint: string;
href?: string;
}) {
const toneClass =
tone === 'danger'
@@ -91,25 +93,38 @@ function RiskSignal({
? 'border-emerald-400/30 bg-emerald-500/10 text-emerald-100'
: 'border-sky-400/30 bg-sky-500/10 text-sky-100';
return (
const content = (
<div className={`rounded-3xl border px-4 py-4 ${toneClass}`}>
<p className="text-xs tracking-[0.22em] uppercase opacity-75">{label}</p>
<p className="mt-3 text-3xl font-semibold">{value}</p>
<p className="mt-2 text-sm opacity-80">{hint}</p>
</div>
);
if (!href) {
return content;
}
return (
<Link
href={href}
className="block transition hover:opacity-90"
>
{content}
</Link>
);
}
function PeakCard({
label,
value,
hint,
href,
}: {
label: string;
value: string;
hint: string;
href?: string;
}) {
return (
const content = (
<div className="rounded-3xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
<p className="text-xs tracking-[0.22em] text-[var(--foreground-muted)] uppercase">
{label}
@@ -120,6 +135,17 @@ function PeakCard({
<p className="mt-2 text-sm text-[var(--foreground-secondary)]">{hint}</p>
</div>
);
if (!href) {
return content;
}
return (
<Link
href={href}
className="block transition hover:[&_div]:border-[var(--border-strong)] hover:[&_div]:bg-[var(--surface-muted)]"
>
{content}
</Link>
);
}
function formatPeakNode(node: DashboardPeakNode | null) {
@@ -135,6 +161,13 @@ function formatPeakNode(node: DashboardPeakNode | null) {
};
}
function buildNodeDetailHref(id?: number | null) {
if (!id) {
return '/node';
}
return `/node/detail?id=${id}`;
}
function OverviewMetric({
label,
value,
@@ -163,7 +196,10 @@ function OverviewMetric({
function NodeHealthRow({ node }: { node: DashboardNodeHealth }) {
return (
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
<Link
href={buildNodeDetailHref(node.id)}
className="block rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4 transition hover:border-[var(--border-strong)] hover:bg-[var(--surface-muted)]"
>
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-sm font-semibold text-[var(--foreground-primary)]">
@@ -229,7 +265,7 @@ function NodeHealthRow({ node }: { node: DashboardNodeHealth }) {
: ' 暂无'}
</p>
</div>
</div>
</Link>
);
}
@@ -263,6 +299,10 @@ export function DashboardOverview() {
);
}
const nodeIdMap = new Map(
overview.nodes.map((node) => [node.node_id, node.id] as const),
);
return (
<div className="space-y-6">
<AppCard
@@ -326,12 +366,14 @@ export function DashboardOverview() {
value={overview.risk.lagging_nodes}
tone="info"
hint="当前版本未追平全局激活配置"
href="/node?risk=lagging"
/>
<RiskSignal
label="OpenResty 异常"
value={overview.risk.unhealthy_nodes}
tone="danger"
hint="运行态已出现不健康节点"
href="/node?risk=unhealthy"
/>
<RiskSignal
label="高 CPU / 内存"
@@ -347,6 +389,13 @@ export function DashboardOverview() {
tone="warning"
hint={`${overview.risk.offline_nodes} 个离线节点`}
/>
<RiskSignal
label="离线节点"
value={overview.risk.offline_nodes}
tone="warning"
hint="节点长时间未心跳或已失联"
href="/node?risk=offline"
/>
</div>
</AppCard>
@@ -373,11 +422,17 @@ export function DashboardOverview() {
label="最忙节点"
value={formatPeakNode(overview.peaks.busiest_node).title}
hint={formatPeakNode(overview.peaks.busiest_node).hint}
href={buildNodeDetailHref(
nodeIdMap.get(overview.peaks.busiest_node?.node_id ?? ''),
)}
/>
<PeakCard
label="优先排查节点"
value={formatPeakNode(overview.peaks.riskiest_node).title}
hint={formatPeakNode(overview.peaks.riskiest_node).hint}
href={buildNodeDetailHref(
nodeIdMap.get(overview.peaks.riskiest_node?.node_id ?? ''),
)}
/>
</div>
</AppCard>
@@ -510,9 +565,10 @@ export function DashboardOverview() {
) : (
<div className="space-y-3">
{overview.active_alerts.map((alert) => (
<div
<Link
key={`${alert.node_id}-${alert.event_type}-${alert.last_triggered_at}`}
className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4"
href={buildNodeDetailHref(nodeIdMap.get(alert.node_id))}
className="block rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4 transition hover:border-[var(--border-strong)] hover:bg-[var(--surface-muted)]"
>
<div className="flex flex-wrap items-center gap-2">
<StatusBadge
@@ -530,7 +586,7 @@ export function DashboardOverview() {
{formatRelativeTime(alert.last_triggered_at)} ·{' '}
{formatDateTime(alert.last_triggered_at)}
</p>
</div>
</Link>
))}
</div>
)}
@@ -526,6 +526,15 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
),
[observability?.traffic_reports],
);
const dominantStatusCode = statusCodeDistribution[0] ?? null;
const dominantDomain = topDomains[0] ?? null;
const resolvedHealthEvents = useMemo(
() =>
observability?.health_events.filter(
(event) => event.status === 'resolved',
) ?? [],
[observability?.health_events],
);
const memoryUsageRatio = formatUsageRatio(
latestMetricSnapshot?.memory_used_bytes,
latestMetricSnapshot?.memory_total_bytes,
@@ -972,6 +981,46 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
title="请求结构分布"
description="聚合最近 24 小时窗口上报,帮助判断错误集中在哪些状态码、流量集中在哪些域名。"
>
<div className="mb-6 grid gap-4 md:grid-cols-3">
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
<p className="text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
主状态码
</p>
<p className="mt-3 text-2xl font-semibold text-[var(--foreground-primary)]">
{dominantStatusCode?.label ?? '—'}
</p>
<p className="mt-2 text-sm text-[var(--foreground-secondary)]">
{dominantStatusCode
? `${dominantStatusCode.value} 次`
: '暂无状态码聚合'}
</p>
</div>
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
<p className="text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
Top Domain
</p>
<p className="mt-3 truncate text-2xl font-semibold text-[var(--foreground-primary)]">
{dominantDomain?.label ?? '—'}
</p>
<p className="mt-2 text-sm text-[var(--foreground-secondary)]">
{dominantDomain
? `${dominantDomain.value} 次`
: '暂无域名聚合'}
</p>
</div>
<div className="rounded-2xl border border-[var(--border-default)] bg-[var(--surface-elevated)] px-4 py-4">
<p className="text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
已恢复事件
</p>
<p className="mt-3 text-2xl font-semibold text-[var(--foreground-primary)]">
{resolvedHealthEvents.length}
</p>
<p className="mt-2 text-sm text-[var(--foreground-secondary)]">
最近 24 小时已恢复健康事件
</p>
</div>
</div>
<div className="grid gap-6 xl:grid-cols-2">
<div>
<p className="mb-4 text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">