mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-13 10:26:38 +08:00
feat(node-detail): 添加主状态码、Top Domain 和已恢复事件统计信息
This commit is contained in:
@@ -76,11 +76,13 @@ function RiskSignal({
|
|||||||
value,
|
value,
|
||||||
tone,
|
tone,
|
||||||
hint,
|
hint,
|
||||||
|
href,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: number;
|
value: number;
|
||||||
tone: 'danger' | 'warning' | 'info' | 'success';
|
tone: 'danger' | 'warning' | 'info' | 'success';
|
||||||
hint: string;
|
hint: string;
|
||||||
|
href?: string;
|
||||||
}) {
|
}) {
|
||||||
const toneClass =
|
const toneClass =
|
||||||
tone === 'danger'
|
tone === 'danger'
|
||||||
@@ -91,25 +93,38 @@ function RiskSignal({
|
|||||||
? 'border-emerald-400/30 bg-emerald-500/10 text-emerald-100'
|
? 'border-emerald-400/30 bg-emerald-500/10 text-emerald-100'
|
||||||
: 'border-sky-400/30 bg-sky-500/10 text-sky-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}`}>
|
<div className={`rounded-3xl border px-4 py-4 ${toneClass}`}>
|
||||||
<p className="text-xs tracking-[0.22em] uppercase opacity-75">{label}</p>
|
<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-3 text-3xl font-semibold">{value}</p>
|
||||||
<p className="mt-2 text-sm opacity-80">{hint}</p>
|
<p className="mt-2 text-sm opacity-80">{hint}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
if (!href) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className="block transition hover:opacity-90"
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PeakCard({
|
function PeakCard({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
hint,
|
hint,
|
||||||
|
href,
|
||||||
}: {
|
}: {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
hint: 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">
|
<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">
|
<p className="text-xs tracking-[0.22em] text-[var(--foreground-muted)] uppercase">
|
||||||
{label}
|
{label}
|
||||||
@@ -120,6 +135,17 @@ function PeakCard({
|
|||||||
<p className="mt-2 text-sm text-[var(--foreground-secondary)]">{hint}</p>
|
<p className="mt-2 text-sm text-[var(--foreground-secondary)]">{hint}</p>
|
||||||
</div>
|
</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) {
|
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({
|
function OverviewMetric({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
@@ -163,7 +196,10 @@ function OverviewMetric({
|
|||||||
|
|
||||||
function NodeHealthRow({ node }: { node: DashboardNodeHealth }) {
|
function NodeHealthRow({ node }: { node: DashboardNodeHealth }) {
|
||||||
return (
|
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 className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-semibold text-[var(--foreground-primary)]">
|
<p className="text-sm font-semibold text-[var(--foreground-primary)]">
|
||||||
@@ -229,7 +265,7 @@ function NodeHealthRow({ node }: { node: DashboardNodeHealth }) {
|
|||||||
: ' 暂无'}
|
: ' 暂无'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<AppCard
|
<AppCard
|
||||||
@@ -326,12 +366,14 @@ export function DashboardOverview() {
|
|||||||
value={overview.risk.lagging_nodes}
|
value={overview.risk.lagging_nodes}
|
||||||
tone="info"
|
tone="info"
|
||||||
hint="当前版本未追平全局激活配置"
|
hint="当前版本未追平全局激活配置"
|
||||||
|
href="/node?risk=lagging"
|
||||||
/>
|
/>
|
||||||
<RiskSignal
|
<RiskSignal
|
||||||
label="OpenResty 异常"
|
label="OpenResty 异常"
|
||||||
value={overview.risk.unhealthy_nodes}
|
value={overview.risk.unhealthy_nodes}
|
||||||
tone="danger"
|
tone="danger"
|
||||||
hint="运行态已出现不健康节点"
|
hint="运行态已出现不健康节点"
|
||||||
|
href="/node?risk=unhealthy"
|
||||||
/>
|
/>
|
||||||
<RiskSignal
|
<RiskSignal
|
||||||
label="高 CPU / 内存"
|
label="高 CPU / 内存"
|
||||||
@@ -347,6 +389,13 @@ export function DashboardOverview() {
|
|||||||
tone="warning"
|
tone="warning"
|
||||||
hint={`${overview.risk.offline_nodes} 个离线节点`}
|
hint={`${overview.risk.offline_nodes} 个离线节点`}
|
||||||
/>
|
/>
|
||||||
|
<RiskSignal
|
||||||
|
label="离线节点"
|
||||||
|
value={overview.risk.offline_nodes}
|
||||||
|
tone="warning"
|
||||||
|
hint="节点长时间未心跳或已失联"
|
||||||
|
href="/node?risk=offline"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
|
||||||
@@ -373,11 +422,17 @@ export function DashboardOverview() {
|
|||||||
label="最忙节点"
|
label="最忙节点"
|
||||||
value={formatPeakNode(overview.peaks.busiest_node).title}
|
value={formatPeakNode(overview.peaks.busiest_node).title}
|
||||||
hint={formatPeakNode(overview.peaks.busiest_node).hint}
|
hint={formatPeakNode(overview.peaks.busiest_node).hint}
|
||||||
|
href={buildNodeDetailHref(
|
||||||
|
nodeIdMap.get(overview.peaks.busiest_node?.node_id ?? ''),
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<PeakCard
|
<PeakCard
|
||||||
label="优先排查节点"
|
label="优先排查节点"
|
||||||
value={formatPeakNode(overview.peaks.riskiest_node).title}
|
value={formatPeakNode(overview.peaks.riskiest_node).title}
|
||||||
hint={formatPeakNode(overview.peaks.riskiest_node).hint}
|
hint={formatPeakNode(overview.peaks.riskiest_node).hint}
|
||||||
|
href={buildNodeDetailHref(
|
||||||
|
nodeIdMap.get(overview.peaks.riskiest_node?.node_id ?? ''),
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
@@ -510,9 +565,10 @@ export function DashboardOverview() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{overview.active_alerts.map((alert) => (
|
{overview.active_alerts.map((alert) => (
|
||||||
<div
|
<Link
|
||||||
key={`${alert.node_id}-${alert.event_type}-${alert.last_triggered_at}`}
|
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">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<StatusBadge
|
<StatusBadge
|
||||||
@@ -530,7 +586,7 @@ export function DashboardOverview() {
|
|||||||
{formatRelativeTime(alert.last_triggered_at)} ·{' '}
|
{formatRelativeTime(alert.last_triggered_at)} ·{' '}
|
||||||
{formatDateTime(alert.last_triggered_at)}
|
{formatDateTime(alert.last_triggered_at)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -526,6 +526,15 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
|||||||
),
|
),
|
||||||
[observability?.traffic_reports],
|
[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(
|
const memoryUsageRatio = formatUsageRatio(
|
||||||
latestMetricSnapshot?.memory_used_bytes,
|
latestMetricSnapshot?.memory_used_bytes,
|
||||||
latestMetricSnapshot?.memory_total_bytes,
|
latestMetricSnapshot?.memory_total_bytes,
|
||||||
@@ -972,6 +981,46 @@ export function NodeDetailPage({ nodeId }: { nodeId: string }) {
|
|||||||
title="请求结构分布"
|
title="请求结构分布"
|
||||||
description="聚合最近 24 小时窗口上报,帮助判断错误集中在哪些状态码、流量集中在哪些域名。"
|
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 className="grid gap-6 xl:grid-cols-2">
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-4 text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
|
<p className="mb-4 text-xs tracking-[0.2em] text-[var(--foreground-muted)] uppercase">
|
||||||
|
|||||||
Reference in New Issue
Block a user