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,41 @@
import { describe, expect, it } from "vitest";
import {
buildMonitoringAvailabilityText,
formatMonitoringAxisBytes,
formatRecentGCLabel,
normalizeMonitoringProviderMode,
} from "./jvmMonitoringPresentation";
describe("jvmMonitoringPresentation", () => {
it("summarizes degraded metrics with missing items and warnings", () => {
expect(
buildMonitoringAvailabilityText({
missingMetrics: ["cpu.process", "memory.rss"],
providerWarnings: ["endpoint cpu metric unavailable"],
}),
).toContain("缺失指标");
});
it("formats recent gc event label with duration", () => {
expect(
formatRecentGCLabel({
timestamp: 1713945600000,
name: "G1 Young Generation",
durationMs: 21,
}),
).toContain("21ms");
});
it("formats byte axis ticks with compact units instead of raw byte numbers", () => {
expect(formatMonitoringAxisBytes(120_000_000)).toBe("114 MB");
expect(formatMonitoringAxisBytes(0)).toBe("0 B");
expect(formatMonitoringAxisBytes(undefined)).toBe("--");
});
it("normalizes provider mode and falls back on unknown values", () => {
expect(normalizeMonitoringProviderMode("AGENT", "jmx")).toBe("agent");
expect(normalizeMonitoringProviderMode("unsupported", "endpoint")).toBe("endpoint");
expect(normalizeMonitoringProviderMode(undefined, "jmx")).toBe("jmx");
});
});

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);

View File

@@ -1,5 +1,5 @@
export type JVMRuntimeMode = 'jmx' | 'endpoint' | 'agent';
export type JVMTabKind = 'overview' | 'resource' | 'audit' | 'diagnostic';
export type JVMTabKind = 'overview' | 'resource' | 'audit' | 'diagnostic' | 'monitoring';
export type JVMModeMeta = {
mode: string;
@@ -36,6 +36,7 @@ const JVM_TAB_KIND_LABELS: Record<JVMTabKind, string> = {
resource: 'JVM 资源',
audit: 'JVM 审计',
diagnostic: 'JVM 诊断',
monitoring: 'JVM 监控',
};
const normalizeMode = (mode: string): string => String(mode || '').trim().toLowerCase();

View File

@@ -0,0 +1,64 @@
import { describe, expect, it } from "vitest";
import {
buildJVMDiagnosticActionDescriptor,
buildJVMMonitoringActionDescriptors,
} from "./jvmSidebarActions";
describe("jvmSidebarActions", () => {
it("builds direct JVM monitoring entries from probed provider capabilities", () => {
expect(
buildJVMMonitoringActionDescriptors("conn-1", [
{ mode: "jmx" },
{ mode: "endpoint" },
{ mode: "jmx" },
]),
).toEqual([
{
key: "conn-1-jvm-monitoring-jmx",
title: "持续监控 · JMX",
providerMode: "jmx",
},
{
key: "conn-1-jvm-monitoring-endpoint",
title: "持续监控 · Endpoint",
providerMode: "endpoint",
},
]);
});
it("skips providers that cannot be browsed when building monitoring entries", () => {
expect(
buildJVMMonitoringActionDescriptors("conn-1", [
{ mode: "jmx", canBrowse: true },
{ mode: "agent", canBrowse: false },
]),
).toEqual([
{
key: "conn-1-jvm-monitoring-jmx",
title: "持续监控 · JMX",
providerMode: "jmx",
},
]);
});
it("builds diagnostic entry independently from provider probing", () => {
expect(
buildJVMDiagnosticActionDescriptor("conn-1", {
enabled: true,
transport: "arthas-tunnel",
}),
).toEqual({
key: "conn-1-jvm-diagnostic",
title: "诊断增强 · Arthas Tunnel",
transport: "arthas-tunnel",
});
expect(
buildJVMDiagnosticActionDescriptor("conn-1", {
enabled: false,
transport: "agent-bridge",
}),
).toBeNull();
});
});

View File

@@ -0,0 +1,77 @@
import type { JVMCapability } from "../types";
import {
JVM_RUNTIME_MODES,
resolveJVMModeMeta,
type JVMRuntimeMode,
} from "./jvmRuntimePresentation";
export type JVMMonitoringActionDescriptor = {
key: string;
title: string;
providerMode: JVMRuntimeMode;
};
export type JVMDiagnosticActionDescriptor = {
key: string;
title: string;
transport: "agent-bridge" | "arthas-tunnel";
};
const normalizeMonitoringMode = (value: unknown): JVMRuntimeMode | null => {
const mode = String(value || "").trim().toLowerCase();
return JVM_RUNTIME_MODES.includes(mode as JVMRuntimeMode)
? (mode as JVMRuntimeMode)
: null;
};
export const buildJVMMonitoringActionDescriptors = (
connectionId: string,
capabilities: Array<Pick<JVMCapability, "mode"> & Partial<Pick<JVMCapability, "canBrowse">>>,
): JVMMonitoringActionDescriptor[] => {
const id = String(connectionId || "").trim();
if (!id) {
return [];
}
const seen = new Set<JVMRuntimeMode>();
const descriptors: JVMMonitoringActionDescriptor[] = [];
capabilities.forEach((capability) => {
if (capability.canBrowse === false) {
return;
}
const providerMode = normalizeMonitoringMode(capability.mode);
if (!providerMode || seen.has(providerMode)) {
return;
}
seen.add(providerMode);
descriptors.push({
key: `${id}-jvm-monitoring-${providerMode}`,
title: `持续监控 · ${resolveJVMModeMeta(providerMode).label}`,
providerMode,
});
});
return descriptors;
};
export const buildJVMDiagnosticActionDescriptor = (
connectionId: string,
diagnostic: { enabled?: boolean; transport?: unknown } | undefined,
): JVMDiagnosticActionDescriptor | null => {
const id = String(connectionId || "").trim();
if (!id || diagnostic?.enabled !== true) {
return null;
}
const transport =
String(diagnostic.transport || "").trim() === "arthas-tunnel"
? "arthas-tunnel"
: "agent-bridge";
return {
key: `${id}-jvm-diagnostic`,
title: `诊断增强 · ${transport === "arthas-tunnel" ? "Arthas Tunnel" : "Agent Bridge"}`,
transport,
};
};