mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-21 05:53:46 +08:00
- Monaco Editor 改为首次使用时按需初始化 - AI 面板改为懒加载,延后加载 Markdown 和图表渲染依赖 - 增加 Windows 低内存视觉模式,支持关闭透明 WebView 和 Acrylic - 补充低内存启动说明与模式解析测试
1040 lines
32 KiB
TypeScript
1040 lines
32 KiB
TypeScript
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||
import Editor from "./MonacoEditor";
|
||
import {
|
||
Alert,
|
||
Button,
|
||
Card,
|
||
Descriptions,
|
||
Empty,
|
||
Input,
|
||
Skeleton,
|
||
Space,
|
||
Tag,
|
||
Typography,
|
||
} from "antd";
|
||
import {
|
||
FileSearchOutlined,
|
||
ReloadOutlined,
|
||
RobotOutlined,
|
||
} from "@ant-design/icons";
|
||
|
||
import { useStore } from "../store";
|
||
import type {
|
||
JVMActionDefinition,
|
||
JVMApplyResult,
|
||
JVMChangePreview,
|
||
JVMChangeRequest,
|
||
JVMAIPlanContext,
|
||
JVMValueSnapshot,
|
||
SavedConnection,
|
||
TabData,
|
||
} from "../types";
|
||
import { buildRpcConnectionConfig } from "../utils/connectionRpcConfig";
|
||
import {
|
||
buildJVMChangeDraftFromAIPlan,
|
||
buildJVMAIPlanPrompt,
|
||
matchesJVMAIPlanTargetTab,
|
||
type JVMAIChangeDraft,
|
||
type JVMAIChangePlan,
|
||
} from "../utils/jvmAiPlan";
|
||
import {
|
||
buildJVMActionPayloadTemplate,
|
||
buildJVMPreviewApplyRequest,
|
||
estimateJVMResourceEditorHeight,
|
||
formatJVMActionDisplayText,
|
||
formatJVMActionSummary,
|
||
formatJVMMetadataForDisplay,
|
||
formatJVMValueForDisplay,
|
||
JVM_DEFAULT_PAYLOAD_TEMPLATE,
|
||
resolveJVMActionDisplay,
|
||
resolveJVMValueEditorLanguage,
|
||
} from "../utils/jvmResourcePresentation";
|
||
import { buildJVMTabTitle } from "../utils/jvmRuntimePresentation";
|
||
import JVMModeBadge from "./jvm/JVMModeBadge";
|
||
import JVMChangePreviewModal from "./jvm/JVMChangePreviewModal";
|
||
import {
|
||
getJVMWorkspaceCardStyle,
|
||
JVMWorkspaceHero,
|
||
JVMWorkspaceShell,
|
||
} from "./jvm/JVMWorkspaceLayout";
|
||
|
||
const { Text } = Typography;
|
||
const DESCRIPTION_STYLES = { label: { width: 120 } } as const;
|
||
const { TextArea } = Input;
|
||
const DEFAULT_PAYLOAD_TEXT = JVM_DEFAULT_PAYLOAD_TEMPLATE;
|
||
|
||
type JVMResourceBrowserProps = {
|
||
tab: TabData;
|
||
};
|
||
|
||
const buildJVMRuntimeConfig = (
|
||
connection: SavedConnection,
|
||
providerMode: string,
|
||
) => {
|
||
const sourceJVM = connection.config.jvm || {};
|
||
return buildRpcConnectionConfig(connection.config, {
|
||
jvm: {
|
||
...sourceJVM,
|
||
preferredMode: providerMode,
|
||
allowedModes: [providerMode],
|
||
},
|
||
});
|
||
};
|
||
|
||
const buildJVMPreviewConfigRevision = (value: unknown): string => {
|
||
let text = "";
|
||
try {
|
||
text = JSON.stringify(value ?? null);
|
||
} catch {
|
||
return "unserializable";
|
||
}
|
||
|
||
let hash = 2166136261;
|
||
for (let index = 0; index < text.length; index += 1) {
|
||
hash ^= text.charCodeAt(index);
|
||
hash = Math.imul(hash, 16777619);
|
||
}
|
||
return (hash >>> 0).toString(16);
|
||
};
|
||
|
||
const buildJVMPreviewRuntimeFingerprint = (
|
||
connection: SavedConnection | undefined,
|
||
providerMode: string,
|
||
): string => {
|
||
const config = connection?.config;
|
||
const jvm = config?.jvm || {};
|
||
return JSON.stringify({
|
||
configRevision: buildJVMPreviewConfigRevision(config),
|
||
type: config?.type || "",
|
||
host: config?.host || "",
|
||
port: config?.port || 0,
|
||
user: config?.user || "",
|
||
providerMode,
|
||
environment: jvm.environment || "",
|
||
readOnly: jvm.readOnly !== false,
|
||
allowedModes: jvm.allowedModes || [],
|
||
preferredMode: jvm.preferredMode || "",
|
||
jmx: {
|
||
enabled: jvm.jmx?.enabled || false,
|
||
host: jvm.jmx?.host || "",
|
||
port: jvm.jmx?.port || 0,
|
||
username: jvm.jmx?.username || "",
|
||
domainAllowlist: jvm.jmx?.domainAllowlist || [],
|
||
},
|
||
endpoint: {
|
||
enabled: jvm.endpoint?.enabled || false,
|
||
baseUrl: jvm.endpoint?.baseUrl || "",
|
||
timeoutSeconds: jvm.endpoint?.timeoutSeconds || 0,
|
||
},
|
||
agent: {
|
||
enabled: jvm.agent?.enabled || false,
|
||
baseUrl: jvm.agent?.baseUrl || "",
|
||
timeoutSeconds: jvm.agent?.timeoutSeconds || 0,
|
||
},
|
||
});
|
||
};
|
||
|
||
const buildJVMPreviewContextKey = (
|
||
connectionId: string,
|
||
mode: string,
|
||
path: string,
|
||
runtimeFingerprint: string,
|
||
): string => `${connectionId}::${mode}::${path}::${runtimeFingerprint}`;
|
||
|
||
const snapshotBlockStyle = (background: string): React.CSSProperties => ({
|
||
margin: 0,
|
||
borderRadius: 8,
|
||
background,
|
||
overflow: "auto",
|
||
});
|
||
|
||
const formatDraftPayload = (draft: JVMAIChangeDraft): string => {
|
||
try {
|
||
return JSON.stringify(draft.payload ?? {}, null, 2);
|
||
} catch {
|
||
return "{}";
|
||
}
|
||
};
|
||
|
||
const resolveDefaultAction = (
|
||
actions: JVMActionDefinition[] | undefined,
|
||
providerMode: "jmx" | "endpoint" | "agent",
|
||
): string => {
|
||
if (actions && actions.length > 0) {
|
||
return String(actions[0].action || "").trim() || "put";
|
||
}
|
||
if (providerMode === "jmx") {
|
||
return "set";
|
||
}
|
||
return "put";
|
||
};
|
||
|
||
const normalizePreviewResult = (value: any): JVMChangePreview | null => {
|
||
if (
|
||
value &&
|
||
typeof value === "object" &&
|
||
typeof value.allowed === "boolean"
|
||
) {
|
||
return value as JVMChangePreview;
|
||
}
|
||
if (value?.data && typeof value.data.allowed === "boolean") {
|
||
return value.data as JVMChangePreview;
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const normalizeApplyResult = (value: any): JVMApplyResult | null => {
|
||
if (value && typeof value === "object" && typeof value.status === "string") {
|
||
return value as JVMApplyResult;
|
||
}
|
||
if (value?.data && typeof value.data.status === "string") {
|
||
return value.data as JVMApplyResult;
|
||
}
|
||
return null;
|
||
};
|
||
|
||
const JVMResourceBrowser: React.FC<JVMResourceBrowserProps> = ({ tab }) => {
|
||
const connection = useStore((state) =>
|
||
state.connections.find((item) => item.id === tab.connectionId),
|
||
);
|
||
const addTab = useStore((state) => state.addTab);
|
||
const theme = useStore((state) => state.theme);
|
||
const darkMode = theme === "dark";
|
||
const providerMode = (tab.providerMode ||
|
||
connection?.config.jvm?.preferredMode ||
|
||
"jmx") as "jmx" | "endpoint" | "agent";
|
||
const resourcePath = String(tab.resourcePath || "").trim();
|
||
const readOnly = connection?.config.jvm?.readOnly !== false;
|
||
const runtimeFingerprint = useMemo(
|
||
() => buildJVMPreviewRuntimeFingerprint(connection, providerMode),
|
||
[connection, providerMode],
|
||
);
|
||
const [loading, setLoading] = useState(true);
|
||
const [snapshot, setSnapshot] = useState<JVMValueSnapshot | null>(null);
|
||
const [error, setError] = useState("");
|
||
const [action, setAction] = useState("");
|
||
const [reason, setReason] = useState("");
|
||
const [payloadText, setPayloadText] = useState(DEFAULT_PAYLOAD_TEXT);
|
||
const [draftSource, setDraftSource] = useState<"manual" | "ai-plan">(
|
||
"manual",
|
||
);
|
||
const [draftResourceId, setDraftResourceId] = useState("");
|
||
const [draftError, setDraftError] = useState("");
|
||
const [applyMessage, setApplyMessage] = useState("");
|
||
const [previewLoading, setPreviewLoading] = useState(false);
|
||
const [previewOpen, setPreviewOpen] = useState(false);
|
||
const [previewResult, setPreviewResult] = useState<JVMChangePreview | null>(
|
||
null,
|
||
);
|
||
const [previewRequest, setPreviewRequest] = useState<JVMChangeRequest | null>(
|
||
null,
|
||
);
|
||
const [previewRuntimeConfig, setPreviewRuntimeConfig] = useState<any | null>(
|
||
null,
|
||
);
|
||
const [previewContextKey, setPreviewContextKey] = useState("");
|
||
const [applyLoading, setApplyLoading] = useState(false);
|
||
const previewSequenceRef = useRef(0);
|
||
const currentPreviewContextKey = buildJVMPreviewContextKey(
|
||
tab.connectionId,
|
||
providerMode,
|
||
resourcePath,
|
||
runtimeFingerprint,
|
||
);
|
||
const previewContextKeyRef = useRef(currentPreviewContextKey);
|
||
previewContextKeyRef.current = currentPreviewContextKey;
|
||
|
||
const clearPreviewState = () => {
|
||
setPreviewOpen(false);
|
||
setPreviewResult(null);
|
||
setPreviewRequest(null);
|
||
setPreviewRuntimeConfig(null);
|
||
setPreviewContextKey("");
|
||
};
|
||
|
||
const displayValue = useMemo(() => formatJVMValueForDisplay(snapshot), [snapshot]);
|
||
const displayLanguage = useMemo(
|
||
() =>
|
||
snapshot?.sensitive
|
||
? "plaintext"
|
||
: resolveJVMValueEditorLanguage(snapshot?.format || "", snapshot?.value),
|
||
[snapshot?.format, snapshot?.sensitive, snapshot?.value],
|
||
);
|
||
const metadataText = useMemo(
|
||
() => formatJVMMetadataForDisplay(snapshot),
|
||
[snapshot],
|
||
);
|
||
const metadataLanguage = useMemo(
|
||
() =>
|
||
snapshot?.sensitive
|
||
? "plaintext"
|
||
: resolveJVMValueEditorLanguage("json", snapshot?.metadata),
|
||
[snapshot?.metadata, snapshot?.sensitive],
|
||
);
|
||
const supportedActions = useMemo(() => {
|
||
if (!Array.isArray(snapshot?.supportedActions)) {
|
||
return [] as JVMActionDefinition[];
|
||
}
|
||
return snapshot.supportedActions.filter(
|
||
(item) => !!String(item?.action || "").trim(),
|
||
);
|
||
}, [snapshot]);
|
||
const selectedActionDefinition = useMemo(
|
||
() => supportedActions.find((item) => item.action === action) || null,
|
||
[action, supportedActions],
|
||
);
|
||
const selectedActionDisplay = useMemo(
|
||
() => resolveJVMActionDisplay(selectedActionDefinition || action),
|
||
[action, selectedActionDefinition],
|
||
);
|
||
|
||
const loadSnapshot = async () => {
|
||
const loadContextKey = currentPreviewContextKey;
|
||
if (!connection) {
|
||
setLoading(false);
|
||
setSnapshot(null);
|
||
setError("连接不存在或已被删除");
|
||
return;
|
||
}
|
||
|
||
if (!resourcePath) {
|
||
setLoading(false);
|
||
setSnapshot(null);
|
||
setError("资源路径为空");
|
||
return;
|
||
}
|
||
|
||
const backendApp = (window as any).go?.app?.App;
|
||
if (typeof backendApp?.JVMGetValue !== "function") {
|
||
setLoading(false);
|
||
setSnapshot(null);
|
||
setError("JVMGetValue 后端方法不可用");
|
||
return;
|
||
}
|
||
|
||
setLoading(true);
|
||
setError("");
|
||
try {
|
||
const result = await backendApp.JVMGetValue(
|
||
buildJVMRuntimeConfig(connection, providerMode),
|
||
resourcePath,
|
||
);
|
||
if (loadContextKey !== previewContextKeyRef.current) {
|
||
return;
|
||
}
|
||
if (!result?.success) {
|
||
setSnapshot(null);
|
||
setError(String(result?.message || "读取 JVM 资源失败"));
|
||
return;
|
||
}
|
||
setSnapshot((result.data || null) as JVMValueSnapshot | null);
|
||
} catch (err: any) {
|
||
setSnapshot(null);
|
||
setError(err?.message || "读取 JVM 资源失败");
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
void loadSnapshot();
|
||
}, [connection, providerMode, resourcePath, runtimeFingerprint, tab.connectionId]);
|
||
|
||
useEffect(() => {
|
||
setSnapshot(null);
|
||
setAction("");
|
||
setReason("");
|
||
setPayloadText(DEFAULT_PAYLOAD_TEXT);
|
||
setDraftSource("manual");
|
||
setDraftResourceId("");
|
||
setDraftError("");
|
||
setApplyMessage("");
|
||
previewSequenceRef.current += 1;
|
||
clearPreviewState();
|
||
}, [currentPreviewContextKey]);
|
||
|
||
useEffect(() => {
|
||
if (action.trim()) {
|
||
return;
|
||
}
|
||
const nextAction = resolveDefaultAction(supportedActions, providerMode);
|
||
setAction(nextAction);
|
||
const nextDefinition = supportedActions.find(
|
||
(item) => item.action === nextAction,
|
||
);
|
||
if (
|
||
String(payloadText || "").trim() === "" ||
|
||
payloadText === DEFAULT_PAYLOAD_TEXT
|
||
) {
|
||
setPayloadText(buildJVMActionPayloadTemplate(nextDefinition, snapshot?.sensitive));
|
||
}
|
||
}, [action, payloadText, providerMode, supportedActions]);
|
||
|
||
useEffect(() => {
|
||
const handler = (event: Event) => {
|
||
const detail = (event as CustomEvent).detail as
|
||
| {
|
||
plan?: JVMAIChangePlan;
|
||
targetTabId?: string;
|
||
connectionId?: string;
|
||
providerMode?: JVMAIPlanContext["providerMode"];
|
||
resourcePath?: string;
|
||
}
|
||
| undefined;
|
||
const plan = detail?.plan;
|
||
if (!plan || (detail?.targetTabId && detail.targetTabId !== tab.id)) {
|
||
return;
|
||
}
|
||
|
||
const planContext =
|
||
detail?.targetTabId &&
|
||
detail?.connectionId &&
|
||
detail?.providerMode &&
|
||
detail?.resourcePath
|
||
? {
|
||
tabId: detail.targetTabId,
|
||
connectionId: detail.connectionId,
|
||
providerMode: detail.providerMode,
|
||
resourcePath: detail.resourcePath,
|
||
}
|
||
: undefined;
|
||
|
||
if (!planContext) {
|
||
setDraftError(
|
||
"AI 计划缺少来源上下文,请在目标 JVM 资源页重新生成后再应用。",
|
||
);
|
||
setApplyMessage("");
|
||
clearPreviewState();
|
||
return;
|
||
}
|
||
|
||
if (!matchesJVMAIPlanTargetTab(tab, planContext)) {
|
||
setDraftError(
|
||
"当前 JVM 页签与 AI 计划的来源上下文不一致,已拒绝自动应用。",
|
||
);
|
||
setApplyMessage("");
|
||
clearPreviewState();
|
||
return;
|
||
}
|
||
|
||
let draftFromPlan: JVMAIChangeDraft;
|
||
try {
|
||
draftFromPlan = buildJVMChangeDraftFromAIPlan(plan);
|
||
} catch (err: any) {
|
||
setDraftError(err?.message || "AI 计划暂时无法转换为 JVM 预览草稿");
|
||
setApplyMessage("");
|
||
clearPreviewState();
|
||
return;
|
||
}
|
||
|
||
setDraftResourceId(draftFromPlan.resourceId);
|
||
setAction(draftFromPlan.action);
|
||
setReason(draftFromPlan.reason);
|
||
setPayloadText(formatDraftPayload(draftFromPlan));
|
||
setDraftSource(draftFromPlan.source || "ai-plan");
|
||
setDraftError("");
|
||
setApplyMessage(
|
||
`已从 AI 计划填充草稿,目标资源为 ${draftFromPlan.resourceId},请先执行“预览变更”再确认写入。`,
|
||
);
|
||
clearPreviewState();
|
||
};
|
||
|
||
window.addEventListener(
|
||
"gonavi:jvm-apply-ai-plan",
|
||
handler as EventListener,
|
||
);
|
||
return () =>
|
||
window.removeEventListener(
|
||
"gonavi:jvm-apply-ai-plan",
|
||
handler as EventListener,
|
||
);
|
||
}, [resourcePath, tab.id]);
|
||
|
||
const handleSelectAction = (
|
||
nextAction: string,
|
||
definition?: JVMActionDefinition | null,
|
||
) => {
|
||
const normalized = String(nextAction || "").trim();
|
||
setAction(normalized);
|
||
if (!normalized) {
|
||
return;
|
||
}
|
||
const currentPayload = String(payloadText || "").trim();
|
||
if (
|
||
!currentPayload ||
|
||
currentPayload === "{}" ||
|
||
payloadText === DEFAULT_PAYLOAD_TEXT
|
||
) {
|
||
setPayloadText(buildJVMActionPayloadTemplate(definition, snapshot?.sensitive));
|
||
}
|
||
};
|
||
|
||
const buildDraftPlan = (): JVMChangeRequest => {
|
||
const trimmedAction = String(action || "").trim() || "put";
|
||
const trimmedReason = String(reason || "").trim();
|
||
if (!trimmedReason) {
|
||
throw new Error("请填写变更原因");
|
||
}
|
||
|
||
const rawPayload = String(payloadText || "").trim();
|
||
let payload: Record<string, any> = {};
|
||
if (rawPayload) {
|
||
const parsed = JSON.parse(rawPayload);
|
||
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||
throw new Error("Payload 必须是 JSON 对象");
|
||
}
|
||
payload = parsed as Record<string, any>;
|
||
}
|
||
|
||
const resourceId = String(draftResourceId || resourcePath).trim();
|
||
if (!resourceId) {
|
||
throw new Error("资源 ID 为空,无法生成变更草稿");
|
||
}
|
||
|
||
return {
|
||
providerMode,
|
||
resourceId,
|
||
action: trimmedAction,
|
||
reason: trimmedReason,
|
||
source: draftSource,
|
||
expectedVersion: snapshot?.version || undefined,
|
||
payload,
|
||
};
|
||
};
|
||
|
||
const handleOpenAudit = () => {
|
||
if (!connection) {
|
||
return;
|
||
}
|
||
|
||
addTab({
|
||
id: `jvm-audit-${connection.id}-${providerMode}`,
|
||
title: buildJVMTabTitle(connection.name, "audit", providerMode),
|
||
type: "jvm-audit",
|
||
connectionId: connection.id,
|
||
providerMode,
|
||
});
|
||
};
|
||
|
||
const handleAskAIForPlan = () => {
|
||
if (!connection) {
|
||
setDraftError("连接不存在或已被删除");
|
||
return;
|
||
}
|
||
|
||
const prompt = buildJVMAIPlanPrompt({
|
||
connectionName: connection.name,
|
||
host: connection.config.host,
|
||
providerMode,
|
||
resourcePath,
|
||
readOnly,
|
||
environment: connection.config.jvm?.environment,
|
||
snapshot,
|
||
});
|
||
|
||
const store = useStore.getState();
|
||
const wasClosed = !store.aiPanelVisible;
|
||
if (wasClosed) {
|
||
store.setAIPanelVisible(true);
|
||
}
|
||
setTimeout(
|
||
() => {
|
||
window.dispatchEvent(
|
||
new CustomEvent("gonavi:ai:inject-prompt", { detail: { prompt } }),
|
||
);
|
||
},
|
||
wasClosed ? 350 : 0,
|
||
);
|
||
};
|
||
|
||
const handlePreview = async () => {
|
||
if (!connection) {
|
||
setDraftError("连接不存在或已被删除");
|
||
return;
|
||
}
|
||
|
||
const backendApp = (window as any).go?.app?.App;
|
||
if (typeof backendApp?.JVMPreviewChange !== "function") {
|
||
setDraftError("JVMPreviewChange 后端方法不可用");
|
||
return;
|
||
}
|
||
|
||
let draftPlan: JVMChangeRequest;
|
||
try {
|
||
draftPlan = buildDraftPlan();
|
||
} catch (err: any) {
|
||
setDraftError(err?.message || "变更草稿不合法");
|
||
return;
|
||
}
|
||
|
||
const previewSequence = ++previewSequenceRef.current;
|
||
const previewContextKey = currentPreviewContextKey;
|
||
const runtimeConfig = buildJVMRuntimeConfig(connection, providerMode);
|
||
|
||
setPreviewLoading(true);
|
||
setDraftError("");
|
||
setApplyMessage("");
|
||
try {
|
||
const result = await backendApp.JVMPreviewChange(
|
||
runtimeConfig,
|
||
draftPlan,
|
||
);
|
||
if (
|
||
previewSequence !== previewSequenceRef.current ||
|
||
previewContextKey !== previewContextKeyRef.current
|
||
) {
|
||
return;
|
||
}
|
||
|
||
if (result?.success === false) {
|
||
clearPreviewState();
|
||
setDraftError(String(result?.message || "预览 JVM 变更失败"));
|
||
return;
|
||
}
|
||
|
||
const preview = normalizePreviewResult(result);
|
||
if (!preview) {
|
||
clearPreviewState();
|
||
setDraftError("预览结果格式不正确");
|
||
return;
|
||
}
|
||
|
||
setPreviewResult(preview);
|
||
setPreviewRequest(draftPlan);
|
||
setPreviewRuntimeConfig(runtimeConfig);
|
||
setPreviewContextKey(previewContextKey);
|
||
setPreviewOpen(true);
|
||
} catch (err: any) {
|
||
clearPreviewState();
|
||
setDraftError(err?.message || "预览 JVM 变更失败");
|
||
} finally {
|
||
setPreviewLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleApply = async () => {
|
||
await Promise.resolve();
|
||
|
||
if (!connection) {
|
||
setDraftError("连接不存在或已被删除");
|
||
return;
|
||
}
|
||
|
||
const backendApp = (window as any).go?.app?.App;
|
||
if (typeof backendApp?.JVMApplyChange !== "function") {
|
||
setDraftError("JVMApplyChange 后端方法不可用");
|
||
return;
|
||
}
|
||
|
||
if (!previewResult || !previewRequest || !previewRuntimeConfig) {
|
||
setDraftError("请先预览变更,再确认执行");
|
||
return;
|
||
}
|
||
if (previewContextKey !== previewContextKeyRef.current) {
|
||
clearPreviewState();
|
||
setDraftError("资源上下文已变化,请重新预览后再执行");
|
||
return;
|
||
}
|
||
|
||
let applyRequest: JVMChangeRequest;
|
||
try {
|
||
applyRequest = buildJVMPreviewApplyRequest(previewRequest, previewResult);
|
||
} catch (err: any) {
|
||
setDraftError(err?.message || "确认令牌缺失,请重新预览后再执行");
|
||
return;
|
||
}
|
||
|
||
setApplyLoading(true);
|
||
setDraftError("");
|
||
setApplyMessage("");
|
||
try {
|
||
const result = await backendApp.JVMApplyChange(
|
||
previewRuntimeConfig,
|
||
applyRequest,
|
||
);
|
||
if (result?.success === false) {
|
||
setDraftError(String(result?.message || "执行 JVM 变更失败"));
|
||
return;
|
||
}
|
||
|
||
const applyResult = normalizeApplyResult(result);
|
||
if (applyResult?.updatedValue) {
|
||
setSnapshot(applyResult.updatedValue);
|
||
}
|
||
|
||
clearPreviewState();
|
||
setApplyMessage(
|
||
applyResult?.message || result?.message || "JVM 变更已执行",
|
||
);
|
||
await loadSnapshot();
|
||
} catch (err: any) {
|
||
setDraftError(err?.message || "执行 JVM 变更失败");
|
||
} finally {
|
||
setApplyLoading(false);
|
||
}
|
||
};
|
||
|
||
if (!connection) {
|
||
return (
|
||
<Empty description="连接不存在或已被删除" style={{ marginTop: 64 }} />
|
||
);
|
||
}
|
||
|
||
const cardStyle = getJVMWorkspaceCardStyle(darkMode);
|
||
|
||
return (
|
||
<>
|
||
<style>{`
|
||
.jvm-resource-browser-scroll-shell {
|
||
scrollbar-width: thin;
|
||
}
|
||
.jvm-resource-browser-scroll-shell::-webkit-scrollbar,
|
||
.jvm-resource-browser-code-block::-webkit-scrollbar {
|
||
width: 10px;
|
||
height: 10px;
|
||
}
|
||
.jvm-resource-browser-scroll-shell::-webkit-scrollbar-thumb,
|
||
.jvm-resource-browser-code-block::-webkit-scrollbar-thumb {
|
||
background: rgba(0, 0, 0, 0.22);
|
||
border-radius: 999px;
|
||
}
|
||
.jvm-resource-browser-scroll-shell::-webkit-scrollbar-track,
|
||
.jvm-resource-browser-code-block::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
@media (max-width: 1120px) {
|
||
.jvm-resource-workbench {
|
||
grid-template-columns: 1fr !important;
|
||
}
|
||
}
|
||
`}</style>
|
||
<JVMWorkspaceShell
|
||
darkMode={darkMode}
|
||
className="jvm-resource-browser-scroll-shell"
|
||
data-jvm-resource-browser-scroll-shell="true"
|
||
>
|
||
<JVMWorkspaceHero
|
||
darkMode={darkMode}
|
||
eyebrow="JVM Resource"
|
||
title="JVM 资源工作台"
|
||
description={
|
||
<>
|
||
<Text strong>{connection.name}</Text>
|
||
<Text type="secondary"> · {resourcePath || "-"}</Text>
|
||
</>
|
||
}
|
||
badges={
|
||
<>
|
||
<JVMModeBadge mode={providerMode} />
|
||
<Tag color={readOnly ? "blue" : "red"}>
|
||
{readOnly ? "只读连接" : "可写连接"}
|
||
</Tag>
|
||
</>
|
||
}
|
||
actions={
|
||
<>
|
||
<Button
|
||
size="small"
|
||
icon={<ReloadOutlined />}
|
||
onClick={() => void loadSnapshot()}
|
||
>
|
||
刷新
|
||
</Button>
|
||
<Button
|
||
size="small"
|
||
icon={<FileSearchOutlined />}
|
||
onClick={handleOpenAudit}
|
||
>
|
||
审计记录
|
||
</Button>
|
||
<Button
|
||
size="small"
|
||
icon={<RobotOutlined />}
|
||
onClick={handleAskAIForPlan}
|
||
>
|
||
AI 生成计划
|
||
</Button>
|
||
</>
|
||
}
|
||
/>
|
||
|
||
<div
|
||
className="jvm-resource-workbench"
|
||
data-jvm-resource-workbench="true"
|
||
style={{
|
||
display: "grid",
|
||
gridTemplateColumns: "minmax(0, 1fr) minmax(360px, 440px)",
|
||
gap: 18,
|
||
alignItems: "start",
|
||
}}
|
||
>
|
||
<Card
|
||
title="资源快照"
|
||
variant="borderless"
|
||
style={{
|
||
...cardStyle,
|
||
gridColumn: readOnly ? "1 / -1" : undefined,
|
||
}}
|
||
>
|
||
{loading ? (
|
||
<Skeleton active paragraph={{ rows: 6 }} />
|
||
) : (
|
||
<Space direction="vertical" size={16} style={{ width: "100%" }}>
|
||
{error ? <Alert type="error" showIcon message={error} /> : null}
|
||
{snapshot ? (
|
||
<>
|
||
<Descriptions
|
||
column={1}
|
||
size="small"
|
||
styles={DESCRIPTION_STYLES}
|
||
>
|
||
<Descriptions.Item label="资源 ID">
|
||
{snapshot.resourceId || "-"}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="资源类型">
|
||
{snapshot.kind || tab.resourceKind || "-"}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="格式">
|
||
{snapshot.format || "-"}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="版本">
|
||
{snapshot.version || "-"}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="可用动作">
|
||
{formatJVMActionSummary(supportedActions)}
|
||
</Descriptions.Item>
|
||
</Descriptions>
|
||
{snapshot.description ? (
|
||
<Text type="secondary">{snapshot.description}</Text>
|
||
) : null}
|
||
<div>
|
||
<Text
|
||
strong
|
||
style={{ display: "block", marginBottom: 8 }}
|
||
>
|
||
资源值
|
||
</Text>
|
||
<div
|
||
className="jvm-resource-browser-code-block"
|
||
style={{
|
||
...snapshotBlockStyle("rgba(0, 0, 0, 0.04)"),
|
||
height: estimateJVMResourceEditorHeight(displayValue),
|
||
}}
|
||
>
|
||
<Editor
|
||
height="100%"
|
||
language={displayLanguage}
|
||
theme={
|
||
darkMode ? "transparent-dark" : "transparent-light"
|
||
}
|
||
value={displayValue}
|
||
options={{
|
||
readOnly: true,
|
||
minimap: { enabled: false },
|
||
lineNumbers: "on",
|
||
wordWrap: "on",
|
||
scrollBeyondLastLine: false,
|
||
automaticLayout: true,
|
||
folding: true,
|
||
renderValidationDecorations: "off",
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
{metadataText ? (
|
||
<div>
|
||
<Text
|
||
strong
|
||
style={{ display: "block", marginBottom: 8 }}
|
||
>
|
||
元数据
|
||
</Text>
|
||
<div
|
||
className="jvm-resource-browser-code-block"
|
||
style={{
|
||
...snapshotBlockStyle("rgba(0, 0, 0, 0.03)"),
|
||
height:
|
||
estimateJVMResourceEditorHeight(metadataText),
|
||
}}
|
||
>
|
||
<Editor
|
||
height="100%"
|
||
language={metadataLanguage}
|
||
theme={
|
||
darkMode
|
||
? "transparent-dark"
|
||
: "transparent-light"
|
||
}
|
||
value={metadataText}
|
||
options={{
|
||
readOnly: true,
|
||
minimap: { enabled: false },
|
||
lineNumbers: "on",
|
||
wordWrap: "on",
|
||
scrollBeyondLastLine: false,
|
||
automaticLayout: true,
|
||
folding: true,
|
||
renderValidationDecorations: "off",
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
</>
|
||
) : error ? null : (
|
||
<Empty description="暂无资源数据" />
|
||
)}
|
||
</Space>
|
||
)}
|
||
</Card>
|
||
|
||
{!readOnly ? (
|
||
<Card title="变更草稿" variant="borderless" style={cardStyle}>
|
||
<Space direction="vertical" size={16} style={{ width: "100%" }}>
|
||
{draftError ? (
|
||
<Alert type="error" showIcon message={draftError} />
|
||
) : null}
|
||
{applyMessage ? (
|
||
<Alert type="success" showIcon message={applyMessage} />
|
||
) : null}
|
||
<Descriptions
|
||
column={1}
|
||
size="small"
|
||
styles={DESCRIPTION_STYLES}
|
||
>
|
||
<Descriptions.Item label="资源路径">
|
||
{resourcePath || "-"}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="目标资源">
|
||
{draftResourceId || resourcePath || "-"}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="资源版本">
|
||
{snapshot?.version || "-"}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="草稿来源">
|
||
{draftSource === "ai-plan" ? "AI 辅助草稿" : "手工编辑"}
|
||
</Descriptions.Item>
|
||
</Descriptions>
|
||
{supportedActions.length > 0 ? (
|
||
<Space
|
||
direction="vertical"
|
||
size={8}
|
||
style={{ width: "100%" }}
|
||
>
|
||
<Text strong>资源支持动作</Text>
|
||
<Space size={8} wrap>
|
||
{supportedActions.map((item) => (
|
||
<Button
|
||
key={item.action}
|
||
size="small"
|
||
type={action === item.action ? "primary" : "default"}
|
||
danger={item.dangerous}
|
||
onClick={() => handleSelectAction(item.action, item)}
|
||
>
|
||
{resolveJVMActionDisplay(item).label}
|
||
</Button>
|
||
))}
|
||
</Space>
|
||
{selectedActionDisplay.description ? (
|
||
<Text type="secondary">
|
||
{selectedActionDisplay.description}
|
||
</Text>
|
||
) : null}
|
||
{selectedActionDefinition?.payloadFields?.length ? (
|
||
<Text type="secondary">
|
||
Payload 字段:
|
||
{selectedActionDefinition.payloadFields
|
||
.map(
|
||
(field) =>
|
||
`${field.name}${field.required ? "(必填)" : ""}`,
|
||
)
|
||
.join("、")}
|
||
</Text>
|
||
) : null}
|
||
</Space>
|
||
) : null}
|
||
<Space direction="vertical" size={8} style={{ width: "100%" }}>
|
||
<Text strong>动作</Text>
|
||
<Input
|
||
value={action}
|
||
onChange={(event) =>
|
||
handleSelectAction(
|
||
event.target.value,
|
||
selectedActionDefinition,
|
||
)
|
||
}
|
||
placeholder={
|
||
providerMode === "jmx"
|
||
? "例如 set 或 invoke"
|
||
: "例如 put / clear / evict"
|
||
}
|
||
maxLength={64}
|
||
/>
|
||
{action ? (
|
||
<Text type="secondary">
|
||
当前动作:
|
||
{formatJVMActionDisplayText(selectedActionDisplay)}
|
||
</Text>
|
||
) : null}
|
||
</Space>
|
||
<Space direction="vertical" size={8} style={{ width: "100%" }}>
|
||
<Text strong>变更原因</Text>
|
||
<Input
|
||
value={reason}
|
||
onChange={(event) => setReason(event.target.value)}
|
||
placeholder="填写本次 JVM 资源变更原因"
|
||
maxLength={200}
|
||
/>
|
||
</Space>
|
||
<Space direction="vertical" size={8} style={{ width: "100%" }}>
|
||
<Text strong>Payload(JSON)</Text>
|
||
<Text type="secondary">
|
||
预览会使用当前草稿;确认执行会使用最近一次成功预览的
|
||
request,修改草稿后请重新预览。
|
||
{selectedActionDefinition?.payloadExample && !snapshot?.sensitive
|
||
? " 已按当前动作填充推荐模板。"
|
||
: ""}
|
||
</Text>
|
||
<TextArea
|
||
value={payloadText}
|
||
onChange={(event) => setPayloadText(event.target.value)}
|
||
autoSize={{ minRows: 8, maxRows: 18 }}
|
||
spellCheck={false}
|
||
/>
|
||
</Space>
|
||
<Space size={12} wrap>
|
||
<Button
|
||
type="primary"
|
||
loading={previewLoading}
|
||
onClick={() => void handlePreview()}
|
||
>
|
||
预览变更
|
||
</Button>
|
||
<Button icon={<RobotOutlined />} onClick={handleAskAIForPlan}>
|
||
让 AI 生成计划
|
||
</Button>
|
||
</Space>
|
||
</Space>
|
||
</Card>
|
||
) : null}
|
||
</div>
|
||
</JVMWorkspaceShell>
|
||
|
||
<JVMChangePreviewModal
|
||
open={previewOpen}
|
||
preview={previewResult}
|
||
applying={applyLoading}
|
||
onCancel={() => {
|
||
if (applyLoading) {
|
||
return;
|
||
}
|
||
setPreviewOpen(false);
|
||
}}
|
||
onConfirm={() => void handleApply()}
|
||
/>
|
||
</>
|
||
);
|
||
};
|
||
|
||
export default JVMResourceBrowser;
|