mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-02 12:39:50 +08:00
✨ feat(jvm-diagnostic): 优化诊断控制台命令体验
- 诊断命令输入使用编辑器外观并支持 Arthas 命令补全 - 新增命令执行 pending 输出、前端终态兜底和历史刷新 - 会话、输出、历史记录统一展示中文语义状态 - 补充诊断控制台和补全展示测试
This commit is contained in:
@@ -1,45 +1,166 @@
|
||||
import React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import JVMDiagnosticConsole from "./JVMDiagnosticConsole";
|
||||
import JVMDiagnosticConsole, {
|
||||
createJVMDiagnosticLocalPendingChunk,
|
||||
createJVMDiagnosticRunningRecord,
|
||||
isJVMDiagnosticTerminalPhase,
|
||||
} from "./JVMDiagnosticConsole";
|
||||
|
||||
const baseState = {
|
||||
connections: [
|
||||
{
|
||||
id: "conn-1",
|
||||
name: "orders-jvm",
|
||||
config: {
|
||||
host: "orders.internal",
|
||||
jvm: {
|
||||
diagnostic: {
|
||||
enabled: true,
|
||||
transport: "agent-bridge",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
jvmDiagnosticDrafts: {},
|
||||
jvmDiagnosticOutputs: {},
|
||||
setJVMDiagnosticDraft: vi.fn(),
|
||||
appendJVMDiagnosticOutput: vi.fn(),
|
||||
clearJVMDiagnosticOutput: vi.fn(),
|
||||
};
|
||||
|
||||
let mockState: any = baseState;
|
||||
let registeredCompletionProvider: any = null;
|
||||
const mockMonaco = {
|
||||
Range: class {
|
||||
startLineNumber: number;
|
||||
startColumn: number;
|
||||
endLineNumber: number;
|
||||
endColumn: number;
|
||||
|
||||
constructor(
|
||||
startLineNumber: number,
|
||||
startColumn: number,
|
||||
endLineNumber: number,
|
||||
endColumn: number,
|
||||
) {
|
||||
this.startLineNumber = startLineNumber;
|
||||
this.startColumn = startColumn;
|
||||
this.endLineNumber = endLineNumber;
|
||||
this.endColumn = endColumn;
|
||||
}
|
||||
},
|
||||
KeyMod: { CtrlCmd: 2048 },
|
||||
KeyCode: { Enter: 3 },
|
||||
editor: {
|
||||
setTheme: vi.fn(),
|
||||
},
|
||||
languages: {
|
||||
CompletionItemKind: {
|
||||
Keyword: 1,
|
||||
Snippet: 2,
|
||||
Value: 3,
|
||||
},
|
||||
CompletionItemInsertTextRule: {
|
||||
InsertAsSnippet: 4,
|
||||
},
|
||||
register: vi.fn(),
|
||||
registerCompletionItemProvider: vi.fn((language: string, provider: any) => {
|
||||
if (language === "jvm-diagnostic") {
|
||||
registeredCompletionProvider = provider;
|
||||
}
|
||||
return { dispose: vi.fn() };
|
||||
}),
|
||||
},
|
||||
};
|
||||
const mockEditor = {
|
||||
addCommand: vi.fn(),
|
||||
};
|
||||
|
||||
vi.mock("@monaco-editor/react", () => ({
|
||||
default: ({ language, value }: { language?: string; value?: string }) => (
|
||||
<div data-monaco-editor-mock="true" data-language={language}>
|
||||
{value}
|
||||
</div>
|
||||
),
|
||||
default: ({
|
||||
beforeMount,
|
||||
language,
|
||||
onMount,
|
||||
value,
|
||||
}: {
|
||||
beforeMount?: (monaco: any) => void;
|
||||
language?: string;
|
||||
onMount?: (editor: any, monaco: any) => void;
|
||||
value?: string;
|
||||
}) => {
|
||||
beforeMount?.(mockMonaco);
|
||||
onMount?.(mockEditor, mockMonaco);
|
||||
return (
|
||||
<div
|
||||
data-before-mount={beforeMount ? "true" : "false"}
|
||||
data-monaco-editor-mock="true"
|
||||
data-language={language}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../store", () => ({
|
||||
useStore: (selector: (state: any) => any) =>
|
||||
selector({
|
||||
connections: [
|
||||
{
|
||||
id: "conn-1",
|
||||
name: "orders-jvm",
|
||||
config: {
|
||||
host: "orders.internal",
|
||||
jvm: {
|
||||
diagnostic: {
|
||||
enabled: true,
|
||||
transport: "agent-bridge",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
jvmDiagnosticDrafts: {},
|
||||
jvmDiagnosticOutputs: {},
|
||||
setJVMDiagnosticDraft: vi.fn(),
|
||||
appendJVMDiagnosticOutput: vi.fn(),
|
||||
clearJVMDiagnosticOutput: vi.fn(),
|
||||
}),
|
||||
useStore: (selector: (state: any) => any) => selector(mockState),
|
||||
}));
|
||||
|
||||
describe("JVMDiagnosticConsole", () => {
|
||||
it("shows observe command presets by default", () => {
|
||||
beforeEach(() => {
|
||||
registeredCompletionProvider = null;
|
||||
mockMonaco.editor.setTheme.mockClear();
|
||||
mockMonaco.languages.register.mockClear();
|
||||
mockMonaco.languages.registerCompletionItemProvider.mockClear();
|
||||
mockEditor.addCommand.mockClear();
|
||||
});
|
||||
|
||||
it("builds local pending output and history while a command is waiting for backend events", () => {
|
||||
const chunk = createJVMDiagnosticLocalPendingChunk({
|
||||
sessionId: "session-1",
|
||||
commandId: "cmd-1",
|
||||
command: "thread -n 5",
|
||||
});
|
||||
const record = createJVMDiagnosticRunningRecord({
|
||||
connectionId: "conn-1",
|
||||
sessionId: "session-1",
|
||||
commandId: "cmd-1",
|
||||
transport: "arthas-tunnel",
|
||||
command: "thread -n 5",
|
||||
source: "manual",
|
||||
reason: "排查线程",
|
||||
});
|
||||
|
||||
expect(chunk).toMatchObject({
|
||||
sessionId: "session-1",
|
||||
commandId: "cmd-1",
|
||||
event: "diagnostic",
|
||||
phase: "running",
|
||||
});
|
||||
expect(chunk.content).toContain("thread -n 5");
|
||||
expect(record).toMatchObject({
|
||||
connectionId: "conn-1",
|
||||
sessionId: "session-1",
|
||||
commandId: "cmd-1",
|
||||
transport: "arthas-tunnel",
|
||||
command: "thread -n 5",
|
||||
status: "running",
|
||||
reason: "排查线程",
|
||||
});
|
||||
expect(isJVMDiagnosticTerminalPhase("completed")).toBe(true);
|
||||
expect(isJVMDiagnosticTerminalPhase("failed")).toBe(true);
|
||||
expect(isJVMDiagnosticTerminalPhase("running")).toBe(false);
|
||||
});
|
||||
|
||||
it("explains the workflow and hides command inputs before session creation", () => {
|
||||
mockState = {
|
||||
...baseState,
|
||||
jvmDiagnosticDrafts: {},
|
||||
};
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<JVMDiagnosticConsole
|
||||
tab={{
|
||||
@@ -51,10 +172,98 @@ describe("JVMDiagnosticConsole", () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain("使用流程");
|
||||
expect(markup).toContain("检查能力(可选)");
|
||||
expect(markup).toContain("先新建会话,再显示命令编辑区");
|
||||
expect(markup).not.toContain("命令模板");
|
||||
expect(markup).not.toContain('data-monaco-editor-mock="true"');
|
||||
});
|
||||
|
||||
it("shows command input, reason field, and presets after a session exists", () => {
|
||||
mockState = {
|
||||
...baseState,
|
||||
jvmDiagnosticDrafts: {
|
||||
"tab-1": {
|
||||
sessionId: "session-1",
|
||||
command: "thread -n 5",
|
||||
reason: "排查 CPU 线程",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<JVMDiagnosticConsole
|
||||
tab={{
|
||||
id: "tab-1",
|
||||
title: "诊断增强",
|
||||
type: "jvm-diagnostic",
|
||||
connectionId: "conn-1",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain("overflow:auto");
|
||||
expect(markup).toContain("JVM 诊断工作台");
|
||||
expect(markup).toContain("会话与能力");
|
||||
expect(markup).toContain("实时输出");
|
||||
expect(markup).toContain("审计历史");
|
||||
expect(markup.indexOf("命令输入")).toBeGreaterThanOrEqual(0);
|
||||
expect(markup).toContain("诊断命令");
|
||||
expect(markup).toContain("诊断原因(可选)");
|
||||
expect(markup).toContain("用于审计记录");
|
||||
expect(markup.indexOf("命令输入")).toBeLessThan(markup.indexOf("实时输出"));
|
||||
expect(markup).toContain("观察类命令");
|
||||
expect(markup).toContain("thread");
|
||||
expect(markup).toContain("执行命令");
|
||||
expect(markup).toContain('data-monaco-editor-mock="true"');
|
||||
expect(markup).toContain('data-language="jvm-diagnostic"');
|
||||
});
|
||||
|
||||
it("uses the same styled editor shell and registers command completion before mount", () => {
|
||||
mockState = {
|
||||
...baseState,
|
||||
jvmDiagnosticDrafts: {
|
||||
"tab-1": {
|
||||
sessionId: "session-1",
|
||||
command: "thr",
|
||||
reason: "排查 CPU 线程",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const markup = renderToStaticMarkup(
|
||||
<JVMDiagnosticConsole
|
||||
tab={{
|
||||
id: "tab-1",
|
||||
title: "诊断增强",
|
||||
type: "jvm-diagnostic",
|
||||
connectionId: "conn-1",
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(markup).toContain(
|
||||
'data-jvm-diagnostic-command-editor-shell="true"',
|
||||
);
|
||||
expect(markup).toContain('data-before-mount="true"');
|
||||
expect(markup).toContain("border-radius:14px");
|
||||
expect(registeredCompletionProvider).toBeTruthy();
|
||||
|
||||
const result = registeredCompletionProvider.provideCompletionItems(
|
||||
{
|
||||
getValueInRange: () => "thr",
|
||||
getWordUntilPosition: () => ({ startColumn: 1, endColumn: 4 }),
|
||||
},
|
||||
{ lineNumber: 1, column: 4 },
|
||||
);
|
||||
|
||||
expect(result.suggestions).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
label: "thread",
|
||||
insertText: "thread ",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import Editor, { type OnMount } from "@monaco-editor/react";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import Editor, { type BeforeMount, type OnMount } from "@monaco-editor/react";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
@@ -30,8 +30,7 @@ import JVMDiagnosticOutput from "./jvm/JVMDiagnosticOutput";
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
const JVM_DIAGNOSTIC_EDITOR_LANGUAGE = "jvm-diagnostic";
|
||||
let jvmDiagnosticLanguageRegistered = false;
|
||||
let jvmDiagnosticCompletionRegistered = false;
|
||||
let jvmDiagnosticCompletionDisposable: { dispose?: () => void } | null = null;
|
||||
|
||||
type JVMDiagnosticConsoleProps = {
|
||||
tab: TabData;
|
||||
@@ -41,6 +40,131 @@ const DEFAULT_COMMAND =
|
||||
JVM_DIAGNOSTIC_COMMAND_PRESETS.find((item) => item.category === "observe")
|
||||
?.command || "thread -n 5";
|
||||
|
||||
const commandEditorShellStyle = (darkMode: boolean): React.CSSProperties => ({
|
||||
borderRadius: 14,
|
||||
border: darkMode ? "1px solid #303030" : "1px solid #e6eef8",
|
||||
background: darkMode ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.04)",
|
||||
overflow: "hidden",
|
||||
});
|
||||
|
||||
const registerJVMDiagnosticMonacoSupport = (monaco: any) => {
|
||||
const languageRegistry = monaco.languages as Record<string, any>;
|
||||
if (!languageRegistry.__gonaviJvmDiagnosticLanguageRegistered) {
|
||||
languageRegistry.__gonaviJvmDiagnosticLanguageRegistered = true;
|
||||
monaco.languages.register({ id: JVM_DIAGNOSTIC_EDITOR_LANGUAGE });
|
||||
}
|
||||
|
||||
if (jvmDiagnosticCompletionDisposable?.dispose) {
|
||||
jvmDiagnosticCompletionDisposable.dispose();
|
||||
}
|
||||
|
||||
jvmDiagnosticCompletionDisposable =
|
||||
monaco.languages.registerCompletionItemProvider(
|
||||
JVM_DIAGNOSTIC_EDITOR_LANGUAGE,
|
||||
{
|
||||
triggerCharacters: [" ", "-", ".", "@", "'", "\"", "{", "/"],
|
||||
provideCompletionItems: (model: any, position: any) => {
|
||||
const textBeforeCursor = model.getValueInRange(
|
||||
new monaco.Range(1, 1, position.lineNumber, position.column),
|
||||
);
|
||||
const word = model.getWordUntilPosition(position);
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: word.startColumn,
|
||||
endColumn: word.endColumn,
|
||||
};
|
||||
|
||||
const suggestions = resolveJVMDiagnosticCompletionItems(
|
||||
textBeforeCursor,
|
||||
).map((item, index) => ({
|
||||
label: item.label,
|
||||
kind:
|
||||
item.scope === "command"
|
||||
? monaco.languages.CompletionItemKind.Keyword
|
||||
: item.isSnippet
|
||||
? monaco.languages.CompletionItemKind.Snippet
|
||||
: monaco.languages.CompletionItemKind.Value,
|
||||
insertText:
|
||||
item.scope === "command"
|
||||
? `${item.insertText} `
|
||||
: item.insertText,
|
||||
insertTextRules: item.isSnippet
|
||||
? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
|
||||
: undefined,
|
||||
detail: item.detail,
|
||||
documentation: item.documentation,
|
||||
range,
|
||||
sortText: `${item.scope === "command" ? "0" : "1"}-${String(index).padStart(3, "0")}`,
|
||||
command:
|
||||
item.scope === "command"
|
||||
? { id: "editor.action.triggerSuggest" }
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
return { suggestions };
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const isJVMDiagnosticTerminalPhase = (phase?: string): boolean =>
|
||||
["completed", "failed", "canceled"].includes(
|
||||
String(phase || "").toLowerCase().trim(),
|
||||
);
|
||||
|
||||
export const createJVMDiagnosticLocalPendingChunk = ({
|
||||
sessionId,
|
||||
commandId,
|
||||
command,
|
||||
timestamp = Date.now(),
|
||||
}: {
|
||||
sessionId: string;
|
||||
commandId: string;
|
||||
command: string;
|
||||
timestamp?: number;
|
||||
}): JVMDiagnosticEventChunk => ({
|
||||
sessionId,
|
||||
commandId,
|
||||
event: "diagnostic",
|
||||
phase: "running",
|
||||
content: `已提交诊断命令,等待后端输出:${command}`,
|
||||
timestamp,
|
||||
metadata: {
|
||||
source: "local-pending",
|
||||
},
|
||||
});
|
||||
|
||||
export const createJVMDiagnosticRunningRecord = ({
|
||||
connectionId,
|
||||
sessionId,
|
||||
commandId,
|
||||
transport,
|
||||
command,
|
||||
source,
|
||||
reason,
|
||||
timestamp = Date.now(),
|
||||
}: {
|
||||
connectionId: string;
|
||||
sessionId: string;
|
||||
commandId: string;
|
||||
transport: string;
|
||||
command: string;
|
||||
source?: string;
|
||||
reason?: string;
|
||||
timestamp?: number;
|
||||
}): JVMDiagnosticAuditRecord => ({
|
||||
timestamp,
|
||||
connectionId,
|
||||
sessionId,
|
||||
commandId,
|
||||
transport,
|
||||
command,
|
||||
source,
|
||||
reason,
|
||||
status: "running",
|
||||
});
|
||||
|
||||
const JVMDiagnosticConsole: React.FC<JVMDiagnosticConsoleProps> = ({ tab }) => {
|
||||
const connection = useStore((state) =>
|
||||
state.connections.find((item) => item.id === tab.connectionId),
|
||||
@@ -63,6 +187,17 @@ const JVMDiagnosticConsole: React.FC<JVMDiagnosticConsoleProps> = ({ tab }) => {
|
||||
const [commandRunning, setCommandRunning] = useState(false);
|
||||
const [activeCommandId, setActiveCommandId] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const activeCommandIdRef = useRef("");
|
||||
const terminalCommandIdsRef = useRef<Set<string>>(new Set());
|
||||
|
||||
const finishActiveCommand = useCallback((commandId: string) => {
|
||||
if (!commandId || activeCommandIdRef.current !== commandId) {
|
||||
return;
|
||||
}
|
||||
activeCommandIdRef.current = "";
|
||||
setCommandRunning(false);
|
||||
setActiveCommandId("");
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!draft.command) {
|
||||
@@ -93,6 +228,7 @@ const JVMDiagnosticConsole: React.FC<JVMDiagnosticConsoleProps> = ({ tab }) => {
|
||||
: null),
|
||||
[diagnosticTransport, draft.sessionId, session],
|
||||
);
|
||||
const hasSession = Boolean(effectiveSession?.sessionId);
|
||||
|
||||
const loadAuditRecords = useCallback(async () => {
|
||||
if (!connection) {
|
||||
@@ -165,14 +301,9 @@ const JVMDiagnosticConsole: React.FC<JVMDiagnosticConsoleProps> = ({ tab }) => {
|
||||
if (payload.chunk.phase === "failed") {
|
||||
setError(payload.chunk.content || "诊断命令执行失败");
|
||||
}
|
||||
if (
|
||||
payload.chunk.commandId &&
|
||||
["completed", "failed", "canceled"].includes(payload.chunk.phase || "")
|
||||
) {
|
||||
if (payload.chunk.commandId === activeCommandId) {
|
||||
setCommandRunning(false);
|
||||
setActiveCommandId("");
|
||||
}
|
||||
if (payload.chunk.commandId && isJVMDiagnosticTerminalPhase(payload.chunk.phase)) {
|
||||
terminalCommandIdsRef.current.add(payload.chunk.commandId);
|
||||
finishActiveCommand(payload.chunk.commandId);
|
||||
void loadAuditRecords();
|
||||
}
|
||||
});
|
||||
@@ -182,7 +313,7 @@ const JVMDiagnosticConsole: React.FC<JVMDiagnosticConsoleProps> = ({ tab }) => {
|
||||
stopListening();
|
||||
}
|
||||
};
|
||||
}, [activeCommandId, appendOutput, loadAuditRecords, tab.id]);
|
||||
}, [appendOutput, finishActiveCommand, loadAuditRecords, tab.id]);
|
||||
|
||||
const handleProbe = async () => {
|
||||
if (!rpcConnectionConfig) {
|
||||
@@ -201,12 +332,12 @@ const JVMDiagnosticConsole: React.FC<JVMDiagnosticConsoleProps> = ({ tab }) => {
|
||||
rpcConnectionConfig,
|
||||
);
|
||||
if (result?.success === false) {
|
||||
throw new Error(String(result?.message || "探测诊断能力失败"));
|
||||
throw new Error(String(result?.message || "检查诊断能力失败"));
|
||||
}
|
||||
setCapabilities(Array.isArray(result?.data) ? result.data : []);
|
||||
} catch (err: any) {
|
||||
setCapabilities([]);
|
||||
setError(err?.message || "探测诊断能力失败");
|
||||
setError(err?.message || "检查诊断能力失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -262,25 +393,50 @@ const JVMDiagnosticConsole: React.FC<JVMDiagnosticConsoleProps> = ({ tab }) => {
|
||||
setError("请先创建诊断会话,再执行命令");
|
||||
return;
|
||||
}
|
||||
if (!draft.command.trim()) {
|
||||
const command = draft.command.trim();
|
||||
if (!command) {
|
||||
setError("诊断命令不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
const sessionId = effectiveSession.sessionId;
|
||||
const commandId = `diag-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const source = draft.source || "manual";
|
||||
const reason = (draft.reason || "").trim();
|
||||
activeCommandIdRef.current = commandId;
|
||||
terminalCommandIdsRef.current.delete(commandId);
|
||||
setCommandRunning(true);
|
||||
setActiveCommandId(commandId);
|
||||
setError("");
|
||||
appendOutput(tab.id, [
|
||||
createJVMDiagnosticLocalPendingChunk({
|
||||
sessionId,
|
||||
commandId,
|
||||
command,
|
||||
}),
|
||||
]);
|
||||
setRecords((current) => [
|
||||
createJVMDiagnosticRunningRecord({
|
||||
connectionId: connection?.id || rpcConnectionConfig.id || "",
|
||||
sessionId,
|
||||
commandId,
|
||||
transport: diagnosticTransport,
|
||||
command,
|
||||
source,
|
||||
reason,
|
||||
}),
|
||||
...current.filter((record) => record.commandId !== commandId),
|
||||
].slice(0, 20));
|
||||
try {
|
||||
const result = await backendApp.JVMExecuteDiagnosticCommand(
|
||||
rpcConnectionConfig,
|
||||
tab.id,
|
||||
{
|
||||
sessionId: effectiveSession.sessionId,
|
||||
sessionId,
|
||||
commandId,
|
||||
command: draft.command.trim(),
|
||||
source: draft.source || "manual",
|
||||
reason: (draft.reason || "").trim(),
|
||||
command,
|
||||
source,
|
||||
reason,
|
||||
},
|
||||
);
|
||||
if (result?.success === false) {
|
||||
@@ -289,11 +445,75 @@ const JVMDiagnosticConsole: React.FC<JVMDiagnosticConsoleProps> = ({ tab }) => {
|
||||
if (result?.message) {
|
||||
message.warning(result.message);
|
||||
}
|
||||
const terminalSeen = terminalCommandIdsRef.current.has(commandId);
|
||||
if (!terminalSeen) {
|
||||
appendOutput(tab.id, [
|
||||
{
|
||||
sessionId,
|
||||
commandId,
|
||||
event: "diagnostic",
|
||||
phase: "completed",
|
||||
content: "诊断命令调用已返回,但未收到后端终态事件,前端已兜底结束等待状态。",
|
||||
timestamp: Date.now(),
|
||||
metadata: {
|
||||
source: "frontend-fallback",
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
finishActiveCommand(commandId);
|
||||
await loadAuditRecords();
|
||||
if (!terminalSeen) {
|
||||
setRecords((current) => {
|
||||
const index = current.findIndex((record) => record.commandId === commandId);
|
||||
if (index >= 0) {
|
||||
const next = [...current];
|
||||
next[index] = { ...next[index], status: "completed" };
|
||||
return next;
|
||||
}
|
||||
return [
|
||||
{
|
||||
...createJVMDiagnosticRunningRecord({
|
||||
connectionId: connection?.id || rpcConnectionConfig.id || "",
|
||||
sessionId,
|
||||
commandId,
|
||||
transport: diagnosticTransport,
|
||||
command,
|
||||
source,
|
||||
reason,
|
||||
}),
|
||||
status: "completed",
|
||||
},
|
||||
...current,
|
||||
].slice(0, 20);
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
setCommandRunning(false);
|
||||
setActiveCommandId("");
|
||||
setError(err?.message || "执行诊断命令失败");
|
||||
const messageText = err?.message || "执行诊断命令失败";
|
||||
if (!terminalCommandIdsRef.current.has(commandId)) {
|
||||
appendOutput(tab.id, [
|
||||
{
|
||||
sessionId,
|
||||
commandId,
|
||||
event: "diagnostic",
|
||||
phase: "failed",
|
||||
content: messageText,
|
||||
timestamp: Date.now(),
|
||||
metadata: {
|
||||
source: "frontend-fallback",
|
||||
},
|
||||
},
|
||||
]);
|
||||
setRecords((current) =>
|
||||
current.map((record) =>
|
||||
record.commandId === commandId
|
||||
? { ...record, status: "failed" }
|
||||
: record,
|
||||
),
|
||||
);
|
||||
}
|
||||
finishActiveCommand(commandId);
|
||||
setError(messageText);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -327,198 +547,305 @@ const JVMDiagnosticConsole: React.FC<JVMDiagnosticConsoleProps> = ({ tab }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleCommandEditorBeforeMount: BeforeMount = (monaco) => {
|
||||
registerJVMDiagnosticMonacoSupport(monaco);
|
||||
};
|
||||
|
||||
const handleCommandEditorMount: OnMount = (editor, monaco) => {
|
||||
monaco.editor.setTheme(darkMode ? "transparent-dark" : "transparent-light");
|
||||
|
||||
editor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, () => {
|
||||
void handleExecuteCommand();
|
||||
});
|
||||
|
||||
if (!jvmDiagnosticLanguageRegistered) {
|
||||
jvmDiagnosticLanguageRegistered = true;
|
||||
monaco.languages.register({ id: JVM_DIAGNOSTIC_EDITOR_LANGUAGE });
|
||||
}
|
||||
|
||||
if (!jvmDiagnosticCompletionRegistered) {
|
||||
jvmDiagnosticCompletionRegistered = true;
|
||||
monaco.languages.registerCompletionItemProvider(
|
||||
JVM_DIAGNOSTIC_EDITOR_LANGUAGE,
|
||||
{
|
||||
triggerCharacters: [" ", "-", ".", "@", "'", "\"", "{", "/"],
|
||||
provideCompletionItems: (model: any, position: any) => {
|
||||
const textBeforeCursor = model.getValueInRange(
|
||||
new monaco.Range(1, 1, position.lineNumber, position.column),
|
||||
);
|
||||
const word = model.getWordUntilPosition(position);
|
||||
const range = {
|
||||
startLineNumber: position.lineNumber,
|
||||
endLineNumber: position.lineNumber,
|
||||
startColumn: word.startColumn,
|
||||
endColumn: word.endColumn,
|
||||
};
|
||||
|
||||
const suggestions = resolveJVMDiagnosticCompletionItems(
|
||||
textBeforeCursor,
|
||||
).map((item, index) => ({
|
||||
label: item.label,
|
||||
kind:
|
||||
item.scope === "command"
|
||||
? monaco.languages.CompletionItemKind.Keyword
|
||||
: item.isSnippet
|
||||
? monaco.languages.CompletionItemKind.Snippet
|
||||
: monaco.languages.CompletionItemKind.Value,
|
||||
insertText:
|
||||
item.scope === "command"
|
||||
? `${item.insertText} `
|
||||
: item.insertText,
|
||||
insertTextRules: item.isSnippet
|
||||
? monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet
|
||||
: undefined,
|
||||
detail: item.detail,
|
||||
documentation: item.documentation,
|
||||
range,
|
||||
sortText: `${item.scope === "command" ? "0" : "1"}-${String(index).padStart(3, "0")}`,
|
||||
command:
|
||||
item.scope === "command"
|
||||
? { id: "editor.action.triggerSuggest" }
|
||||
: undefined,
|
||||
}));
|
||||
|
||||
return { suggestions };
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
if (!connection) {
|
||||
return <Empty description="连接不存在或已被删除" style={{ marginTop: 64 }} />;
|
||||
}
|
||||
|
||||
const pageBackground = darkMode
|
||||
? "linear-gradient(135deg, #101820 0%, #141414 48%, #1f1f1f 100%)"
|
||||
: "linear-gradient(135deg, #eef4ff 0%, #f7f9fc 45%, #ffffff 100%)";
|
||||
const heroBackground = darkMode
|
||||
? "linear-gradient(135deg, rgba(22,119,255,0.22), rgba(82,196,26,0.08))"
|
||||
: "linear-gradient(135deg, rgba(22,119,255,0.14), rgba(19,194,194,0.08))";
|
||||
const cardStyle = {
|
||||
borderRadius: 16,
|
||||
boxShadow: darkMode
|
||||
? "0 18px 44px rgba(0, 0, 0, 0.22)"
|
||||
: "0 18px 44px rgba(24, 54, 96, 0.08)",
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ padding: 20, display: "grid", gap: 16, height: "100%" }}
|
||||
style={{
|
||||
padding: 24,
|
||||
display: "grid",
|
||||
gap: 18,
|
||||
height: "100%",
|
||||
minHeight: 0,
|
||||
overflow: "auto",
|
||||
alignContent: "start",
|
||||
background: pageBackground,
|
||||
}}
|
||||
data-jvm-diagnostic-console="true"
|
||||
>
|
||||
<Card>
|
||||
<Space direction="vertical" size={8} style={{ width: "100%" }}>
|
||||
<Space size={8} wrap>
|
||||
<Text strong>{connection.name}</Text>
|
||||
<Tag color="blue">{diagnosticTransport}</Tag>
|
||||
{effectiveSession ? <Tag color="green">会话已建立</Tag> : <Tag>未建会话</Tag>}
|
||||
<Card
|
||||
variant="borderless"
|
||||
style={{
|
||||
...cardStyle,
|
||||
background: heroBackground,
|
||||
border: darkMode ? "1px solid rgba(255,255,255,0.08)" : "1px solid rgba(22,119,255,0.12)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "minmax(0, 1fr) auto",
|
||||
gap: 18,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<Text type="secondary">JVM Diagnostics</Text>
|
||||
<Typography.Title level={3} style={{ margin: "4px 0 8px" }}>
|
||||
JVM 诊断工作台
|
||||
</Typography.Title>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
||||
<Text strong>{connection.name}</Text>
|
||||
<Text type="secondary">
|
||||
{" "}· {connection.config.host || "unknown"}:{connection.config.port || 0}
|
||||
{" "}· {diagnosticTransport}
|
||||
</Text>
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<Space wrap style={{ justifyContent: "flex-end" }}>
|
||||
<Tag color={hasSession ? "green" : "default"}>
|
||||
{hasSession ? "会话已建立" : "未建会话"}
|
||||
</Tag>
|
||||
{commandRunning ? <Tag color="processing">命令执行中</Tag> : null}
|
||||
</Space>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
||||
先创建诊断会话,再执行命令;AI 回复中的结构化诊断计划可以一键回填到这里,执行结果会实时流入输出面板。
|
||||
</Paragraph>
|
||||
<Space wrap>
|
||||
<Button size="small" onClick={() => void handleProbe()} loading={loading}>
|
||||
探测能力
|
||||
<Button onClick={() => void handleProbe()} loading={loading}>
|
||||
检查能力
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
type={hasSession ? "default" : "primary"}
|
||||
onClick={() => void handleStartSession()}
|
||||
loading={loading}
|
||||
>
|
||||
新建会话
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
onClick={() => void handleExecuteCommand()}
|
||||
loading={commandRunning}
|
||||
>
|
||||
执行命令
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
disabled={!commandRunning || !effectiveSession?.sessionId || !activeCommandId}
|
||||
onClick={() => void handleCancelCommand()}
|
||||
loading={loading && commandRunning}
|
||||
>
|
||||
取消命令
|
||||
</Button>
|
||||
<Button size="small" onClick={() => clearOutput(tab.id)}>
|
||||
清空输出
|
||||
</Button>
|
||||
<Button size="small" onClick={() => void loadAuditRecords()} loading={historyLoading}>
|
||||
刷新历史
|
||||
{hasSession ? "重建会话" : "新建会话"}
|
||||
</Button>
|
||||
{hasSession ? (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => void handleExecuteCommand()}
|
||||
loading={commandRunning}
|
||||
>
|
||||
执行命令
|
||||
</Button>
|
||||
) : null}
|
||||
{hasSession ? (
|
||||
<Button
|
||||
danger
|
||||
disabled={!commandRunning || !effectiveSession?.sessionId || !activeCommandId}
|
||||
onClick={() => void handleCancelCommand()}
|
||||
loading={loading && commandRunning}
|
||||
>
|
||||
取消命令
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
{error ? <Alert type="error" showIcon message={error} /> : null}
|
||||
{capabilities.length ? (
|
||||
<Space size={8} wrap>
|
||||
{capabilities.map((item) => (
|
||||
<Tag key={item.transport} color="processing">
|
||||
{item.transport}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
) : null}
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card title="命令模板">
|
||||
<JVMCommandPresetBar
|
||||
onSelectPreset={(preset) =>
|
||||
setDraft(tab.id, {
|
||||
command: preset.command,
|
||||
reason: preset.description,
|
||||
source: "manual",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card title="命令输入">
|
||||
<div style={{ display: "grid", gap: 12 }}>
|
||||
<Editor
|
||||
height={180}
|
||||
language={JVM_DIAGNOSTIC_EDITOR_LANGUAGE}
|
||||
theme={darkMode ? "transparent-dark" : "transparent-light"}
|
||||
value={draft.command}
|
||||
onMount={handleCommandEditorMount}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
automaticLayout: true,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: "on",
|
||||
quickSuggestions: true,
|
||||
suggestOnTriggerCharacters: true,
|
||||
lineNumbers: "off",
|
||||
folding: false,
|
||||
glyphMargin: false,
|
||||
}}
|
||||
onChange={(value) =>
|
||||
setDraft(tab.id, {
|
||||
command: value || "",
|
||||
source: "manual",
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
value={draft.reason || ""}
|
||||
placeholder="输入诊断原因,便于审计和 AI 上下文理解"
|
||||
onChange={(event) => setDraft(tab.id, { reason: event.target.value })}
|
||||
/>
|
||||
</div>
|
||||
{error ? <Alert type="error" showIcon message={error} style={{ marginTop: 16 }} /> : null}
|
||||
</Card>
|
||||
|
||||
{!hasSession ? (
|
||||
<Card title="使用流程" variant="borderless" style={cardStyle}>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 220px), 1fr))",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
{[
|
||||
["1", "检查能力(可选)", "读取诊断通道、流式输出和命令权限,不创建会话、不执行命令。"],
|
||||
["2", "新建会话", "创建一次诊断上下文。Arthas Tunnel 的目标连接会在测试或执行命令时发生。"],
|
||||
["3", "执行命令", "先新建会话,再显示命令编辑区;会话创建后才显示原因输入和模板区。"],
|
||||
].map(([index, title, description]) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
padding: 14,
|
||||
borderRadius: 14,
|
||||
border: darkMode ? "1px solid #303030" : "1px solid #e6eef8",
|
||||
background: darkMode ? "rgba(255,255,255,0.03)" : "rgba(255,255,255,0.72)",
|
||||
}}
|
||||
>
|
||||
<Tag color="blue">{index}</Tag>
|
||||
<Text strong>{title}</Text>
|
||||
<Paragraph type="secondary" style={{ margin: "8px 0 0" }}>
|
||||
{description}
|
||||
</Paragraph>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 360px), 1fr))",
|
||||
gap: 18,
|
||||
alignItems: "start",
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "grid", gap: 18, minWidth: 0 }}>
|
||||
<Card title="命令输入" variant="borderless" style={cardStyle}>
|
||||
<div style={{ display: "grid", gap: 14 }}>
|
||||
<div style={{ display: "grid", gap: 6 }}>
|
||||
<Text strong>诊断命令</Text>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
||||
输入 Arthas/诊断命令,例如 thread -n 5、dashboard、jvm;也可以从下方模板一键回填。按 Ctrl/Cmd + Enter 可执行。
|
||||
</Paragraph>
|
||||
<div
|
||||
data-jvm-diagnostic-command-editor-shell="true"
|
||||
style={commandEditorShellStyle(darkMode)}
|
||||
>
|
||||
<Editor
|
||||
beforeMount={handleCommandEditorBeforeMount}
|
||||
height={220}
|
||||
language={JVM_DIAGNOSTIC_EDITOR_LANGUAGE}
|
||||
theme={
|
||||
darkMode ? "transparent-dark" : "transparent-light"
|
||||
}
|
||||
value={draft.command}
|
||||
onMount={handleCommandEditorMount}
|
||||
options={{
|
||||
minimap: { enabled: false },
|
||||
fontSize: 13,
|
||||
automaticLayout: true,
|
||||
scrollBeyondLastLine: false,
|
||||
wordWrap: "on",
|
||||
quickSuggestions: {
|
||||
other: true,
|
||||
comments: false,
|
||||
strings: true,
|
||||
},
|
||||
suggestOnTriggerCharacters: true,
|
||||
lineNumbers: "off",
|
||||
folding: false,
|
||||
glyphMargin: false,
|
||||
renderLineHighlight: "all",
|
||||
roundedSelection: true,
|
||||
}}
|
||||
onChange={(value) =>
|
||||
setDraft(tab.id, {
|
||||
command: value || "",
|
||||
source: "manual",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "grid", gap: 6 }}>
|
||||
<Text strong>诊断原因(可选)</Text>
|
||||
<Input
|
||||
value={draft.reason || ""}
|
||||
placeholder="例如:排查 CPU 飙高、确认线程阻塞、定位慢方法"
|
||||
onChange={(event) => setDraft(tab.id, { reason: event.target.value })}
|
||||
/>
|
||||
<Text type="secondary">
|
||||
用于审计记录和 AI 上下文理解,不会作为 Arthas 命令发送到目标 JVM。
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="命令模板" variant="borderless" style={cardStyle}>
|
||||
<JVMCommandPresetBar
|
||||
onSelectPreset={(preset) =>
|
||||
setDraft(tab.id, {
|
||||
command: preset.command,
|
||||
reason: preset.description,
|
||||
source: "manual",
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card title="会话与能力" variant="borderless" style={cardStyle}>
|
||||
<Space direction="vertical" size={12} style={{ width: "100%" }}>
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
<Tag color="blue">{effectiveSession?.sessionId}</Tag>
|
||||
<Tag>{effectiveSession?.transport || diagnosticTransport}</Tag>
|
||||
<Tag color={commandRunning ? "processing" : "green"}>
|
||||
{commandRunning ? "命令执行中" : "空闲"}
|
||||
</Tag>
|
||||
</div>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
||||
检查能力只读取通道权限;执行命令前必须先建会话。输出优先看下方“实时输出”,审计记录看“审计历史”。
|
||||
</Paragraph>
|
||||
<Space wrap>
|
||||
<Button size="small" onClick={() => clearOutput(tab.id)}>
|
||||
清空输出
|
||||
</Button>
|
||||
<Button size="small" onClick={() => void loadAuditRecords()} loading={historyLoading}>
|
||||
刷新历史
|
||||
</Button>
|
||||
</Space>
|
||||
{capabilities.length ? (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="能力检查结果"
|
||||
description={
|
||||
<Space size={8} wrap>
|
||||
{capabilities.map((item) => (
|
||||
<Space key={item.transport} size={4} wrap>
|
||||
<Tag color="processing">{item.transport}</Tag>
|
||||
<Tag color={item.canOpenSession ? "green" : "red"}>
|
||||
{item.canOpenSession ? "可建会话" : "不可建会话"}
|
||||
</Tag>
|
||||
<Tag color={item.canStream ? "green" : "red"}>
|
||||
{item.canStream ? "支持流式输出" : "不支持流式输出"}
|
||||
</Tag>
|
||||
<Tag color={item.allowObserveCommands ? "green" : "red"}>
|
||||
{item.allowObserveCommands ? "允许观察命令" : "禁止观察命令"}
|
||||
</Tag>
|
||||
{item.allowTraceCommands ? <Tag color="gold">允许 Trace</Tag> : null}
|
||||
{item.allowMutatingCommands ? <Tag color="red">允许变更类命令</Tag> : null}
|
||||
</Space>
|
||||
))}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="尚未检查能力"
|
||||
description="如需确认当前连接是否允许 observe/trace/高风险命令,可点击顶部“检查能力”。"
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gap: 16,
|
||||
gridTemplateColumns: "minmax(0, 2fr) minmax(320px, 1fr)",
|
||||
gap: 18,
|
||||
gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 360px), 1fr))",
|
||||
alignItems: "start",
|
||||
}}
|
||||
>
|
||||
<Card title="输出面板">
|
||||
<Card title="实时输出" variant="borderless" style={cardStyle}>
|
||||
<JVMDiagnosticOutput chunks={chunks} />
|
||||
</Card>
|
||||
<Card title="会话与历史">
|
||||
<Card title="审计历史" variant="borderless" style={cardStyle}>
|
||||
<JVMDiagnosticHistory session={effectiveSession} records={records} />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,13 @@ import type {
|
||||
JVMDiagnosticAuditRecord,
|
||||
JVMDiagnosticSessionHandle,
|
||||
} from "../../types";
|
||||
import {
|
||||
formatJVMDiagnosticCommandTypeLabel,
|
||||
formatJVMDiagnosticRiskLabel,
|
||||
formatJVMDiagnosticSourceLabel,
|
||||
formatJVMDiagnosticPhaseLabel,
|
||||
formatJVMDiagnosticTransportLabel,
|
||||
} from "../../utils/jvmDiagnosticPresentation";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -23,7 +30,7 @@ const JVMDiagnosticHistory: React.FC<JVMDiagnosticHistoryProps> = ({
|
||||
{session ? (
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
<Tag color="blue">{session.sessionId}</Tag>
|
||||
<Tag>{session.transport}</Tag>
|
||||
<Tag>{formatJVMDiagnosticTransportLabel(session.transport)}</Tag>
|
||||
</div>
|
||||
) : (
|
||||
<Empty
|
||||
@@ -55,10 +62,16 @@ const JVMDiagnosticHistory: React.FC<JVMDiagnosticHistoryProps> = ({
|
||||
{record.command}
|
||||
</Text>
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
{record.status ? <Tag color="green">{record.status}</Tag> : null}
|
||||
{record.riskLevel ? <Tag color="gold">{record.riskLevel}</Tag> : null}
|
||||
{record.commandType ? <Tag color="blue">{record.commandType}</Tag> : null}
|
||||
{record.source ? <Tag>{record.source}</Tag> : null}
|
||||
{record.status ? (
|
||||
<Tag color="green">{formatJVMDiagnosticPhaseLabel(record.status)}</Tag>
|
||||
) : null}
|
||||
{record.riskLevel ? (
|
||||
<Tag color="gold">{formatJVMDiagnosticRiskLabel(record.riskLevel)}</Tag>
|
||||
) : null}
|
||||
{record.commandType ? (
|
||||
<Tag color="blue">{formatJVMDiagnosticCommandTypeLabel(record.commandType)}</Tag>
|
||||
) : null}
|
||||
{record.source ? <Tag>{formatJVMDiagnosticSourceLabel(record.source)}</Tag> : null}
|
||||
</div>
|
||||
<Text type="secondary">
|
||||
{record.reason || "未填写诊断原因"}
|
||||
|
||||
@@ -2,7 +2,11 @@ import React from "react";
|
||||
import { Empty, List, Tag, Typography } from "antd";
|
||||
|
||||
import type { JVMDiagnosticEventChunk } from "../../types";
|
||||
import { formatJVMDiagnosticChunkText } from "../../utils/jvmDiagnosticPresentation";
|
||||
import {
|
||||
formatJVMDiagnosticChunkText,
|
||||
formatJVMDiagnosticEventLabel,
|
||||
formatJVMDiagnosticPhaseLabel,
|
||||
} from "../../utils/jvmDiagnosticPresentation";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
@@ -35,8 +39,10 @@ const JVMDiagnosticOutput: React.FC<JVMDiagnosticOutputProps> = ({ chunks }) =>
|
||||
{formatJVMDiagnosticChunkText(chunk)}
|
||||
</Text>
|
||||
<div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
|
||||
{chunk.phase ? <Tag color="geekblue">{chunk.phase}</Tag> : null}
|
||||
{chunk.event ? <Tag>{chunk.event}</Tag> : null}
|
||||
{chunk.phase ? (
|
||||
<Tag color="geekblue">{formatJVMDiagnosticPhaseLabel(chunk.phase)}</Tag>
|
||||
) : null}
|
||||
{chunk.event ? <Tag>{formatJVMDiagnosticEventLabel(chunk.event)}</Tag> : null}
|
||||
{chunk.commandId ? <Tag color="blue">{chunk.commandId}</Tag> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,12 @@ describe("jvmDiagnosticCompletion", () => {
|
||||
expect(items.some((item) => item.label === "trace")).toBe(true);
|
||||
});
|
||||
|
||||
it("suggests the jvm command from the command input hint", () => {
|
||||
const items = resolveJVMDiagnosticCompletionItems("jv");
|
||||
|
||||
expect(items.some((item) => item.label === "jvm")).toBe(true);
|
||||
});
|
||||
|
||||
it("switches to argument mode after the command head", () => {
|
||||
expect(resolveJVMDiagnosticCompletionMode("thread -")).toEqual({
|
||||
head: "thread",
|
||||
|
||||
@@ -29,6 +29,11 @@ const BASE_COMMAND_DEFINITIONS: DiagnosticCommandDefinition[] = [
|
||||
detail: "观察类命令",
|
||||
documentation: "查看 JVM 运行总览。",
|
||||
},
|
||||
{
|
||||
head: "jvm",
|
||||
detail: "观察类命令",
|
||||
documentation: "查看 JVM 内存、线程、类加载、GC 和运行参数信息。",
|
||||
},
|
||||
{
|
||||
head: "thread",
|
||||
detail: "观察类命令",
|
||||
@@ -158,6 +163,15 @@ const ARGUMENT_ITEMS_BY_HEAD: Record<string, JVMDiagnosticCompletionItem[]> = {
|
||||
scope: "argument",
|
||||
},
|
||||
],
|
||||
jvm: [
|
||||
{
|
||||
label: "jvm",
|
||||
insertText: "",
|
||||
detail: "直接执行",
|
||||
documentation: "查看 JVM 内存、线程、类加载、GC 和运行参数信息。",
|
||||
scope: "argument",
|
||||
},
|
||||
],
|
||||
thread: [
|
||||
{
|
||||
label: "繁忙线程 TOP N (-n)",
|
||||
|
||||
@@ -2,6 +2,11 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
formatJVMDiagnosticChunkText,
|
||||
formatJVMDiagnosticCommandTypeLabel,
|
||||
formatJVMDiagnosticPhaseLabel,
|
||||
formatJVMDiagnosticRiskLabel,
|
||||
formatJVMDiagnosticSourceLabel,
|
||||
formatJVMDiagnosticTransportLabel,
|
||||
groupJVMDiagnosticPresets,
|
||||
resolveJVMDiagnosticRiskColor,
|
||||
} from "./jvmDiagnosticPresentation";
|
||||
@@ -17,14 +22,22 @@ describe("jvmDiagnosticPresentation", () => {
|
||||
expect(groups[0].items.some((item) => item.label === "thread")).toBe(true);
|
||||
});
|
||||
|
||||
it("formats chunk text with phase prefix when content exists", () => {
|
||||
it("formats chunk text with localized phase prefix when content exists", () => {
|
||||
expect(
|
||||
formatJVMDiagnosticChunkText({
|
||||
sessionId: "sess-1",
|
||||
phase: "running",
|
||||
content: "thread -n 5",
|
||||
}),
|
||||
).toBe("running: thread -n 5");
|
||||
).toBe("执行中:thread -n 5");
|
||||
});
|
||||
|
||||
it("localizes diagnostic status, transport, risk and source labels", () => {
|
||||
expect(formatJVMDiagnosticPhaseLabel("completed")).toBe("已完成");
|
||||
expect(formatJVMDiagnosticTransportLabel("arthas-tunnel")).toBe("Arthas Tunnel");
|
||||
expect(formatJVMDiagnosticRiskLabel("high")).toBe("高风险");
|
||||
expect(formatJVMDiagnosticCommandTypeLabel("trace")).toBe("跟踪类");
|
||||
expect(formatJVMDiagnosticSourceLabel("ai-plan")).toBe("AI 计划");
|
||||
});
|
||||
|
||||
it("maps risk levels to tag colors", () => {
|
||||
|
||||
@@ -66,6 +66,43 @@ const RISK_COLORS: Record<"low" | "medium" | "high", string> = {
|
||||
high: "red",
|
||||
};
|
||||
|
||||
const PHASE_LABELS: Record<string, string> = {
|
||||
running: "执行中",
|
||||
completed: "已完成",
|
||||
failed: "失败",
|
||||
canceled: "已取消",
|
||||
canceling: "取消中",
|
||||
diagnostic: "诊断事件",
|
||||
};
|
||||
|
||||
const EVENT_LABELS: Record<string, string> = {
|
||||
diagnostic: "诊断输出",
|
||||
chunk: "输出片段",
|
||||
done: "执行结束",
|
||||
};
|
||||
|
||||
const TRANSPORT_LABELS: Record<string, string> = {
|
||||
"agent-bridge": "Agent Bridge",
|
||||
"arthas-tunnel": "Arthas Tunnel",
|
||||
};
|
||||
|
||||
const RISK_LABELS: Record<string, string> = {
|
||||
low: "低风险",
|
||||
medium: "中风险",
|
||||
high: "高风险",
|
||||
};
|
||||
|
||||
const COMMAND_TYPE_LABELS: Record<string, string> = {
|
||||
observe: "观察类",
|
||||
trace: "跟踪类",
|
||||
mutating: "高风险类",
|
||||
};
|
||||
|
||||
const SOURCE_LABELS: Record<string, string> = {
|
||||
manual: "手动输入",
|
||||
"ai-plan": "AI 计划",
|
||||
};
|
||||
|
||||
export const formatJVMDiagnosticPresetCategory = (
|
||||
category: JVMDiagnosticPresetCategory,
|
||||
): string => CATEGORY_LABELS[category];
|
||||
@@ -74,6 +111,41 @@ export const resolveJVMDiagnosticRiskColor = (
|
||||
riskLevel: "low" | "medium" | "high",
|
||||
): string => RISK_COLORS[riskLevel];
|
||||
|
||||
const normalizeLabelKey = (value?: string | null): string =>
|
||||
String(value || "").trim().toLowerCase();
|
||||
|
||||
const formatWithFallback = (
|
||||
value: string | undefined | null,
|
||||
labels: Record<string, string>,
|
||||
fallback = "未知",
|
||||
): string => {
|
||||
const normalized = normalizeLabelKey(value);
|
||||
if (!normalized) {
|
||||
return fallback;
|
||||
}
|
||||
return labels[normalized] || String(value || "").trim();
|
||||
};
|
||||
|
||||
export const formatJVMDiagnosticPhaseLabel = (phase?: string | null): string =>
|
||||
formatWithFallback(phase, PHASE_LABELS);
|
||||
|
||||
export const formatJVMDiagnosticEventLabel = (event?: string | null): string =>
|
||||
formatWithFallback(event, EVENT_LABELS);
|
||||
|
||||
export const formatJVMDiagnosticTransportLabel = (
|
||||
transport?: string | null,
|
||||
): string => formatWithFallback(transport, TRANSPORT_LABELS);
|
||||
|
||||
export const formatJVMDiagnosticRiskLabel = (risk?: string | null): string =>
|
||||
formatWithFallback(risk, RISK_LABELS);
|
||||
|
||||
export const formatJVMDiagnosticCommandTypeLabel = (
|
||||
type?: string | null,
|
||||
): string => formatWithFallback(type, COMMAND_TYPE_LABELS);
|
||||
|
||||
export const formatJVMDiagnosticSourceLabel = (source?: string | null): string =>
|
||||
formatWithFallback(source, SOURCE_LABELS);
|
||||
|
||||
export const groupJVMDiagnosticPresets = (
|
||||
presets: JVMDiagnosticCommandPreset[] = JVM_DIAGNOSTIC_COMMAND_PRESETS,
|
||||
): Array<{
|
||||
@@ -90,16 +162,19 @@ export const groupJVMDiagnosticPresets = (
|
||||
export const formatJVMDiagnosticChunkText = (
|
||||
chunk: JVMDiagnosticEventChunk,
|
||||
): string => {
|
||||
const phase = String(chunk.phase || chunk.event || "").trim();
|
||||
const rawPhase = String(chunk.phase || chunk.event || "").trim();
|
||||
const phase = chunk.phase
|
||||
? formatJVMDiagnosticPhaseLabel(chunk.phase)
|
||||
: formatJVMDiagnosticEventLabel(chunk.event);
|
||||
const content = String(chunk.content || "").trim();
|
||||
if (!phase && !content) {
|
||||
if (!rawPhase && !content) {
|
||||
return "空事件";
|
||||
}
|
||||
if (!phase) {
|
||||
if (!rawPhase) {
|
||||
return content;
|
||||
}
|
||||
if (!content) {
|
||||
return phase;
|
||||
}
|
||||
return `${phase}: ${content}`;
|
||||
return `${phase}:${content}`;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user