feat(jvm-ui): 完善 JVM 工作台与监控入口

- 新增 JVM 持续监控仪表盘、图表、状态卡和详情面板

- 统一概览、资源浏览、审计页面的 JVM 工作台布局

- Sidebar 和 TabManager 支持监控入口、诊断入口兜底和上下文切换

- 补充前端状态模型、展示文案和组件回归测试
This commit is contained in:
Syngnat
2026-04-26 14:34:02 +08:00
parent 9d08b185d0
commit ff2b86819d
25 changed files with 2260 additions and 274 deletions

View File

@@ -0,0 +1,176 @@
import type {
JVMMonitoringPoint,
JVMMonitoringRecentGCEvent,
JVMMonitoringSessionState,
} from "../types";
const METRIC_LABELS: Record<string, string> = {
"heap.used": "堆内存",
"heap.non_heap": "非堆内存",
"gc.count": "垃圾回收次数",
"gc.time": "垃圾回收耗时",
"gc.events": "最近垃圾回收事件",
"thread.count": "线程数",
"thread.states": "线程状态",
"class.loading": "类加载",
"cpu.process": "进程 CPU",
"cpu.system": "系统 CPU",
"memory.rss": "进程物理内存",
"memory.virtual": "进程虚拟内存",
};
export type JVMMonitoringProviderMode = JVMMonitoringSessionState["providerMode"];
const MONITORING_PROVIDER_MODES: JVMMonitoringProviderMode[] = [
"jmx",
"endpoint",
"agent",
];
const THREAD_STATE_LABELS: Record<string, string> = {
NEW: "新建",
RUNNABLE: "可运行",
BLOCKED: "阻塞",
WAITING: "等待中",
TIMED_WAITING: "限时等待",
TERMINATED: "已终止",
};
const timeFormatter = new Intl.DateTimeFormat("zh-CN", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
});
export type MonitoringChartPoint = JVMMonitoringPoint & {
timeLabel: string;
};
export const resolveMonitoringMetricLabel = (metric: string): string =>
METRIC_LABELS[String(metric || "").trim()] || String(metric || "").trim();
export const resolveThreadStateLabel = (state?: string | null): string => {
const normalized = String(state || "").trim().toUpperCase();
return THREAD_STATE_LABELS[normalized] || String(state || "").trim();
};
export const formatMonitoringTime = (timestamp?: number): string => {
if (typeof timestamp !== "number" || !Number.isFinite(timestamp)) {
return "--";
}
return timeFormatter.format(new Date(timestamp));
};
export const formatBytes = (value?: number): string => {
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
return "--";
}
const units = ["B", "KB", "MB", "GB", "TB"];
let next = value;
let unitIndex = 0;
while (next >= 1024 && unitIndex < units.length - 1) {
next /= 1024;
unitIndex += 1;
}
const precision = next >= 100 || unitIndex === 0 ? 0 : next >= 10 ? 1 : 2;
return `${next.toFixed(precision)} ${units[unitIndex]}`;
};
export const formatMonitoringAxisBytes = (value?: number): string => formatBytes(value);
export const formatPercent = (value?: number): string => {
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
return "--";
}
return `${(value * 100).toFixed(1)}%`;
};
export const formatCompactNumber = (value?: number): string => {
if (typeof value !== "number" || !Number.isFinite(value)) {
return "--";
}
return value.toLocaleString("zh-CN");
};
export const formatDurationMs = (value?: number): string => {
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
return "--";
}
return `${Math.round(value)}ms`;
};
export const normalizeMonitoringProviderMode = (
value: unknown,
fallback: JVMMonitoringProviderMode = "jmx",
): JVMMonitoringProviderMode => {
const normalized = String(value || "").trim().toLowerCase();
if (MONITORING_PROVIDER_MODES.includes(normalized as JVMMonitoringProviderMode)) {
return normalized as JVMMonitoringProviderMode;
}
return MONITORING_PROVIDER_MODES.includes(fallback) ? fallback : "jmx";
};
export const buildMonitoringAvailabilityText = ({
missingMetrics,
providerWarnings,
}: Pick<JVMMonitoringSessionState, "missingMetrics" | "providerWarnings">): string => {
const fragments: string[] = [];
if (Array.isArray(missingMetrics) && missingMetrics.length > 0) {
fragments.push(
`缺失指标:${missingMetrics
.map((metric) => resolveMonitoringMetricLabel(metric))
.join("、")}`,
);
}
if (Array.isArray(providerWarnings) && providerWarnings.length > 0) {
fragments.push(`监控来源告警:${providerWarnings.join("")}`);
}
if (fragments.length === 0) {
return "当前监控会话未发现明显降级。";
}
return fragments.join(" | ");
};
export const formatRecentGCLabel = (
event: JVMMonitoringRecentGCEvent,
): string => {
const parts = [
formatMonitoringTime(event.timestamp),
String(event.name || "").trim(),
typeof event.durationMs === "number" ? `${event.durationMs}ms` : "",
String(event.cause || "").trim(),
].filter(Boolean);
return parts.join(" · ");
};
export const buildMonitoringChartPoints = (
points: JVMMonitoringPoint[] = [],
): MonitoringChartPoint[] =>
points.map((point) => ({
...point,
timeLabel: formatMonitoringTime(point.timestamp),
}));
export const extractThreadStateRows = (
point?: JVMMonitoringPoint,
): Array<{ state: string; label: string; count: number }> =>
Object.entries(point?.threadStateCounts || {})
.map(([state, count]) => ({
state,
label: resolveThreadStateLabel(state),
count: Number(count) || 0,
}))
.sort((left, right) => right.count - left.count);
export const monitoringMetricAvailable = (
session: Pick<JVMMonitoringSessionState, "availableMetrics"> | undefined,
metric: string,
): boolean =>
Array.isArray(session?.availableMetrics) &&
session.availableMetrics.includes(metric);