feat(frontend):接入JVM连接表单与展示元数据

This commit is contained in:
Syngnat
2026-04-23 09:23:28 +08:00
parent 03a1506686
commit 177dafacc9
4 changed files with 343 additions and 17 deletions

View File

@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { Modal, Form, Input, InputNumber, Button, message, Checkbox, Divider, Select, Alert, Card, Row, Col, Typography, Collapse, Space, Table, Tag } from 'antd';
import { DatabaseOutlined, ConsoleSqlOutlined, FileTextOutlined, CloudServerOutlined, AppstoreAddOutlined, CloudOutlined, CheckCircleFilled, CloseCircleFilled, LinkOutlined, EditOutlined, AppstoreOutlined, BgColorsOutlined } from '@ant-design/icons';
import { DatabaseOutlined, FileTextOutlined, CloudOutlined, CheckCircleFilled, CloseCircleFilled, LinkOutlined, EditOutlined, AppstoreOutlined, BgColorsOutlined } from '@ant-design/icons';
import { getDbIcon, getDbDefaultColor, getDbIconLabel, DB_ICON_TYPES, PRESET_ICON_COLORS } from './DatabaseIcons';
import { useStore } from '../store';
import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
@@ -14,10 +14,11 @@ import { resolveConnectionSecretDraft } from '../utils/connectionSecretDraft';
import { getCustomConnectionDsnValidationMessage } from '../utils/customConnectionDsn';
import { CUSTOM_CONNECTION_DRIVER_HELP } from '../utils/driverImportGuidance';
import { applyNoAutoCapAttributes, noAutoCapInputProps } from '../utils/inputAutoCap';
import { DBGetDatabases, GetDriverStatusList, MongoDiscoverMembers, TestConnection, RedisConnect, SelectDatabaseFile, SelectSSHKeyFile } from '../../wailsjs/go/app/App';
import { buildDefaultJVMConnectionValues, buildJVMConnectionConfig } from '../utils/jvmConnectionConfig';
import { JVM_RUNTIME_MODES, resolveJVMModeMeta } from '../utils/jvmRuntimePresentation';
import { DBGetDatabases, GetDriverStatusList, MongoDiscoverMembers, TestConnection, RedisConnect, SelectDatabaseFile, SelectSSHKeyFile, TestJVMConnection } from '../../wailsjs/go/app/App';
import { ConnectionConfig, MongoMemberInfo, SavedConnection } from '../types';
const { Meta } = Card;
const { Text } = Typography;
const MAX_URI_LENGTH = 4096;
const MAX_URI_HOSTS = 32;
@@ -51,6 +52,7 @@ const createEmptyConnectionSecretClearState = (): ConnectionSecretClearState =>
const getDefaultPortByType = (type: string) => {
switch (type) {
case 'jvm': return 9010;
case 'mysql': return 3306;
case 'doris':
case 'diros': return 9030;
@@ -169,6 +171,16 @@ const ConnectionModal: React.FC<{
const mongoTopology = Form.useWatch('mongoTopology', form) || 'single';
const mongoSrv = Form.useWatch('mongoSrv', form) || false;
const redisTopology = Form.useWatch('redisTopology', form) || 'single';
const jvmAllowedModes = Form.useWatch('jvmAllowedModes', form);
const jvmPreferredMode = Form.useWatch('jvmPreferredMode', form) || 'jmx';
const normalizedJvmAllowedModes = useMemo(() => {
const modes = Array.isArray(jvmAllowedModes)
? jvmAllowedModes
.map((mode) => String(mode || '').trim().toLowerCase())
.filter((mode) => JVM_RUNTIME_MODES.includes(mode as typeof JVM_RUNTIME_MODES[number]))
: [];
return modes.length > 0 ? Array.from(new Set(modes)) : ['jmx'];
}, [jvmAllowedModes]);
const isMySQLLike = dbType === 'mysql' || dbType === 'mariadb' || dbType === 'diros' || dbType === 'sphinx';
const isSSLType = supportsSSLForType(dbType);
const sslHintText = isMySQLLike
@@ -1187,8 +1199,10 @@ const ConnectionModal: React.FC<{
setStep(2);
const config: any = initialValues.config || {};
const configType = String(config.type || 'mysql');
const isJvmConfigType = configType === 'jvm';
const defaultPort = getDefaultPortByType(configType);
const isFileDbConfigType = isFileDatabaseType(configType);
const jvmDefaultValues = buildDefaultJVMConnectionValues();
const normalizedHosts = isFileDbConfigType ? [] : normalizeAddressList(config.hosts, defaultPort);
const primaryAddress = isFileDbConfigType
? null
@@ -1208,6 +1222,18 @@ const ConnectionModal: React.FC<{
const mysqlIsReplica = String(config.topology || '').toLowerCase() === 'replica' || mysqlReplicaHosts.length > 0;
const mongoIsReplica = String(config.topology || '').toLowerCase() === 'replica' || mongoHosts.length > 0 || !!config.replicaSet;
const redisIsCluster = String(config.topology || '').toLowerCase() === 'cluster' || redisHosts.length > 0;
const jvmAllowedModes = Array.isArray(config.jvm?.allowedModes) && config.jvm.allowedModes.length > 0
? config.jvm.allowedModes.map((mode: string) => String(mode || '').trim().toLowerCase())
: jvmDefaultValues.jvmAllowedModes;
const normalizedJvmAllowedModes = jvmAllowedModes.filter((mode: string) =>
JVM_RUNTIME_MODES.includes(mode as typeof JVM_RUNTIME_MODES[number]),
);
const resolvedJvmAllowedModes = normalizedJvmAllowedModes.length > 0
? Array.from(new Set(normalizedJvmAllowedModes))
: jvmDefaultValues.jvmAllowedModes;
const resolvedJvmPreferredMode = resolvedJvmAllowedModes.includes(String(config.jvm?.preferredMode || '').trim().toLowerCase())
? String(config.jvm?.preferredMode || '').trim().toLowerCase()
: resolvedJvmAllowedModes[0];
const hasHttpTunnel = !!config.useHttpTunnel;
const hasProxy = !hasHttpTunnel && !!config.useProxy;
form.setFieldsValue({
@@ -1261,7 +1287,27 @@ const ConnectionModal: React.FC<{
savePassword: config.savePassword !== false,
redisDB: Number.isFinite(Number(config.redisDB)) ? Number(config.redisDB) : 0,
mongoReplicaUser: config.mongoReplicaUser || '',
mongoReplicaPassword: config.mongoReplicaPassword || ''
mongoReplicaPassword: config.mongoReplicaPassword || '',
jvmReadOnly: isJvmConfigType ? config.jvm?.readOnly ?? jvmDefaultValues.jvmReadOnly : jvmDefaultValues.jvmReadOnly,
jvmAllowedModes: isJvmConfigType ? resolvedJvmAllowedModes : jvmDefaultValues.jvmAllowedModes,
jvmPreferredMode: isJvmConfigType ? resolvedJvmPreferredMode : jvmDefaultValues.jvmPreferredMode,
jvmEnvironment: isJvmConfigType ? config.jvm?.environment || jvmDefaultValues.jvmEnvironment : jvmDefaultValues.jvmEnvironment,
jvmEndpointEnabled: isJvmConfigType
? config.jvm?.endpoint?.enabled ?? resolvedJvmAllowedModes.includes('endpoint')
: jvmDefaultValues.jvmEndpointEnabled,
jvmEndpointBaseUrl: isJvmConfigType ? config.jvm?.endpoint?.baseUrl || '' : jvmDefaultValues.jvmEndpointBaseUrl,
jvmEndpointApiKey: isJvmConfigType ? config.jvm?.endpoint?.apiKey || '' : jvmDefaultValues.jvmEndpointApiKey,
jvmEndpointTimeoutSeconds: isJvmConfigType
? Number(config.jvm?.endpoint?.timeoutSeconds || config.timeout || 30)
: Number(config.timeout || 30),
jvmJmxHost: isJvmConfigType && config.jvm?.jmx?.host && config.jvm.jmx.host !== primaryHost
? config.jvm.jmx.host
: '',
jvmJmxPort: isJvmConfigType && Number(config.jvm?.jmx?.port) > 0 && Number(config.jvm.jmx.port) !== Number(primaryPort || defaultPort)
? Number(config.jvm.jmx.port)
: undefined,
jvmJmxUsername: isJvmConfigType ? config.jvm?.jmx?.username || '' : '',
jvmJmxPassword: isJvmConfigType ? config.jvm?.jmx?.password || '' : ''
});
setUseSSL(!!config.useSSL);
setCustomIconType(initialValues.iconType);
@@ -1559,10 +1605,13 @@ const ConnectionModal: React.FC<{
: 30;
const rpcTimeoutMs = (timeoutSeconds + 5) * 1000;
// Use different API for Redis
// Use different API for Redis / JVM
const isRedisType = values.type === 'redis';
const isJVMType = values.type === 'jvm';
const res = await withClientTimeout(
isRedisType
isJVMType
? TestJVMConnection(config as any)
: isRedisType
? RedisConnect(config as any)
: TestConnection(config as any),
rpcTimeoutMs,
@@ -1574,7 +1623,7 @@ const ConnectionModal: React.FC<{
setTestResult({ type: 'success', message: res.message });
if (isRedisType) {
setRedisDbList(Array.from({ length: 16 }, (_, i) => i));
} else {
} else if (!isJVMType) {
// Other databases: fetch database list
const dbRes = await withClientTimeout(
DBGetDatabases(config as any),
@@ -1675,6 +1724,26 @@ const ConnectionModal: React.FC<{
const buildConfig = async (values: any, forPersist: boolean): Promise<ConnectionConfig> => {
const mergedValues = { ...values };
if (String(mergedValues.type || '').trim().toLowerCase() === 'jvm') {
const nextJvmAllowedModes = Array.isArray(mergedValues.jvmAllowedModes)
? mergedValues.jvmAllowedModes
.map((mode: string) => String(mode || '').trim().toLowerCase())
.filter((mode: string) => JVM_RUNTIME_MODES.includes(mode as typeof JVM_RUNTIME_MODES[number]))
: [];
const resolvedJvmAllowedModes = nextJvmAllowedModes.length > 0
? Array.from(new Set(nextJvmAllowedModes))
: buildDefaultJVMConnectionValues().jvmAllowedModes;
return buildJVMConnectionConfig({
...buildDefaultJVMConnectionValues(),
...mergedValues,
jvmAllowedModes: resolvedJvmAllowedModes,
jvmPreferredMode: resolvedJvmAllowedModes.includes(String(mergedValues.jvmPreferredMode || '').trim().toLowerCase())
? String(mergedValues.jvmPreferredMode || '').trim().toLowerCase()
: resolvedJvmAllowedModes[0],
jvmEndpointEnabled: resolvedJvmAllowedModes.includes('endpoint'),
jvmEndpointTimeoutSeconds: Number(mergedValues.jvmEndpointTimeoutSeconds || mergedValues.timeout || 30),
});
}
const parsedUriValues = parseUriToValues(mergedValues.uri, mergedValues.type);
const isEmptyField = (value: unknown) => (
value === undefined
@@ -1905,7 +1974,66 @@ const ConnectionModal: React.FC<{
form.setFieldsValue({ type: type });
const defaultPort = getDefaultPortByType(type);
if (isFileDatabaseType(type)) {
if (type === 'jvm') {
const jvmDefaultValues = buildDefaultJVMConnectionValues();
setUseSSL(false);
setUseSSH(false);
setUseProxy(false);
setUseHttpTunnel(false);
form.setFieldsValue({
...jvmDefaultValues,
user: '',
password: '',
database: '',
useSSL: false,
sslMode: undefined,
sslCertPath: undefined,
sslKeyPath: undefined,
useSSH: false,
sshHost: '',
sshPort: 22,
sshUser: '',
sshPassword: '',
sshKeyPath: '',
useProxy: false,
proxyType: 'socks5',
proxyHost: '',
proxyPort: 1080,
proxyUser: '',
proxyPassword: '',
useHttpTunnel: false,
httpTunnelHost: '',
httpTunnelPort: 8080,
httpTunnelUser: '',
httpTunnelPassword: '',
timeout: 30,
uri: '',
includeDatabases: undefined,
includeRedisDatabases: undefined,
mysqlTopology: 'single',
redisTopology: 'single',
mongoTopology: 'single',
mongoSrv: false,
mongoReadPreference: 'primary',
mongoReplicaSet: '',
mongoAuthSource: '',
mongoAuthMechanism: '',
savePassword: true,
mysqlReplicaHosts: [],
redisHosts: [],
mongoHosts: [],
mysqlReplicaUser: '',
mysqlReplicaPassword: '',
mongoReplicaUser: '',
mongoReplicaPassword: '',
redisDB: 0,
jvmEndpointTimeoutSeconds: 30,
jvmJmxHost: '',
jvmJmxPort: undefined,
jvmJmxUsername: '',
jvmJmxPassword: '',
});
} else if (isFileDatabaseType(type)) {
setUseSSL(false);
setUseSSH(false);
setUseProxy(false);
@@ -2008,6 +2136,7 @@ const ConnectionModal: React.FC<{
const isFileDb = isFileDatabaseType(dbType);
const isCustom = dbType === 'custom';
const isRedis = dbType === 'redis';
const isJVM = dbType === 'jvm';
const currentDriverType = normalizeDriverType(dbType);
const currentDriverSnapshot = driverStatusMap[currentDriverType];
const currentDriverUnavailableReason = currentDriverType !== 'custom'
@@ -2044,6 +2173,7 @@ const ConnectionModal: React.FC<{
{ key: 'tdengine', name: 'TDengine', icon: getDbIcon('tdengine', undefined, 36) },
]},
{ label: '其他', items: [
{ key: 'jvm', name: 'JVM Runtime', icon: getDbIcon('jvm', undefined, 36) },
{ key: 'custom', name: 'Custom (自定义)', icon: getDbIcon('custom', undefined, 36) },
]},
];
@@ -2128,7 +2258,7 @@ const ConnectionModal: React.FC<{
<Input {...noAutoCapInputProps} placeholder="例如:本地测试库" />
</Form.Item>
{!isCustom && (
{!isCustom && !isJVM && (
<>
<Form.Item
name="uri"
@@ -2178,6 +2308,83 @@ const ConnectionModal: React.FC<{
description: '当前已保存连接字符串。留空表示继续沿用,输入新值表示替换。',
})}
</>
) : isJVM ? (
<>
<div style={{ display: 'grid', gridTemplateColumns: 'minmax(0, 1fr) 120px', gap: 16, alignItems: 'start' }}>
<Form.Item
name="host"
label="主机地址 (Host)"
rules={[{ required: true, message: '请输入 JVM 主机地址' }]}
style={{ marginBottom: 0 }}
>
<Input {...noAutoCapInputProps} placeholder="localhost" />
</Form.Item>
<Form.Item
name="port"
label="端口 (Port)"
rules={[{ required: true, message: '请输入 JVM 端口号' }]}
style={{ marginBottom: 0 }}
>
<InputNumber style={{ width: '100%' }} min={1} max={65535} />
</Form.Item>
</div>
<Form.Item
name="jvmAllowedModes"
label="允许接入模式"
rules={[{ required: true, message: '请至少选择一种 JVM 接入模式' }]}
help="首期以 JMX / Endpoint 为主Agent 仅保留扩展位。"
>
<Select
mode="multiple"
allowClear
placeholder="请选择 JVM 接入模式"
options={JVM_RUNTIME_MODES.map((mode) => ({
value: mode,
label: resolveJVMModeMeta(mode).label,
}))}
/>
</Form.Item>
<Form.Item
name="jvmPreferredMode"
label="首选接入模式"
rules={[{ required: true, message: '请选择首选 JVM 接入模式' }]}
>
<Select
options={normalizedJvmAllowedModes.map((mode) => ({
value: mode,
label: resolveJVMModeMeta(mode).label,
}))}
/>
</Form.Item>
<Form.Item name="jvmReadOnly" valuePropName="checked" style={{ marginBottom: 12 }}>
<Checkbox></Checkbox>
</Form.Item>
<Form.Item
name="jvmEndpointBaseUrl"
label="JVM Endpoint Base URL"
rules={[{
required: jvmPreferredMode === 'endpoint',
message: '启用 Endpoint 模式时请输入 Base URL',
}]}
help="当允许或首选 Endpoint 时,用于后端探测与访问 JVM 管理端点。"
>
<Input {...noAutoCapInputProps} placeholder="例如https://orders.internal/manage/jvm" />
</Form.Item>
<Form.Item
name="timeout"
label="连接超时 (秒)"
help="JVM 连接测试超时时间,默认 30 秒"
rules={[{ type: 'number', min: 1, max: 300, message: '超时时间范围: 1-300 秒' }]}
style={{ marginBottom: 0 }}
>
<InputNumber style={{ width: '100%' }} min={1} max={300} placeholder="30" />
</Form.Item>
</>
) : (
<>
<div style={{ display: 'grid', gridTemplateColumns: isFileDb ? 'minmax(0, 1fr) 120px' : 'minmax(0, 1fr) 120px', gap: 16, alignItems: 'start' }}>
@@ -2428,7 +2635,7 @@ const ConnectionModal: React.FC<{
</>
)}
{!isFileDb && !isRedis && (
{!isFileDb && !isRedis && !isJVM && (
<>
<div style={{ display: 'grid', gridTemplateColumns: dbType === 'mongodb' ? 'minmax(0, 1fr) minmax(0, 1fr) 180px' : 'repeat(2, minmax(0, 1fr))', gap: 16 }}>
<Form.Item
@@ -2480,7 +2687,7 @@ const ConnectionModal: React.FC<{
</Form.Item>
)}
{!isFileDb && !isRedis && (
{!isFileDb && !isRedis && !isJVM && (
<Form.Item name="includeDatabases" label="显示数据库 (留空显示全部)" help="连接测试成功后可选择" style={{ marginTop: 12, marginBottom: 0 }}>
<Select mode="multiple" placeholder="选择显示的数据库" allowClear>
{dbList.map(db => <Select.Option key={db} value={db}>{db}</Select.Option>)}
@@ -2492,7 +2699,7 @@ const ConnectionModal: React.FC<{
</div>
);
const networkSecuritySection = !isFileDb ? (() => {
const networkSecuritySection = !isFileDb && !isJVM ? (() => {
const networkItems: Array<{
key: 'ssl' | 'ssh' | 'proxy' | 'httpTunnel';
title: string;
@@ -2815,6 +3022,18 @@ const ConnectionModal: React.FC<{
mongoReplicaUser: '',
mongoReplicaPassword: '',
redisDB: 0,
jvmReadOnly: true,
jvmAllowedModes: ['jmx'],
jvmPreferredMode: 'jmx',
jvmEnvironment: 'dev',
jvmEndpointEnabled: false,
jvmEndpointBaseUrl: '',
jvmEndpointApiKey: '',
jvmEndpointTimeoutSeconds: 30,
jvmJmxHost: '',
jvmJmxPort: undefined,
jvmJmxUsername: '',
jvmJmxPassword: '',
}}
onValuesChange={(changed) => {
if (testResult) {
@@ -2871,6 +3090,21 @@ const ConnectionModal: React.FC<{
}
}
if (changed.type !== undefined) setDbType(changed.type);
if (changed.jvmAllowedModes !== undefined) {
const nextModes = Array.isArray(changed.jvmAllowedModes)
? changed.jvmAllowedModes
.map((mode: string) => String(mode || '').trim().toLowerCase())
.filter((mode: string) => JVM_RUNTIME_MODES.includes(mode as typeof JVM_RUNTIME_MODES[number]))
: [];
const resolvedModes = nextModes.length > 0 ? Array.from(new Set(nextModes)) : ['jmx'];
const currentPreferredMode = String(form.getFieldValue('jvmPreferredMode') || '').trim().toLowerCase();
form.setFieldValue('jvmAllowedModes', resolvedModes);
form.setFieldValue(
'jvmPreferredMode',
resolvedModes.includes(currentPreferredMode) ? currentPreferredMode : resolvedModes[0],
);
form.setFieldValue('jvmEndpointEnabled', resolvedModes.includes('endpoint'));
}
if (changed.redisTopology !== undefined) {
const supportedDbs = Array.from({ length: 16 }, (_, i) => i);
setRedisDbList(supportedDbs);
@@ -2914,7 +3148,7 @@ const ConnectionModal: React.FC<{
{(() => {
const sectionItems: Array<{ key: 'basic' | 'network' | 'appearance'; title: string; description: string; icon: React.ReactNode }> = [
{ key: 'basic', title: '基础信息', description: '名称、地址、认证、URI 与数据库范围', icon: <DatabaseOutlined /> },
...(!isCustom && !isFileDb ? [{ key: 'network' as const, title: '网络与安全', description: 'SSL、SSH、代理与高级连接', icon: <CloudOutlined /> }] : []),
...(!isCustom && !isFileDb && !isJVM ? [{ key: 'network' as const, title: '网络与安全', description: 'SSL、SSH、代理与高级连接', icon: <CloudOutlined /> }] : []),
{ key: 'appearance', title: '外观', description: '自定义图标与颜色', icon: <BgColorsOutlined /> },
];
const resolvedSection = sectionItems.some((item) => item.key === activeConfigSection)
@@ -3222,5 +3456,3 @@ const ConnectionModal: React.FC<{
};
export default ConnectionModal;

View File

@@ -15,6 +15,7 @@ const DB_DEFAULT_COLORS: Record<string, string> = {
postgres: '#336791',
redis: '#DC382D',
mongodb: '#47A248',
jvm: '#1677FF',
kingbase: '#1890FF',
dameng: '#E6002D',
oracle: '#F80000',
@@ -136,6 +137,9 @@ const HighGoIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
const TDengineIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.tdengine} label="TD" />
);
const JVMIcon: React.FC<DbIconProps> = ({ size = 16, color }) => (
<ColorBadge size={size} color={color || DB_DEFAULT_COLORS.jvm} label="JVM" />
);
/** Custom — 齿轮图标 */
const CustomIcon: React.FC<DbIconProps> = ({ size = 16, color }) => {
@@ -166,6 +170,7 @@ const DB_ICON_MAP: Record<string, React.FC<DbIconProps>> = {
postgres: PostgresIcon,
redis: RedisIcon,
mongodb: MongoDBIcon,
jvm: JVMIcon,
kingbase: KingBaseIcon,
dameng: DamengIcon,
oracle: OracleIcon,
@@ -181,7 +186,7 @@ const DB_ICON_MAP: Record<string, React.FC<DbIconProps>> = {
/** 可选图标类型列表(用于图标选择器 UI */
export const DB_ICON_TYPES: string[] = [
'mysql', 'mariadb', 'postgres', 'redis', 'mongodb',
'mysql', 'mariadb', 'postgres', 'redis', 'mongodb', 'jvm',
'oracle', 'sqlserver', 'sqlite', 'duckdb', 'clickhouse',
'kingbase', 'dameng', 'vastbase', 'highgo', 'tdengine', 'custom',
];
@@ -200,7 +205,8 @@ export const getDbIcon = (type: string, color?: string, size?: number): React.Re
export const getDbIconLabel = (type: string): string => {
const labels: Record<string, string> = {
mysql: 'MySQL', mariadb: 'MariaDB', postgres: 'PostgreSQL',
redis: 'Redis', mongodb: 'MongoDB', oracle: 'Oracle',
redis: 'Redis', mongodb: 'MongoDB', jvm: 'JVM',
oracle: 'Oracle',
sqlserver: 'SQL Server', clickhouse: 'ClickHouse', sqlite: 'SQLite',
duckdb: 'DuckDB', kingbase: '金仓', dameng: '达梦',
vastbase: 'VastBase', highgo: '瀚高', tdengine: 'TDengine',

View File

@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest';
import { buildJVMTabTitle, resolveJVMModeMeta } from './jvmRuntimePresentation';
describe('jvmRuntimePresentation', () => {
it('returns labels for built-in JVM modes', () => {
expect(resolveJVMModeMeta('jmx').label).toBe('JMX');
expect(resolveJVMModeMeta('endpoint').label).toBe('Endpoint');
});
it('builds overview tab titles with connection name and mode label', () => {
expect(buildJVMTabTitle('Orders JVM', 'overview', 'jmx')).toBe('[Orders JVM] JVM 概览 · JMX');
});
});

View File

@@ -0,0 +1,74 @@
export type JVMRuntimeMode = 'jmx' | 'endpoint' | 'agent';
export type JVMTabKind = 'overview' | 'resource' | 'audit';
export type JVMModeMeta = {
mode: string;
label: string;
color: string;
backgroundColor: string;
};
export const JVM_RUNTIME_MODES: JVMRuntimeMode[] = ['jmx', 'endpoint', 'agent'];
const JVM_MODE_META_MAP: Record<JVMRuntimeMode, JVMModeMeta> = {
jmx: {
mode: 'jmx',
label: 'JMX',
color: '#1D39C4',
backgroundColor: 'rgba(29, 57, 196, 0.12)',
},
endpoint: {
mode: 'endpoint',
label: 'Endpoint',
color: '#1677FF',
backgroundColor: 'rgba(22, 119, 255, 0.12)',
},
agent: {
mode: 'agent',
label: 'Agent',
color: '#FA8C16',
backgroundColor: 'rgba(250, 140, 22, 0.12)',
},
};
const JVM_TAB_KIND_LABELS: Record<JVMTabKind, string> = {
overview: 'JVM 概览',
resource: 'JVM 资源浏览',
audit: 'JVM 审计记录',
};
const normalizeMode = (mode: string): string => String(mode || '').trim().toLowerCase();
const toTitleCase = (value: string): string => {
if (!value) {
return 'Unknown';
}
return value.charAt(0).toUpperCase() + value.slice(1);
};
export const resolveJVMModeMeta = (mode: string): JVMModeMeta => {
const normalizedMode = normalizeMode(mode);
if (normalizedMode in JVM_MODE_META_MAP) {
return JVM_MODE_META_MAP[normalizedMode as JVMRuntimeMode];
}
return {
mode: normalizedMode || 'unknown',
label: toTitleCase(normalizedMode || 'unknown'),
color: '#8C8C8C',
backgroundColor: 'rgba(140, 140, 140, 0.12)',
};
};
export const buildJVMTabTitle = (
connectionName: string,
tabKind: JVMTabKind,
mode: string,
): string => {
const trimmedConnectionName = String(connectionName || '').trim();
const tabLabel = JVM_TAB_KIND_LABELS[tabKind] || 'JVM';
const modeLabel = resolveJVMModeMeta(mode).label;
const prefix = trimmedConnectionName ? `[${trimmedConnectionName}] ` : '';
return `${prefix}${tabLabel} · ${modeLabel}`;
};