mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 16:16:37 +08:00
Refactor dashboard overview: remove active alerts and lagging nodes from summary, update tests and components accordingly
- Removed active alerts and lagging nodes from DashboardSummary and related types. - Updated DashboardOverview component to reflect changes in data structure. - Adjusted tests to align with the new dashboard overview structure, ensuring no active alerts are displayed. - Simplified the dashboard metrics and risk signals, focusing on essential health and capacity metrics. - Enhanced the WorldStage component to present updated traffic and capacity information. - Removed unused alert-related functions and components to streamline the codebase.
This commit is contained in:
@@ -16,7 +16,6 @@ function normalizeDashboardOverview(
|
||||
return {
|
||||
...overview,
|
||||
nodes: arrayOrEmpty(overview.nodes),
|
||||
active_alerts: arrayOrEmpty(overview.active_alerts),
|
||||
distributions: {
|
||||
...overview.distributions,
|
||||
source_countries: arrayOrEmpty(overview.distributions?.source_countries),
|
||||
|
||||
@@ -11,11 +11,7 @@ import { LoadingState } from '@/components/feedback/loading-state';
|
||||
import { AppCard } from '@/components/ui/app-card';
|
||||
import { StatusBadge } from '@/components/ui/status-badge';
|
||||
import { getDashboardOverview } from '@/features/dashboard/api/overview';
|
||||
import type {
|
||||
DashboardAlert,
|
||||
DashboardNodeHealth,
|
||||
DashboardPeakNode,
|
||||
} from '@/features/dashboard/types';
|
||||
import type { DashboardNodeHealth } from '@/features/dashboard/types';
|
||||
import { WorldStage } from '@/features/dashboard/components/world-stage';
|
||||
import {
|
||||
getNodeStatusLabel,
|
||||
@@ -59,48 +55,6 @@ function formatTrendHour(value: string) {
|
||||
return `${date.getHours().toString().padStart(2, '0')}:00`;
|
||||
}
|
||||
|
||||
function formatPeakHour(value: string) {
|
||||
if (!value) {
|
||||
return '暂无';
|
||||
}
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '暂无';
|
||||
}
|
||||
return `${formatTrendHour(value)} - ${date
|
||||
.getMinutes()
|
||||
.toString()
|
||||
.padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
function getAlertVariant(
|
||||
alert: DashboardAlert,
|
||||
): 'success' | 'warning' | 'danger' | 'info' {
|
||||
if (alert.status === 'resolved') {
|
||||
return 'success';
|
||||
}
|
||||
if (alert.severity === 'critical') {
|
||||
return 'danger';
|
||||
}
|
||||
if (alert.severity === 'warning') {
|
||||
return 'warning';
|
||||
}
|
||||
return 'info';
|
||||
}
|
||||
|
||||
function formatPeakNode(node: DashboardPeakNode | null) {
|
||||
if (!node) {
|
||||
return {
|
||||
title: '暂无',
|
||||
hint: '当前没有可用节点数据',
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: node.node_name,
|
||||
hint: `请求 ${node.request_count} · 错误 ${node.error_count} · CPU ${formatPercent(node.cpu_usage_percent)}`,
|
||||
};
|
||||
}
|
||||
|
||||
function buildNodeDetailHref(id?: number | null) {
|
||||
if (!id) {
|
||||
return '/node';
|
||||
@@ -173,82 +127,6 @@ function OverviewMetric({
|
||||
);
|
||||
}
|
||||
|
||||
function RiskSignal({
|
||||
label,
|
||||
value,
|
||||
tone,
|
||||
hint,
|
||||
href,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
tone: 'danger' | 'warning' | 'info' | 'success';
|
||||
hint: string;
|
||||
href?: string;
|
||||
}) {
|
||||
const toneClass =
|
||||
tone === 'danger'
|
||||
? 'border-rose-400/28 bg-rose-500/10 text-rose-100'
|
||||
: tone === 'warning'
|
||||
? 'border-amber-400/28 bg-amber-500/10 text-amber-100'
|
||||
: tone === 'success'
|
||||
? 'border-emerald-400/28 bg-emerald-500/10 text-emerald-100'
|
||||
: 'border-sky-400/28 bg-sky-500/10 text-sky-100';
|
||||
|
||||
const content = (
|
||||
<div className={`rounded-3xl border px-4 py-4 ${toneClass}`}>
|
||||
<p className="text-xs tracking-[0.22em] opacity-75 uppercase">{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;
|
||||
}) {
|
||||
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}
|
||||
</p>
|
||||
<p className="mt-3 text-xl font-semibold text-[var(--foreground-primary)]">
|
||||
{value}
|
||||
</p>
|
||||
<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 NodeHealthRow({ node }: { node: DashboardNodeHealth }) {
|
||||
return (
|
||||
<Link
|
||||
@@ -354,148 +232,60 @@ export function DashboardOverview() {
|
||||
);
|
||||
}
|
||||
|
||||
const nodeIdMap = new Map(
|
||||
overview.nodes.map((node) => [node.node_id, node.id] as const),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<WorldStage
|
||||
generatedAt={overview.generated_at}
|
||||
summary={overview.summary}
|
||||
risk={overview.risk}
|
||||
config={overview.config}
|
||||
traffic={overview.traffic}
|
||||
capacity={overview.capacity}
|
||||
nodes={overview.nodes}
|
||||
sourceCountries={overview.distributions.source_countries}
|
||||
/>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[1.15fr_0.85fr]">
|
||||
<AppCard
|
||||
title="系统健康摘要"
|
||||
description="先回答系统是否健康、容量是否紧张、是否存在追平偏差和异常流量。"
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<OverviewMetric
|
||||
label="在线节点"
|
||||
value={`${overview.summary.online_nodes}/${overview.summary.total_nodes}`}
|
||||
hint={`${overview.summary.offline_nodes} 离线 · ${overview.summary.pending_nodes} 待接入`}
|
||||
accent="sky"
|
||||
/>
|
||||
<OverviewMetric
|
||||
label="活动异常"
|
||||
value={overview.summary.active_alerts}
|
||||
hint={`${overview.summary.unhealthy_nodes} 个 OpenResty 不健康`}
|
||||
accent="rose"
|
||||
/>
|
||||
<OverviewMetric
|
||||
label="配置落后"
|
||||
value={overview.summary.lagging_nodes}
|
||||
hint={overview.config.active_version || '当前无激活版本'}
|
||||
accent="amber"
|
||||
/>
|
||||
<OverviewMetric
|
||||
label="最近窗口请求"
|
||||
value={overview.traffic.request_count.toLocaleString('zh-CN')}
|
||||
hint={`QPS ${overview.traffic.estimated_qps.toFixed(1)} · UV ${overview.traffic.unique_visitors.toLocaleString('zh-CN')}`}
|
||||
accent="emerald"
|
||||
/>
|
||||
<OverviewMetric
|
||||
label="平均 CPU"
|
||||
value={formatPercent(overview.capacity.average_cpu_usage_percent)}
|
||||
hint={`${overview.capacity.high_cpu_nodes} 个高 CPU 节点`}
|
||||
accent="amber"
|
||||
/>
|
||||
<OverviewMetric
|
||||
label="平均内存"
|
||||
value={formatPercent(overview.capacity.average_memory_usage_percent)}
|
||||
hint={`${overview.capacity.high_memory_nodes} 个高内存节点`}
|
||||
accent="sky"
|
||||
/>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<AppCard
|
||||
title="峰值与风险摘要"
|
||||
description="快速回答什么时候最忙、哪里最危险,以及首页能直接行动的入口。"
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<PeakCard
|
||||
label="请求峰值时段"
|
||||
value={formatPeakHour(
|
||||
overview.peaks.peak_request_hour.bucket_started_at,
|
||||
)}
|
||||
hint={`峰值请求 ${overview.peaks.peak_request_hour.request_count.toLocaleString('zh-CN')}`}
|
||||
/>
|
||||
<PeakCard
|
||||
label="错误峰值时段"
|
||||
value={formatPeakHour(
|
||||
overview.peaks.peak_error_hour.bucket_started_at,
|
||||
)}
|
||||
hint={`峰值错误 ${overview.peaks.peak_error_hour.error_count.toLocaleString('zh-CN')}`}
|
||||
/>
|
||||
<PeakCard
|
||||
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>
|
||||
|
||||
<div className="mt-4 grid gap-4 md:grid-cols-2">
|
||||
<RiskSignal
|
||||
label="Critical"
|
||||
value={overview.risk.critical_alerts}
|
||||
tone="danger"
|
||||
hint="当前仍在触发中的严重异常"
|
||||
/>
|
||||
<RiskSignal
|
||||
label="Warning"
|
||||
value={overview.risk.warning_alerts}
|
||||
tone="warning"
|
||||
hint="需要尽快介入但尚未到故障级"
|
||||
/>
|
||||
<RiskSignal
|
||||
label="配置落后"
|
||||
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 / 内存"
|
||||
value={
|
||||
overview.risk.high_cpu_nodes + overview.risk.high_memory_nodes
|
||||
}
|
||||
tone="warning"
|
||||
hint={`${overview.risk.high_cpu_nodes} 个高 CPU · ${overview.risk.high_memory_nodes} 个高内存`}
|
||||
/>
|
||||
<RiskSignal
|
||||
label="离线节点"
|
||||
value={overview.risk.offline_nodes}
|
||||
tone="warning"
|
||||
hint="节点长时间未心跳或已失联"
|
||||
href="/node?risk=offline"
|
||||
/>
|
||||
</div>
|
||||
</AppCard>
|
||||
</div>
|
||||
<AppCard
|
||||
title="系统健康摘要"
|
||||
description="聚焦在线率、运行质量、流量体量与资源负载,保留首屏最核心的运营信号。"
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<OverviewMetric
|
||||
label="在线节点"
|
||||
value={`${overview.summary.online_nodes}/${overview.summary.total_nodes}`}
|
||||
hint={`${overview.summary.offline_nodes} 离线 · ${overview.summary.pending_nodes} 待接入`}
|
||||
accent="sky"
|
||||
/>
|
||||
<OverviewMetric
|
||||
label="OpenResty 异常"
|
||||
value={overview.summary.unhealthy_nodes}
|
||||
hint="当前运行态已出现不健康节点"
|
||||
accent="rose"
|
||||
/>
|
||||
<OverviewMetric
|
||||
label="最近窗口请求"
|
||||
value={overview.traffic.request_count.toLocaleString('zh-CN')}
|
||||
hint={`QPS ${overview.traffic.estimated_qps.toFixed(1)} · UV ${overview.traffic.unique_visitors.toLocaleString('zh-CN')}`}
|
||||
accent="emerald"
|
||||
/>
|
||||
<OverviewMetric
|
||||
label="平均 CPU"
|
||||
value={formatPercent(overview.capacity.average_cpu_usage_percent)}
|
||||
hint={`${overview.capacity.high_cpu_nodes} 个高 CPU 节点`}
|
||||
accent="amber"
|
||||
/>
|
||||
<OverviewMetric
|
||||
label="平均内存"
|
||||
value={formatPercent(overview.capacity.average_memory_usage_percent)}
|
||||
hint={`${overview.capacity.high_memory_nodes} 个高内存节点`}
|
||||
accent="sky"
|
||||
/>
|
||||
<OverviewMetric
|
||||
label="高存储节点"
|
||||
value={overview.capacity.high_storage_nodes}
|
||||
hint={`${overview.traffic.reported_nodes} 个节点参与当前窗口上报`}
|
||||
accent="amber"
|
||||
/>
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-2">
|
||||
<AppCard
|
||||
@@ -559,7 +349,7 @@ export function DashboardOverview() {
|
||||
</AppCard>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[1.15fr_0.85fr]">
|
||||
<div className="grid gap-6 xl:grid-cols-[1.1fr_0.9fr]">
|
||||
<AppCard
|
||||
title="24 小时网络与磁盘趋势"
|
||||
description="把 OpenResty 吞吐和磁盘读写放到同一屏,方便判断是流量拉升还是资源抖动。"
|
||||
@@ -621,94 +411,9 @@ export function DashboardOverview() {
|
||||
</div>
|
||||
</AppCard>
|
||||
|
||||
<AppCard
|
||||
title="活动异常"
|
||||
description="优先展示当前仍在触发中的问题。"
|
||||
>
|
||||
{overview.active_alerts.length === 0 ? (
|
||||
<EmptyState
|
||||
title="暂无活动异常"
|
||||
description="当前系统没有正在触发的节点健康事件。"
|
||||
/>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{overview.active_alerts.map((alert) => (
|
||||
<Link
|
||||
key={`${alert.node_id}-${alert.event_type}-${alert.last_triggered_at}`}
|
||||
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
|
||||
label={alert.event_type.replaceAll('_', ' ')}
|
||||
variant={getAlertVariant(alert)}
|
||||
/>
|
||||
<p className="text-sm font-medium text-[var(--foreground-primary)]">
|
||||
{alert.node_name}
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-[var(--foreground-secondary)]">
|
||||
{alert.message || '暂无详细消息'}
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-[var(--foreground-muted)]">
|
||||
{formatRelativeTime(alert.last_triggered_at)} ·{' '}
|
||||
{formatDateTime(alert.last_triggered_at)}
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</AppCard>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-3">
|
||||
<AppCard
|
||||
title="来源分布"
|
||||
description="聚合最近 24 小时主要来源国家,优先识别流量重心变化。"
|
||||
>
|
||||
<RankChart
|
||||
items={overview.distributions.source_countries.map((item) => ({
|
||||
label: item.key,
|
||||
value: item.value,
|
||||
}))}
|
||||
color="#38bdf8"
|
||||
emptyMessage="暂无来源分布数据"
|
||||
/>
|
||||
</AppCard>
|
||||
|
||||
<AppCard
|
||||
title="状态码分布"
|
||||
description="快速判断成功响应是否仍是主流,以及错误码是否有抬升。"
|
||||
>
|
||||
<RankChart
|
||||
items={overview.distributions.status_codes.map((item) => ({
|
||||
label: `HTTP ${item.key}`,
|
||||
value: item.value,
|
||||
}))}
|
||||
color="#f59e0b"
|
||||
emptyMessage="暂无状态码分布"
|
||||
/>
|
||||
</AppCard>
|
||||
|
||||
<AppCard
|
||||
title="Top Domain"
|
||||
description="观察主要流量集中在哪些域名,方便判断业务热度与异常集中点。"
|
||||
>
|
||||
<RankChart
|
||||
items={overview.distributions.top_domains.map((item) => ({
|
||||
label: item.key,
|
||||
value: item.value,
|
||||
}))}
|
||||
color="#34d399"
|
||||
emptyMessage="暂无域名分布"
|
||||
/>
|
||||
</AppCard>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-[1.2fr_0.8fr]">
|
||||
<AppCard
|
||||
title="Top 节点榜单"
|
||||
description="把流量峰值、错误热点和容量压力节点并排展示,方便值守时快速判断先处理谁。"
|
||||
description="把流量、错误与容量压力节点并排展示,保留快速定位热点与瓶颈的入口。"
|
||||
>
|
||||
<div className="grid gap-6 xl:grid-cols-3">
|
||||
<div>
|
||||
@@ -760,61 +465,49 @@ export function DashboardOverview() {
|
||||
</div>
|
||||
</div>
|
||||
</AppCard>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 xl:grid-cols-3">
|
||||
<AppCard
|
||||
title="来源分布"
|
||||
description="聚合最近 24 小时主要来源国家,优先识别流量重心变化。"
|
||||
>
|
||||
<RankChart
|
||||
items={overview.distributions.source_countries.map((item) => ({
|
||||
label: item.key,
|
||||
value: item.value,
|
||||
}))}
|
||||
color="#38bdf8"
|
||||
emptyMessage="暂无来源分布数据"
|
||||
/>
|
||||
</AppCard>
|
||||
|
||||
<AppCard
|
||||
title="处置建议"
|
||||
description="把首页信号翻译成可行动的下一步,降低值守判断成本。"
|
||||
title="状态码分布"
|
||||
description="快速判断成功响应是否仍是主流,以及错误码是否有抬升。"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-3xl border border-rose-400/18 bg-rose-500/8 px-4 py-4">
|
||||
<p className="text-xs tracking-[0.22em] text-rose-200 uppercase">
|
||||
第一优先级
|
||||
</p>
|
||||
<p className="mt-3 text-lg font-semibold text-[var(--foreground-primary)]">
|
||||
先处理活动异常与 OpenResty 不健康节点
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-[var(--foreground-secondary)]">
|
||||
当前有 {overview.summary.active_alerts} 个活动异常,{` `}
|
||||
{overview.summary.unhealthy_nodes} 个 OpenResty 不健康节点。
|
||||
</p>
|
||||
</div>
|
||||
<RankChart
|
||||
items={overview.distributions.status_codes.map((item) => ({
|
||||
label: `HTTP ${item.key}`,
|
||||
value: item.value,
|
||||
}))}
|
||||
color="#f59e0b"
|
||||
emptyMessage="暂无状态码分布"
|
||||
/>
|
||||
</AppCard>
|
||||
|
||||
<div className="rounded-3xl border border-amber-400/18 bg-amber-500/8 px-4 py-4">
|
||||
<p className="text-xs tracking-[0.22em] text-amber-200 uppercase">
|
||||
第二优先级
|
||||
</p>
|
||||
<p className="mt-3 text-lg font-semibold text-[var(--foreground-primary)]">
|
||||
检查配置落后与容量逼近节点
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-[var(--foreground-secondary)]">
|
||||
当前有 {overview.summary.lagging_nodes} 个配置落后节点,{` `}
|
||||
{overview.capacity.high_cpu_nodes +
|
||||
overview.capacity.high_memory_nodes +
|
||||
overview.capacity.high_storage_nodes}{' '}
|
||||
个节点出现明显容量压力。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-3xl border border-sky-400/18 bg-sky-500/8 px-4 py-4">
|
||||
<p className="text-xs tracking-[0.22em] text-sky-200 uppercase">
|
||||
观察项
|
||||
</p>
|
||||
<p className="mt-3 text-lg font-semibold text-[var(--foreground-primary)]">
|
||||
关注峰值时段与来源分布变化
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-[var(--foreground-secondary)]">
|
||||
请求峰值出现在 {formatPeakHour(
|
||||
overview.peaks.peak_request_hour.bucket_started_at,
|
||||
)}
|
||||
,当前主要来源集中在
|
||||
{overview.distributions.source_countries
|
||||
.slice(0, 2)
|
||||
.map((item) => item.key)
|
||||
.join(' / ') || ' 暂无来源数据'}
|
||||
。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<AppCard
|
||||
title="Top Domain"
|
||||
description="观察主要流量集中在哪些域名,方便判断业务热度与异常集中点。"
|
||||
>
|
||||
<RankChart
|
||||
items={overview.distributions.top_domains.map((item) => ({
|
||||
label: item.key,
|
||||
value: item.value,
|
||||
}))}
|
||||
color="#34d399"
|
||||
emptyMessage="暂无域名分布"
|
||||
/>
|
||||
</AppCard>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -285,8 +285,8 @@ export function WorldStageMap({
|
||||
roam: false,
|
||||
silent: true,
|
||||
layoutCenter: ['50%', '50%'],
|
||||
layoutSize: '132%',
|
||||
zoom: 1.12,
|
||||
layoutSize: '220%',
|
||||
zoom: 1.2,
|
||||
itemStyle: {
|
||||
areaColor: mapPalette.areaColor,
|
||||
borderColor: mapPalette.borderColor,
|
||||
|
||||
@@ -1,25 +1,17 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { EmptyState } from '@/components/feedback/empty-state';
|
||||
import { useTheme } from '@/components/providers/theme-provider';
|
||||
import { StatusBadge } from '@/components/ui/status-badge';
|
||||
import type {
|
||||
DashboardConfig,
|
||||
DashboardCapacity,
|
||||
DashboardNodeHealth,
|
||||
DashboardRiskSummary,
|
||||
DashboardSummary,
|
||||
DashboardTraffic,
|
||||
DistributionItem,
|
||||
} from '@/features/dashboard/types';
|
||||
import {
|
||||
getNodeStatusLabel,
|
||||
getNodeStatusVariant,
|
||||
getOpenrestyStatusLabel,
|
||||
getOpenrestyStatusVariant,
|
||||
} from '@/features/nodes/utils';
|
||||
import { cn } from '@/lib/utils/cn';
|
||||
import { formatDateTime } from '@/lib/utils/date';
|
||||
|
||||
@@ -40,13 +32,6 @@ function formatPercent(value: number) {
|
||||
return `${value.toFixed(value >= 100 ? 0 : 1)}%`;
|
||||
}
|
||||
|
||||
function buildNodeDetailHref(id?: number | null) {
|
||||
if (!id) {
|
||||
return '/node';
|
||||
}
|
||||
return `/node/detail?id=${id}`;
|
||||
}
|
||||
|
||||
function HeroMetric({
|
||||
label,
|
||||
value,
|
||||
@@ -174,15 +159,15 @@ function CountrySignal({
|
||||
export function WorldStage({
|
||||
generatedAt,
|
||||
summary,
|
||||
risk,
|
||||
config,
|
||||
traffic,
|
||||
capacity,
|
||||
nodes,
|
||||
sourceCountries,
|
||||
}: {
|
||||
generatedAt: string;
|
||||
summary: DashboardSummary;
|
||||
risk: DashboardRiskSummary;
|
||||
config: DashboardConfig;
|
||||
traffic: DashboardTraffic;
|
||||
capacity: DashboardCapacity;
|
||||
nodes: DashboardNodeHealth[];
|
||||
sourceCountries: DistributionItem[];
|
||||
}) {
|
||||
@@ -223,15 +208,9 @@ export function WorldStage({
|
||||
summary.total_nodes > 0
|
||||
? (summary.online_nodes / summary.total_nodes) * 100
|
||||
: 0;
|
||||
const syncedNodes = Math.max(
|
||||
0,
|
||||
summary.total_nodes - summary.lagging_nodes - summary.pending_nodes,
|
||||
);
|
||||
const syncRate =
|
||||
summary.total_nodes > 0 ? (syncedNodes / summary.total_nodes) * 100 : 0;
|
||||
const healthyNodes = Math.max(
|
||||
0,
|
||||
summary.online_nodes - summary.unhealthy_nodes - risk.offline_nodes,
|
||||
summary.online_nodes - summary.unhealthy_nodes,
|
||||
);
|
||||
const healthyRate =
|
||||
summary.total_nodes > 0 ? (healthyNodes / summary.total_nodes) * 100 : 0;
|
||||
@@ -280,7 +259,7 @@ export function WorldStage({
|
||||
isDark ? 'text-slate-300' : 'text-slate-600',
|
||||
)}
|
||||
>
|
||||
在同一视图汇总节点在线率、运行状态、配置一致性与全球流量来源,
|
||||
在同一视图汇总节点在线率、运行状态、资源负载与全球流量来源,
|
||||
让首屏可以直接承担值守与研判入口。
|
||||
</p>
|
||||
</div>
|
||||
@@ -396,7 +375,7 @@ export function WorldStage({
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-1">
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-2">
|
||||
<HeroMetric
|
||||
label="在线覆盖"
|
||||
value={formatPercent(onlineRate)}
|
||||
@@ -410,119 +389,18 @@ export function WorldStage({
|
||||
isDark={isDark}
|
||||
/>
|
||||
<HeroMetric
|
||||
label="配置追平"
|
||||
value={formatPercent(syncRate)}
|
||||
hint={`${summary.lagging_nodes} 个节点未对齐 ${config.active_version || '当前激活版本'}`}
|
||||
label="最近窗口请求"
|
||||
value={traffic.request_count.toLocaleString('zh-CN')}
|
||||
hint={`QPS ${traffic.estimated_qps.toFixed(1)} · ${traffic.reported_nodes} 个节点已上报`}
|
||||
isDark={isDark}
|
||||
/>
|
||||
<HeroMetric
|
||||
label="活动风险"
|
||||
value={summary.active_alerts.toLocaleString('zh-CN')}
|
||||
hint={`${risk.critical_alerts} 个严重告警 · ${risk.warning_alerts} 个预警`}
|
||||
label="平均 CPU"
|
||||
value={formatPercent(capacity.average_cpu_usage_percent)}
|
||||
hint={`${capacity.high_cpu_nodes} 个节点 CPU 偏高`}
|
||||
isDark={isDark}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-[28px] border px-5 py-5 backdrop-blur',
|
||||
isDark
|
||||
? 'border-white/10 bg-white/6'
|
||||
: 'border-slate-200/80 bg-white/80 shadow-[0_18px_40px_rgba(148,163,184,0.12)]',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p
|
||||
className={cn(
|
||||
'text-[11px] tracking-[0.24em] uppercase',
|
||||
isDark ? 'text-slate-300' : 'text-slate-500',
|
||||
)}
|
||||
>
|
||||
节点风险清单
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
'mt-2 text-lg font-semibold',
|
||||
isDark ? 'text-white' : 'text-slate-950',
|
||||
)}
|
||||
>
|
||||
优先处置节点
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/node"
|
||||
className={cn(
|
||||
'rounded-full border px-3 py-1.5 text-xs transition',
|
||||
isDark
|
||||
? 'border-white/12 text-slate-100 hover:bg-white/8'
|
||||
: 'border-slate-200 text-slate-700 hover:bg-slate-100/80',
|
||||
)}
|
||||
>
|
||||
查看全部节点
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-4 space-y-3">
|
||||
{nodes.slice(0, 4).map((node) => (
|
||||
<Link
|
||||
key={node.node_id}
|
||||
href={buildNodeDetailHref(node.id)}
|
||||
className={cn(
|
||||
'block rounded-2xl border px-4 py-4 transition',
|
||||
isDark
|
||||
? 'border-white/8 bg-slate-950/20 hover:border-white/18 hover:bg-white/6'
|
||||
: 'border-slate-200/80 bg-slate-50/70 hover:border-slate-300 hover:bg-white',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p
|
||||
className={cn(
|
||||
'text-sm font-semibold',
|
||||
isDark ? 'text-white' : 'text-slate-950',
|
||||
)}
|
||||
>
|
||||
{node.name}
|
||||
</p>
|
||||
<p
|
||||
className={cn(
|
||||
'mt-1 text-xs',
|
||||
isDark ? 'text-slate-400' : 'text-slate-500',
|
||||
)}
|
||||
>
|
||||
请求量 {node.request_count.toLocaleString('zh-CN')} · 错误数{' '}
|
||||
{node.error_count.toLocaleString('zh-CN')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<StatusBadge
|
||||
label={getNodeStatusLabel(node.status)}
|
||||
variant={getNodeStatusVariant(node.status)}
|
||||
/>
|
||||
<StatusBadge
|
||||
label={getOpenrestyStatusLabel(node.openresty_status)}
|
||||
variant={getOpenrestyStatusVariant(
|
||||
node.openresty_status,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
{nodes.length === 0 ? (
|
||||
<div
|
||||
className={cn(
|
||||
'rounded-2xl border border-dashed px-4 py-5 text-sm',
|
||||
isDark
|
||||
? 'border-white/12 bg-slate-950/20 text-slate-300'
|
||||
: 'border-slate-300/70 bg-slate-50/75 text-slate-600',
|
||||
)}
|
||||
>
|
||||
当前暂无节点运行数据。节点开始上报 heartbeat 后,这里会展示待优先处理的节点。
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -4,8 +4,6 @@ export interface DashboardSummary {
|
||||
offline_nodes: number;
|
||||
pending_nodes: number;
|
||||
unhealthy_nodes: number;
|
||||
active_alerts: number;
|
||||
lagging_nodes: number;
|
||||
}
|
||||
|
||||
export interface DashboardTraffic {
|
||||
@@ -24,48 +22,6 @@ export interface DashboardCapacity {
|
||||
high_storage_nodes: number;
|
||||
}
|
||||
|
||||
export interface DashboardConfig {
|
||||
active_version: string;
|
||||
lagging_nodes: number;
|
||||
pending_nodes: number;
|
||||
}
|
||||
|
||||
export interface DashboardRiskSummary {
|
||||
critical_alerts: number;
|
||||
warning_alerts: number;
|
||||
info_alerts: number;
|
||||
offline_nodes: number;
|
||||
unhealthy_nodes: number;
|
||||
lagging_nodes: number;
|
||||
high_cpu_nodes: number;
|
||||
high_memory_nodes: number;
|
||||
high_storage_nodes: number;
|
||||
}
|
||||
|
||||
export interface DashboardPeakHour {
|
||||
bucket_started_at: string;
|
||||
request_count: number;
|
||||
error_count: number;
|
||||
}
|
||||
|
||||
export interface DashboardPeakNode {
|
||||
node_id: string;
|
||||
node_name: string;
|
||||
request_count: number;
|
||||
error_count: number;
|
||||
cpu_usage_percent: number;
|
||||
active_event_count: number;
|
||||
openresty_status: 'healthy' | 'unhealthy' | 'unknown';
|
||||
storage_usage_percent: number;
|
||||
}
|
||||
|
||||
export interface DashboardPeakSummary {
|
||||
peak_request_hour: DashboardPeakHour;
|
||||
peak_error_hour: DashboardPeakHour;
|
||||
busiest_node: DashboardPeakNode | null;
|
||||
riskiest_node: DashboardPeakNode | null;
|
||||
}
|
||||
|
||||
export interface DistributionItem {
|
||||
key: string;
|
||||
value: number;
|
||||
@@ -134,26 +90,12 @@ export interface DashboardNodeHealth {
|
||||
unique_visitor_count: number;
|
||||
}
|
||||
|
||||
export interface DashboardAlert {
|
||||
node_id: string;
|
||||
node_name: string;
|
||||
event_type: string;
|
||||
severity: 'info' | 'warning' | 'critical';
|
||||
message: string;
|
||||
last_triggered_at: string;
|
||||
status: 'active' | 'resolved';
|
||||
}
|
||||
|
||||
export interface DashboardOverview {
|
||||
generated_at: string;
|
||||
summary: DashboardSummary;
|
||||
traffic: DashboardTraffic;
|
||||
capacity: DashboardCapacity;
|
||||
config: DashboardConfig;
|
||||
risk: DashboardRiskSummary;
|
||||
peaks: DashboardPeakSummary;
|
||||
distributions: TrafficDistributions;
|
||||
trends: DashboardTrends;
|
||||
nodes: DashboardNodeHealth[];
|
||||
active_alerts: DashboardAlert[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user