mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-10 17:43:15 +08:00
✨ feat(jvm): 打通 JVM 只读资源浏览链路
- 后端新增 JVMListResources 与 JVMGetValue 接口并补齐回归测试 - Sidebar 基于能力探测展示 JVM 模式节点并懒加载资源节点 - TabManager 接入 JVMOverview、JVMResourceBrowser 与模式徽标展示 - 补齐 JVM Tab 元数据与连接持久化 sanitize 逻辑 - 更新需求追踪文档并记录 Task 4 验证结果与残余风险
This commit is contained in:
@@ -26,7 +26,7 @@
|
||||
- [x] 阶段 2(影响分析):完成
|
||||
- [x] 阶段 3(方案设计):完成(已形成正式设计文档)
|
||||
- [x] 阶段 4(实施计划):完成(已形成正式实施计划)
|
||||
- [ ] 阶段 5(实现与自检):进行中(Task 1、Task 2、Task 3 已完成并通过回归)
|
||||
- [ ] 阶段 5(实现与自检):进行中(Task 1、Task 2、Task 3、Task 4 已完成并通过回归)
|
||||
- [ ] 阶段 6(评审与交付):
|
||||
- [ ] 阶段 7(发布与观察):
|
||||
|
||||
@@ -43,10 +43,11 @@
|
||||
- 已完成 Task 1:JVM 共享契约与配置归一化
|
||||
- 已完成 Task 2:Provider 注册、连接测试与能力探测 API
|
||||
- 已完成 Task 3:JVM 连接表单、图标与展示文案接入
|
||||
- 已完成 Task 4:只读资源浏览与 JVM Tab
|
||||
- 进行中:
|
||||
- Task 4:只读资源浏览与 JVM Tab
|
||||
- Task 5:Guard/Audit、AI 结构化计划等后续任务准备
|
||||
- 待处理:
|
||||
- Task 5+:Guard/Audit、AI 结构化计划等后续任务
|
||||
- Task 5+:Guard/Audit、AI 结构化计划等后续任务实现
|
||||
|
||||
## 5. 风险与阻塞
|
||||
- 风险:
|
||||
@@ -55,6 +56,7 @@
|
||||
- 若依赖 attach agent 或表达式执行,需严格控制安全边界与可观测性
|
||||
- 若目标 JVM 不允许预埋或动态注入 Agent,则“通用型”能力边界会明显收缩
|
||||
- 多接入模式会带来能力不一致问题,UI 与权限模型必须显式展示“当前模式支持什么/不支持什么”
|
||||
- 当前 JMX / Endpoint provider 的资源浏览与值读取仍是骨架实现,Task 4 已打通接口与 UI 链路,但真实资源展开会返回 `not implemented`
|
||||
- 阻塞:
|
||||
- 目标应用技术栈、缓存框架与接入约束尚未明确
|
||||
- 缓解措施:
|
||||
@@ -86,6 +88,8 @@
|
||||
- Task 2 已完成规格审查与代码质量审查,结论均通过
|
||||
- 已完成 JVM 连接类型卡片、最小表单字段、连接测试分发与展示文案接入
|
||||
- Task 3 已完成规格审查与代码质量审查;过程中修复了 JVM 标题文案偏差、模式选项暴露范围、编辑态模式静默降级和 endpoint timeout 失真问题
|
||||
- 已完成 JVM 只读资源浏览链路:后端新增 `JVMListResources` / `JVMGetValue`,前端新增 `jvm-overview` / `jvm-resource` tab 与侧边栏 JVM 模式/资源节点
|
||||
- Task 4 已完成规格复审;代码质量复审确认真实 provider 浏览能力仍为后续任务范围,另外已修正 JVM 资源 tab 同名问题
|
||||
- 证据(日志/截图/链接):
|
||||
- `cmd/optional-driver-agent/main.go`
|
||||
- `internal/db/database.go`
|
||||
@@ -127,7 +131,20 @@
|
||||
- `cd frontend && npm test -- src/utils/jvmRuntimePresentation.test.ts`
|
||||
- `cd frontend && npm test -- src/utils/jvmConnectionConfig.test.ts`
|
||||
- `cd frontend && npm run build`
|
||||
- `internal/app/methods_jvm.go`
|
||||
- `internal/app/methods_jvm_test.go`
|
||||
- `frontend/src/components/Sidebar.tsx`
|
||||
- `frontend/src/components/TabManager.tsx`
|
||||
- `frontend/src/components/JVMOverview.tsx`
|
||||
- `frontend/src/components/JVMResourceBrowser.tsx`
|
||||
- `frontend/src/components/jvm/JVMModeBadge.tsx`
|
||||
- `frontend/src/store.ts`
|
||||
- `frontend/src/types.ts`
|
||||
- `go test ./internal/app -run 'TestJVM(ListResources|GetValue)' -count=1`
|
||||
- `go test ./internal/app -run 'TestJVMProbeCapabilities|TestTestJVMConnection' -count=1`
|
||||
- `cd frontend && npm test -- src/utils/jvmRuntimePresentation.test.ts`
|
||||
- `cd frontend && npm run build`
|
||||
|
||||
## 8. 下一步
|
||||
- 下一步行动:进入 Task 4,打通 JVM 只读资源浏览与 Tab 路由,建立首个可打开的 JVM 运行时视图
|
||||
- 下一步行动:进入 Task 5,补齐 Guard/Audit 和 AI 结构化修改计划能力,并收敛真实 provider 的资源浏览实现边界
|
||||
- 负责人:Codex
|
||||
|
||||
67
frontend/src/components/JVMOverview.tsx
Normal file
67
frontend/src/components/JVMOverview.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Card, Descriptions, Empty, Space, Tag, Typography } from 'antd';
|
||||
|
||||
import { useStore } from '../store';
|
||||
import type { SavedConnection, TabData } from '../types';
|
||||
import JVMModeBadge from './jvm/JVMModeBadge';
|
||||
|
||||
const { Paragraph, Text } = Typography;
|
||||
|
||||
type JVMOverviewProps = {
|
||||
tab: TabData;
|
||||
};
|
||||
|
||||
const JVMOverview: React.FC<JVMOverviewProps> = ({ tab }) => {
|
||||
const connection = useStore((state) => state.connections.find((item) => item.id === tab.connectionId));
|
||||
const providerMode = tab.providerMode || connection?.config.jvm?.preferredMode || 'jmx';
|
||||
const readOnly = connection?.config.jvm?.readOnly !== false;
|
||||
const allowedModes = connection?.config.jvm?.allowedModes || [];
|
||||
|
||||
const endpointSummary = useMemo(() => {
|
||||
if (!connection?.config.jvm?.endpoint) {
|
||||
return '';
|
||||
}
|
||||
const endpoint = connection.config.jvm.endpoint;
|
||||
if (!endpoint.enabled && !endpoint.baseUrl) {
|
||||
return '';
|
||||
}
|
||||
return endpoint.baseUrl || '已启用';
|
||||
}, [connection]);
|
||||
|
||||
if (!connection) {
|
||||
return <Empty description="连接不存在或已被删除" style={{ marginTop: 64 }} />;
|
||||
}
|
||||
|
||||
const jmxHost = connection.config.jvm?.jmx?.host || connection.config.host;
|
||||
const jmxPort = connection.config.jvm?.jmx?.port || connection.config.port;
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20, display: 'grid', gap: 16 }}>
|
||||
<Card>
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Space size={12} wrap>
|
||||
<JVMModeBadge mode={providerMode} />
|
||||
<Tag color={readOnly ? 'blue' : 'red'}>{readOnly ? '只读连接' : '可写连接'}</Tag>
|
||||
<Tag>{connection.config.jvm?.environment || 'dev'}</Tag>
|
||||
</Space>
|
||||
<Paragraph style={{ marginBottom: 0 }}>
|
||||
<Text strong>{connection.name}</Text>
|
||||
<Text type="secondary"> · {connection.config.host}:{connection.config.port}</Text>
|
||||
</Paragraph>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card title="连接摘要">
|
||||
<Descriptions column={1} size="small" labelStyle={{ width: 120 }}>
|
||||
<Descriptions.Item label="当前模式">{providerMode}</Descriptions.Item>
|
||||
<Descriptions.Item label="允许模式">{allowedModes.length > 0 ? allowedModes.join(', ') : 'jmx'}</Descriptions.Item>
|
||||
<Descriptions.Item label="JMX 地址">{`${jmxHost}:${jmxPort}`}</Descriptions.Item>
|
||||
<Descriptions.Item label="Endpoint">{endpointSummary || '未配置'}</Descriptions.Item>
|
||||
<Descriptions.Item label="资源浏览">{'通过侧边栏展开模式节点后懒加载'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JVMOverview;
|
||||
168
frontend/src/components/JVMResourceBrowser.tsx
Normal file
168
frontend/src/components/JVMResourceBrowser.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Alert, Button, Card, Descriptions, Empty, Skeleton, Space, Typography } from 'antd';
|
||||
import { ReloadOutlined } from '@ant-design/icons';
|
||||
|
||||
import { useStore } from '../store';
|
||||
import type { JVMValueSnapshot, SavedConnection, TabData } from '../types';
|
||||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import JVMModeBadge from './jvm/JVMModeBadge';
|
||||
|
||||
const { Paragraph, Text } = Typography;
|
||||
|
||||
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 formatValue = (value: unknown): string => {
|
||||
if (typeof value === 'string') {
|
||||
return value;
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return String(value);
|
||||
}
|
||||
};
|
||||
|
||||
const JVMResourceBrowser: React.FC<JVMResourceBrowserProps> = ({ tab }) => {
|
||||
const connection = useStore((state) => state.connections.find((item) => item.id === tab.connectionId));
|
||||
const providerMode = tab.providerMode || connection?.config.jvm?.preferredMode || 'jmx';
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [snapshot, setSnapshot] = useState<JVMValueSnapshot | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const displayValue = useMemo(() => formatValue(snapshot?.value), [snapshot]);
|
||||
|
||||
const loadSnapshot = async () => {
|
||||
if (!connection) {
|
||||
setLoading(false);
|
||||
setSnapshot(null);
|
||||
setError('连接不存在或已被删除');
|
||||
return;
|
||||
}
|
||||
|
||||
const resourcePath = String(tab.resourcePath || '').trim();
|
||||
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 (!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, tab.connectionId, tab.resourcePath]);
|
||||
|
||||
if (!connection) {
|
||||
return <Empty description="连接不存在或已被删除" style={{ marginTop: 64 }} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: 20, display: 'grid', gap: 16 }}>
|
||||
<Card>
|
||||
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|
||||
<Space size={12} wrap>
|
||||
<JVMModeBadge mode={providerMode} />
|
||||
<Button size="small" icon={<ReloadOutlined />} onClick={() => void loadSnapshot()}>
|
||||
刷新
|
||||
</Button>
|
||||
</Space>
|
||||
<Paragraph style={{ marginBottom: 0 }}>
|
||||
<Text strong>{connection.name}</Text>
|
||||
</Paragraph>
|
||||
<Text type="secondary">{tab.resourcePath}</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card title="资源快照">
|
||||
{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" labelStyle={{ width: 120 }}>
|
||||
<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>
|
||||
<pre
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
background: 'rgba(0, 0, 0, 0.04)',
|
||||
overflow: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{displayValue}
|
||||
</pre>
|
||||
{snapshot.metadata && Object.keys(snapshot.metadata).length > 0 ? (
|
||||
<pre
|
||||
style={{
|
||||
margin: 0,
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
background: 'rgba(0, 0, 0, 0.03)',
|
||||
overflow: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{JSON.stringify(snapshot.metadata, null, 2)}
|
||||
</pre>
|
||||
) : null}
|
||||
</>
|
||||
) : error ? null : <Empty description="暂无资源数据" />}
|
||||
</Space>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default JVMResourceBrowser;
|
||||
@@ -36,9 +36,9 @@ import { Tree, message, Dropdown, MenuProps, Input, Button, Modal, Form, Badge,
|
||||
} from '@ant-design/icons';
|
||||
import { useStore } from '../store';
|
||||
import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
|
||||
import { SavedConnection, ExternalSQLTreeEntry } from '../types';
|
||||
import { SavedConnection, ExternalSQLTreeEntry, JVMCapability, JVMResourceSummary } from '../types';
|
||||
import { getDbIcon } from './DatabaseIcons';
|
||||
import { DBGetDatabases, DBGetTables, DBQuery, DBShowCreateTable, ExportTable, OpenSQLFile, ExecuteSQLFile, CancelSQLFileExecution, CreateDatabase, RenameDatabase, DropDatabase, RenameTable, DropTable, DropView, DropFunction, RenameView, SelectSQLDirectory, ListSQLDirectory, ReadSQLFile } from '../../wailsjs/go/app/App';
|
||||
import { DBGetDatabases, DBGetTables, DBQuery, DBShowCreateTable, ExportTable, OpenSQLFile, ExecuteSQLFile, CancelSQLFileExecution, CreateDatabase, RenameDatabase, DropDatabase, RenameTable, DropTable, DropView, DropFunction, RenameView, SelectSQLDirectory, ListSQLDirectory, ReadSQLFile, JVMProbeCapabilities } from '../../wailsjs/go/app/App';
|
||||
import { getTableDataDangerActionMeta, supportsTableTruncateAction, type TableDataDangerActionKind } from './tableDataDangerActions';
|
||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
@@ -48,8 +48,10 @@ import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||||
import { noAutoCapInputProps } from '../utils/inputAutoCap';
|
||||
import { normalizeSidebarViewName, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata';
|
||||
import { resolveConnectionHostTokens } from '../utils/tabDisplay';
|
||||
import { buildJVMTabTitle } from '../utils/jvmRuntimePresentation';
|
||||
import { buildTableSelectQuery } from '../utils/objectQueryTemplates';
|
||||
import { buildExternalSQLDirectoryId, buildExternalSQLRootNode, buildExternalSQLTabId, type ExternalSQLTreeNode } from '../utils/externalSqlTree';
|
||||
import JVMModeBadge from './jvm/JVMModeBadge';
|
||||
|
||||
const { Search } = Input;
|
||||
|
||||
@@ -60,7 +62,7 @@ interface TreeNode {
|
||||
children?: TreeNode[];
|
||||
icon?: React.ReactNode;
|
||||
dataRef?: any;
|
||||
type?: 'connection' | 'database' | 'table' | 'view' | 'db-trigger' | 'routine' | 'object-group' | 'queries-folder' | 'saved-query' | 'external-sql-root' | 'external-sql-directory' | 'external-sql-folder' | 'external-sql-file' | 'folder-columns' | 'folder-indexes' | 'folder-fks' | 'folder-triggers' | 'redis-db' | 'tag';
|
||||
type?: 'connection' | 'database' | 'table' | 'view' | 'db-trigger' | 'routine' | 'object-group' | 'queries-folder' | 'saved-query' | 'external-sql-root' | 'external-sql-directory' | 'external-sql-folder' | 'external-sql-file' | 'folder-columns' | 'folder-indexes' | 'folder-fks' | 'folder-triggers' | 'redis-db' | 'tag' | 'jvm-mode' | 'jvm-resource';
|
||||
}
|
||||
|
||||
type BatchTableExportMode = 'schema' | 'backup' | 'dataOnly';
|
||||
@@ -968,6 +970,43 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
ssh: conn.config.ssh || { host: "", port: 22, user: "", password: "", keyPath: "" }
|
||||
};
|
||||
|
||||
if (conn.config.type === 'jvm') {
|
||||
try {
|
||||
const res = await JVMProbeCapabilities(buildRuntimeConfig(conn) as any);
|
||||
if (res.success) {
|
||||
setConnectionStates(prev => ({ ...prev, [conn.id]: 'success' }));
|
||||
const capabilities: JVMCapability[] = Array.isArray(res.data) ? res.data as JVMCapability[] : [];
|
||||
const modeNodes: TreeNode[] = capabilities.map((capability) => ({
|
||||
title: capability.displayLabel || capability.mode,
|
||||
key: `${conn.id}-jvm-mode-${capability.mode}`,
|
||||
icon: <HddOutlined />,
|
||||
type: 'jvm-mode',
|
||||
dataRef: {
|
||||
...conn,
|
||||
providerMode: capability.mode,
|
||||
canBrowse: capability.canBrowse,
|
||||
canWrite: capability.canWrite,
|
||||
reason: capability.reason,
|
||||
displayLabel: capability.displayLabel,
|
||||
},
|
||||
isLeaf: capability.canBrowse !== true,
|
||||
}));
|
||||
setTreeData(origin => updateTreeData(origin, node.key, modeNodes));
|
||||
} else {
|
||||
setConnectionStates(prev => ({ ...prev, [conn.id]: 'error' }));
|
||||
setLoadedKeys(prev => prev.filter(k => k !== node.key));
|
||||
message.error({ content: res.message, key: `conn-${conn.id}-jvm-caps` });
|
||||
}
|
||||
} catch (e: any) {
|
||||
setConnectionStates(prev => ({ ...prev, [conn.id]: 'error' }));
|
||||
setLoadedKeys(prev => prev.filter(k => k !== node.key));
|
||||
message.error({ content: '连接失败: ' + (e?.message || String(e)), key: `conn-${conn.id}-jvm-caps` });
|
||||
} finally {
|
||||
loadingNodesRef.current.delete(loadKey);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle Redis connections differently
|
||||
if (conn.config.type === 'redis') {
|
||||
try {
|
||||
@@ -1042,6 +1081,53 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
}
|
||||
};
|
||||
|
||||
const loadJVMResources = async (node: any) => {
|
||||
const conn = node.dataRef as SavedConnection & { providerMode?: string; resourcePath?: string };
|
||||
const providerMode = String(conn.providerMode || '').trim().toLowerCase();
|
||||
const parentPath = String(conn.resourcePath || '').trim();
|
||||
const loadKey = `jvm-resources-${conn.id}-${providerMode}-${parentPath}`;
|
||||
if (loadingNodesRef.current.has(loadKey)) return;
|
||||
loadingNodesRef.current.add(loadKey);
|
||||
|
||||
try {
|
||||
const backendApp = (window as any).go?.app?.App;
|
||||
if (typeof backendApp?.JVMListResources !== 'function') {
|
||||
throw new Error('JVMListResources 后端方法不可用');
|
||||
}
|
||||
|
||||
const res = await backendApp.JVMListResources(buildJVMRuntimeConfig(conn, providerMode), parentPath);
|
||||
if (res.success) {
|
||||
const resourceRows: JVMResourceSummary[] = Array.isArray(res.data) ? res.data as JVMResourceSummary[] : [];
|
||||
const resourceNodes: TreeNode[] = resourceRows.map((item) => ({
|
||||
title: item.name || item.path || item.id,
|
||||
key: `${conn.id}-jvm-resource-${providerMode}-${item.path}`,
|
||||
icon: item.hasChildren ? <FolderOpenOutlined /> : <HddOutlined />,
|
||||
type: 'jvm-resource',
|
||||
dataRef: {
|
||||
...conn,
|
||||
providerMode: item.providerMode || providerMode,
|
||||
resourcePath: item.path,
|
||||
resourceKind: item.kind,
|
||||
canRead: item.canRead,
|
||||
canWrite: item.canWrite,
|
||||
hasChildren: item.hasChildren,
|
||||
sensitive: item.sensitive,
|
||||
},
|
||||
isLeaf: item.hasChildren !== true,
|
||||
}));
|
||||
setTreeData(origin => updateTreeData(origin, node.key, resourceNodes));
|
||||
} else {
|
||||
setLoadedKeys(prev => prev.filter(k => k !== node.key));
|
||||
message.error({ content: res.message, key: `jvm-resource-${node.key}` });
|
||||
}
|
||||
} catch (e: any) {
|
||||
setLoadedKeys(prev => prev.filter(k => k !== node.key));
|
||||
message.error({ content: '加载 JVM 资源失败: ' + (e?.message || String(e)), key: `jvm-resource-${node.key}` });
|
||||
} finally {
|
||||
loadingNodesRef.current.delete(loadKey);
|
||||
}
|
||||
};
|
||||
|
||||
const loadTables = async (node: any) => {
|
||||
const conn = node.dataRef; // has dbName
|
||||
const dbName = conn.dbName;
|
||||
@@ -1369,6 +1455,8 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
|
||||
if (type === 'connection') {
|
||||
await loadDatabases({ key, dataRef });
|
||||
} else if (type === 'jvm-mode' || type === 'jvm-resource') {
|
||||
await loadJVMResources({ key, dataRef });
|
||||
} else if (type === 'database') {
|
||||
await loadTables({ key, dataRef });
|
||||
} else if (type === 'table') {
|
||||
@@ -1461,6 +1549,8 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
setActiveContext({ connectionId: dataRef.id, dbName: dataRef.dbName });
|
||||
} else if (type === 'table') {
|
||||
setActiveContext({ connectionId: dataRef.id, dbName: dataRef.dbName });
|
||||
} else if (type === 'jvm-mode' || type === 'jvm-resource') {
|
||||
setActiveContext({ connectionId: dataRef.id, dbName: '' });
|
||||
} else if (type === 'view' || type === 'db-trigger' || type === 'routine') {
|
||||
setActiveContext({ connectionId: dataRef.id, dbName: dataRef.dbName });
|
||||
} else if (type === 'saved-query') {
|
||||
@@ -1507,6 +1597,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const { type, dataRef, key: nodeKey } = node;
|
||||
if (type === 'connection') setActiveContext({ connectionId: nodeKey, dbName: '' });
|
||||
else if (type === 'database') setActiveContext({ connectionId: dataRef.id, dbName: dataRef.dbName });
|
||||
else if (type === 'jvm-mode' || type === 'jvm-resource') setActiveContext({ connectionId: dataRef.id, dbName: '' });
|
||||
else if (type === 'table' || type === 'view' || type === 'db-trigger' || type === 'routine') setActiveContext({ connectionId: dataRef.id, dbName: dataRef.dbName });
|
||||
else if (type === 'saved-query') setActiveContext({ connectionId: dataRef.connectionId, dbName: dataRef.dbName });
|
||||
else if (type === 'external-sql-root' || type === 'external-sql-directory' || type === 'external-sql-folder' || type === 'external-sql-file') setActiveContext({ connectionId: dataRef.connectionId, dbName: dataRef.dbName });
|
||||
@@ -1585,6 +1676,16 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
routineType
|
||||
});
|
||||
return;
|
||||
} else if (node.type === 'jvm-mode') {
|
||||
const { providerMode, id } = node.dataRef;
|
||||
const conn = (connections.find((item) => item.id === id) || node.dataRef) as SavedConnection;
|
||||
openJVMOverviewTab(conn, providerMode);
|
||||
return;
|
||||
} else if (node.type === 'jvm-resource') {
|
||||
const { providerMode, resourcePath, resourceKind, id } = node.dataRef;
|
||||
const conn = (connections.find((item) => item.id === id) || node.dataRef) as SavedConnection;
|
||||
openJVMResourceTab(conn, providerMode, resourcePath, resourceKind);
|
||||
return;
|
||||
}
|
||||
|
||||
const key = node.key;
|
||||
@@ -2380,6 +2481,43 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
});
|
||||
};
|
||||
|
||||
const buildJVMRuntimeConfig = (conn: SavedConnection & { dbName?: string }, providerMode: string) => {
|
||||
const sourceJVM = conn.config.jvm || {};
|
||||
return buildRpcConnectionConfig(conn.config, {
|
||||
database: '',
|
||||
jvm: {
|
||||
...sourceJVM,
|
||||
preferredMode: providerMode as 'jmx' | 'endpoint' | 'agent',
|
||||
allowedModes: [providerMode as 'jmx' | 'endpoint' | 'agent'],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const openJVMOverviewTab = (conn: SavedConnection, providerMode: string) => {
|
||||
addTab({
|
||||
id: `jvm-overview-${conn.id}-${providerMode}`,
|
||||
title: buildJVMTabTitle(conn.name, 'overview', providerMode),
|
||||
type: 'jvm-overview',
|
||||
connectionId: conn.id,
|
||||
providerMode: providerMode as 'jmx' | 'endpoint' | 'agent',
|
||||
});
|
||||
};
|
||||
|
||||
const openJVMResourceTab = (conn: SavedConnection, providerMode: string, resourcePath: string, resourceKind?: string) => {
|
||||
const trimmedResourcePath = String(resourcePath || '').trim();
|
||||
addTab({
|
||||
id: `jvm-resource-${conn.id}-${providerMode}-${encodeURIComponent(trimmedResourcePath)}`,
|
||||
title: trimmedResourcePath
|
||||
? `${buildJVMTabTitle(conn.name, 'resource', providerMode)} · ${trimmedResourcePath}`
|
||||
: buildJVMTabTitle(conn.name, 'resource', providerMode),
|
||||
type: 'jvm-resource',
|
||||
connectionId: conn.id,
|
||||
providerMode: providerMode as 'jmx' | 'endpoint' | 'agent',
|
||||
resourcePath: trimmedResourcePath,
|
||||
resourceKind,
|
||||
});
|
||||
};
|
||||
|
||||
const getConnectionNodeRef = (connRef: any) => {
|
||||
const latestConn = connections.find(c => c.id === connRef.id);
|
||||
return { key: connRef.id, dataRef: latestConn || connRef };
|
||||
@@ -3969,6 +4107,21 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
hoverTitle = String(node?.dataRef?.path || displayTitle);
|
||||
}
|
||||
|
||||
if (node.type === 'jvm-mode') {
|
||||
return (
|
||||
<span
|
||||
title={hoverTitle}
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: 8, minWidth: 0 }}
|
||||
>
|
||||
<JVMModeBadge
|
||||
mode={String(node?.dataRef?.providerMode || displayTitle)}
|
||||
label={displayTitle}
|
||||
reason={String(node?.dataRef?.reason || '').trim() || undefined}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === 'external-sql-root') {
|
||||
return (
|
||||
<span
|
||||
|
||||
@@ -16,6 +16,8 @@ import RedisMonitor from './RedisMonitor';
|
||||
import TriggerViewer from './TriggerViewer';
|
||||
import DefinitionViewer from './DefinitionViewer';
|
||||
import TableOverview from './TableOverview';
|
||||
import JVMOverview from './JVMOverview';
|
||||
import JVMResourceBrowser from './JVMResourceBrowser';
|
||||
import type { TabData } from '../types';
|
||||
import { buildTabDisplayTitle } from '../utils/tabDisplay';
|
||||
|
||||
@@ -203,6 +205,16 @@ const TabManager: React.FC = () => {
|
||||
content = <DefinitionViewer tab={tab} />;
|
||||
} else if (tab.type === 'table-overview') {
|
||||
content = <TableOverview tab={tab} />;
|
||||
} else if (tab.type === 'jvm-overview') {
|
||||
content = <JVMOverview tab={tab} />;
|
||||
} else if (tab.type === 'jvm-resource') {
|
||||
content = <JVMResourceBrowser tab={tab} />;
|
||||
} else if (tab.type === 'jvm-audit') {
|
||||
content = (
|
||||
<div style={{ padding: 24 }}>
|
||||
当前任务未实现 JVM 审计页。
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const menuItems: MenuProps['items'] = [
|
||||
|
||||
67
frontend/src/components/jvm/JVMModeBadge.tsx
Normal file
67
frontend/src/components/jvm/JVMModeBadge.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'antd';
|
||||
|
||||
import { resolveJVMModeMeta } from '../../utils/jvmRuntimePresentation';
|
||||
|
||||
type JVMModeBadgeProps = {
|
||||
mode: string;
|
||||
label?: string;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
const JVMModeBadge: React.FC<JVMModeBadgeProps> = ({
|
||||
mode,
|
||||
label,
|
||||
reason,
|
||||
}) => {
|
||||
const meta = resolveJVMModeMeta(mode);
|
||||
const displayLabel = String(label || meta.label || 'Unknown').trim() || 'Unknown';
|
||||
const content = (
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
height: 20,
|
||||
padding: '0 8px',
|
||||
borderRadius: 999,
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
color: meta.color,
|
||||
background: meta.backgroundColor,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{displayLabel}
|
||||
</span>
|
||||
{reason ? (
|
||||
<span
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: '#cf1322',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{reason}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
);
|
||||
|
||||
if (!reason) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return <Tooltip title={reason}>{content}</Tooltip>;
|
||||
};
|
||||
|
||||
export default JVMModeBadge;
|
||||
@@ -47,6 +47,7 @@ const DEFAULT_TIMEOUT_SECONDS = 30;
|
||||
const MAX_TIMEOUT_SECONDS = 3600;
|
||||
const PERSIST_VERSION = 8;
|
||||
const DEFAULT_CONNECTION_TYPE = 'mysql';
|
||||
const DEFAULT_JVM_PORT = 9010;
|
||||
const DEFAULT_GLOBAL_PROXY: GlobalProxyConfig = {
|
||||
enabled: false,
|
||||
type: 'socks5',
|
||||
@@ -73,6 +74,7 @@ const SUPPORTED_CONNECTION_TYPES = new Set([
|
||||
'mongodb',
|
||||
'highgo',
|
||||
'vastbase',
|
||||
'jvm',
|
||||
'sqlite',
|
||||
'duckdb',
|
||||
'custom',
|
||||
@@ -97,6 +99,8 @@ const SSL_SUPPORTED_CONNECTION_TYPES = new Set([
|
||||
|
||||
const getDefaultPortByType = (type: string): number => {
|
||||
switch (type) {
|
||||
case 'jvm':
|
||||
return DEFAULT_JVM_PORT;
|
||||
case 'mysql':
|
||||
case 'mariadb':
|
||||
return 3306;
|
||||
@@ -217,6 +221,68 @@ const normalizeConnectionType = (value: unknown): string => {
|
||||
return SUPPORTED_CONNECTION_TYPES.has(type) ? type : DEFAULT_CONNECTION_TYPE;
|
||||
};
|
||||
|
||||
const sanitizeJVMModes = (value: unknown): Array<'jmx' | 'endpoint' | 'agent'> => {
|
||||
if (!Array.isArray(value)) return ['jmx'];
|
||||
const result: Array<'jmx' | 'endpoint' | 'agent'> = [];
|
||||
const seen = new Set<'jmx' | 'endpoint' | 'agent'>();
|
||||
value.forEach((entry) => {
|
||||
const normalized = toTrimmedString(entry).toLowerCase();
|
||||
if (normalized !== 'jmx' && normalized !== 'endpoint' && normalized !== 'agent') return;
|
||||
if (seen.has(normalized)) return;
|
||||
seen.add(normalized);
|
||||
result.push(normalized);
|
||||
});
|
||||
return result.length > 0 ? result : ['jmx'];
|
||||
};
|
||||
|
||||
const sanitizeJVMConfig = (
|
||||
value: unknown,
|
||||
options: {
|
||||
host: string;
|
||||
port: number;
|
||||
timeout: number;
|
||||
persistSecrets: boolean;
|
||||
}
|
||||
): ConnectionConfig['jvm'] => {
|
||||
const raw = (value && typeof value === 'object') ? value as Record<string, unknown> : {};
|
||||
const allowedModes = sanitizeJVMModes(raw.allowedModes);
|
||||
const preferredModeRaw = toTrimmedString(raw.preferredMode).toLowerCase();
|
||||
const preferredMode = allowedModes.includes(preferredModeRaw as 'jmx' | 'endpoint' | 'agent')
|
||||
? preferredModeRaw as 'jmx' | 'endpoint' | 'agent'
|
||||
: allowedModes[0];
|
||||
const environmentRaw = toTrimmedString(raw.environment, 'dev').toLowerCase();
|
||||
const environment: 'dev' | 'uat' | 'prod' = environmentRaw === 'uat'
|
||||
? 'uat'
|
||||
: environmentRaw === 'prod'
|
||||
? 'prod'
|
||||
: 'dev';
|
||||
const jmxRaw = (raw.jmx && typeof raw.jmx === 'object') ? raw.jmx as Record<string, unknown> : {};
|
||||
const endpointRaw = (raw.endpoint && typeof raw.endpoint === 'object') ? raw.endpoint as Record<string, unknown> : {};
|
||||
const fallbackPort = options.port > 0 ? options.port : DEFAULT_JVM_PORT;
|
||||
const fallbackTimeout = options.timeout > 0 ? options.timeout : DEFAULT_TIMEOUT_SECONDS;
|
||||
|
||||
return {
|
||||
environment,
|
||||
readOnly: typeof raw.readOnly === 'boolean' ? raw.readOnly : true,
|
||||
allowedModes,
|
||||
preferredMode,
|
||||
jmx: {
|
||||
enabled: jmxRaw.enabled === true || allowedModes.includes('jmx'),
|
||||
host: toTrimmedString(jmxRaw.host, options.host) || options.host,
|
||||
port: normalizePort(jmxRaw.port, fallbackPort),
|
||||
username: toTrimmedString(jmxRaw.username),
|
||||
password: options.persistSecrets ? toTrimmedString(jmxRaw.password) : '',
|
||||
domainAllowlist: sanitizeStringArray(jmxRaw.domainAllowlist, 256),
|
||||
},
|
||||
endpoint: {
|
||||
enabled: endpointRaw.enabled === true,
|
||||
baseUrl: toTrimmedString(endpointRaw.baseUrl),
|
||||
apiKey: options.persistSecrets ? toTrimmedString(endpointRaw.apiKey) : '',
|
||||
timeoutSeconds: normalizeIntegerInRange(endpointRaw.timeoutSeconds, fallbackTimeout, 1, MAX_TIMEOUT_SECONDS),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const sanitizeConnectionConfig = (value: unknown): ConnectionConfig => {
|
||||
const raw = (value && typeof value === 'object') ? value as Record<string, unknown> : {};
|
||||
const type = normalizeConnectionType(raw.type);
|
||||
@@ -309,6 +375,15 @@ const sanitizeConnectionConfig = (value: unknown): ConnectionConfig => {
|
||||
safeConfig.dsn = toTrimmedString(raw.dsn).slice(0, MAX_URI_LENGTH);
|
||||
}
|
||||
|
||||
if (type === 'jvm') {
|
||||
safeConfig.jvm = sanitizeJVMConfig(raw.jvm, {
|
||||
host: safeConfig.host,
|
||||
port: safeConfig.port,
|
||||
timeout: safeConfig.timeout || DEFAULT_TIMEOUT_SECONDS,
|
||||
persistSecrets: savePassword,
|
||||
});
|
||||
}
|
||||
|
||||
return safeConfig;
|
||||
};
|
||||
|
||||
|
||||
@@ -201,13 +201,16 @@ export interface TriggerDefinition {
|
||||
export interface TabData {
|
||||
id: string;
|
||||
title: string;
|
||||
type: 'query' | 'table' | 'design' | 'redis-keys' | 'redis-command' | 'redis-monitor' | 'trigger' | 'view-def' | 'routine-def' | 'table-overview';
|
||||
type: 'query' | 'table' | 'design' | 'redis-keys' | 'redis-command' | 'redis-monitor' | 'trigger' | 'view-def' | 'routine-def' | 'table-overview' | 'jvm-overview' | 'jvm-resource' | 'jvm-audit';
|
||||
connectionId: string;
|
||||
dbName?: string;
|
||||
tableName?: string;
|
||||
query?: string;
|
||||
initialTab?: string;
|
||||
readOnly?: boolean;
|
||||
providerMode?: 'jmx' | 'endpoint' | 'agent';
|
||||
resourcePath?: string;
|
||||
resourceKind?: string;
|
||||
redisDB?: number; // Redis database index for redis tabs
|
||||
triggerName?: string; // Trigger name for trigger tabs
|
||||
viewName?: string; // View name for view definition tabs
|
||||
@@ -410,4 +413,3 @@ export interface SecurityUpdateStatus {
|
||||
issues: SecurityUpdateIssue[];
|
||||
lastError?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,13 +7,22 @@ import (
|
||||
|
||||
var newJVMProvider = jvm.NewProvider
|
||||
|
||||
func (a *App) TestJVMConnection(cfg connection.ConnectionConfig) connection.QueryResult {
|
||||
func resolveJVMProvider(cfg connection.ConnectionConfig) (connection.ConnectionConfig, jvm.Provider, error) {
|
||||
normalized, err := jvm.NormalizeConnectionConfig(cfg)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
return connection.ConnectionConfig{}, nil, err
|
||||
}
|
||||
|
||||
provider, err := newJVMProvider(normalized.JVM.PreferredMode)
|
||||
if err != nil {
|
||||
return connection.ConnectionConfig{}, nil, err
|
||||
}
|
||||
|
||||
return normalized, provider, nil
|
||||
}
|
||||
|
||||
func (a *App) TestJVMConnection(cfg connection.ConnectionConfig) connection.QueryResult {
|
||||
normalized, provider, err := resolveJVMProvider(cfg)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
@@ -25,6 +34,34 @@ func (a *App) TestJVMConnection(cfg connection.ConnectionConfig) connection.Quer
|
||||
return connection.QueryResult{Success: true, Message: "JVM 连接成功"}
|
||||
}
|
||||
|
||||
func (a *App) JVMListResources(cfg connection.ConnectionConfig, parentPath string) connection.QueryResult {
|
||||
normalized, provider, err := resolveJVMProvider(cfg)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
items, err := provider.ListResources(a.ctx, normalized, parentPath)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
return connection.QueryResult{Success: true, Data: items}
|
||||
}
|
||||
|
||||
func (a *App) JVMGetValue(cfg connection.ConnectionConfig, resourcePath string) connection.QueryResult {
|
||||
normalized, provider, err := resolveJVMProvider(cfg)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
value, err := provider.GetValue(a.ctx, normalized, resourcePath)
|
||||
if err != nil {
|
||||
return connection.QueryResult{Success: false, Message: err.Error()}
|
||||
}
|
||||
|
||||
return connection.QueryResult{Success: true, Data: value}
|
||||
}
|
||||
|
||||
func (a *App) JVMProbeCapabilities(cfg connection.ConnectionConfig) connection.QueryResult {
|
||||
normalized, err := jvm.NormalizeConnectionConfig(cfg)
|
||||
if err != nil {
|
||||
|
||||
@@ -15,7 +15,9 @@ type fakeJVMProvider struct {
|
||||
probe []jvm.Capability
|
||||
probeErr error
|
||||
list []jvm.ResourceSummary
|
||||
listErr error
|
||||
value jvm.ValueSnapshot
|
||||
valueErr error
|
||||
apply jvm.ApplyResult
|
||||
}
|
||||
|
||||
@@ -27,10 +29,10 @@ func (f fakeJVMProvider) ProbeCapabilities(context.Context, connection.Connectio
|
||||
return f.probe, f.probeErr
|
||||
}
|
||||
func (f fakeJVMProvider) ListResources(context.Context, connection.ConnectionConfig, string) ([]jvm.ResourceSummary, error) {
|
||||
return f.list, nil
|
||||
return f.list, f.listErr
|
||||
}
|
||||
func (f fakeJVMProvider) GetValue(context.Context, connection.ConnectionConfig, string) (jvm.ValueSnapshot, error) {
|
||||
return f.value, nil
|
||||
return f.value, f.valueErr
|
||||
}
|
||||
func (f fakeJVMProvider) PreviewChange(context.Context, connection.ConnectionConfig, jvm.ChangeRequest) (jvm.ChangePreview, error) {
|
||||
return jvm.ChangePreview{Allowed: true, Summary: "preview"}, nil
|
||||
@@ -238,3 +240,84 @@ func TestJVMProbeCapabilitiesUsesReadableLabelForUnsupportedMode(t *testing.T) {
|
||||
t.Fatalf("expected unsupported mode error, got %#v", items[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestJVMListResourcesReturnsProviderPayload(t *testing.T) {
|
||||
app := NewAppWithSecretStore(nil)
|
||||
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
|
||||
return fakeJVMProvider{
|
||||
list: []jvm.ResourceSummary{
|
||||
{
|
||||
ID: "memory.heap",
|
||||
Kind: "folder",
|
||||
Name: "Heap",
|
||||
Path: "/memory/heap",
|
||||
ProviderMode: jvm.ModeJMX,
|
||||
CanRead: true,
|
||||
HasChildren: true,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
res := app.JVMListResources(connection.ConnectionConfig{
|
||||
Type: "jvm",
|
||||
Host: "orders.internal",
|
||||
JVM: connection.JVMConfig{
|
||||
PreferredMode: "jmx",
|
||||
AllowedModes: []string{"jmx"},
|
||||
},
|
||||
}, "/memory")
|
||||
|
||||
if !res.Success {
|
||||
t.Fatalf("expected success, got %+v", res)
|
||||
}
|
||||
items, ok := res.Data.([]jvm.ResourceSummary)
|
||||
if !ok || len(items) != 1 {
|
||||
t.Fatalf("expected one resource summary, got %#v", res.Data)
|
||||
}
|
||||
if items[0].Path != "/memory/heap" {
|
||||
t.Fatalf("expected resource path %q, got %#v", "/memory/heap", items[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestJVMGetValueReturnsProviderPayload(t *testing.T) {
|
||||
app := NewAppWithSecretStore(nil)
|
||||
restore := swapJVMProviderFactory(func(mode string) (jvm.Provider, error) {
|
||||
return fakeJVMProvider{
|
||||
value: jvm.ValueSnapshot{
|
||||
ResourceID: "memory.heap.used",
|
||||
Kind: "metric",
|
||||
Format: "number",
|
||||
Value: 128,
|
||||
Metadata: map[string]any{
|
||||
"unit": "MiB",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
})
|
||||
defer restore()
|
||||
|
||||
res := app.JVMGetValue(connection.ConnectionConfig{
|
||||
Type: "jvm",
|
||||
Host: "orders.internal",
|
||||
JVM: connection.JVMConfig{
|
||||
PreferredMode: "jmx",
|
||||
AllowedModes: []string{"jmx"},
|
||||
},
|
||||
}, "/memory/heap/used")
|
||||
|
||||
if !res.Success {
|
||||
t.Fatalf("expected success, got %+v", res)
|
||||
}
|
||||
snapshot, ok := res.Data.(jvm.ValueSnapshot)
|
||||
if !ok {
|
||||
t.Fatalf("expected value snapshot, got %#v", res.Data)
|
||||
}
|
||||
if snapshot.ResourceID != "memory.heap.used" {
|
||||
t.Fatalf("expected resource id %q, got %#v", "memory.heap.used", snapshot)
|
||||
}
|
||||
if snapshot.Metadata["unit"] != "MiB" {
|
||||
t.Fatalf("expected unit metadata %q, got %#v", "MiB", snapshot.Metadata)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user