mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-08 03:21:32 +08:00
🐛 fix(oceanbase): 修复 Oracle 协议保存与连接链路
- 测试连接统一走 RPC 配置构造,确保 OceanBase Oracle 协议生效 - 保存连接时同步写入 oceanBaseProtocol 与 protocol 参数 - 编辑回显支持从显式字段、连接参数和 URI 恢复协议 - 双击连接时清理旧树缓存,避免复用 MySQL 协议子节点 - 补充 OceanBase 协议解析与缓存 key 隔离测试
This commit is contained in:
@@ -62,6 +62,7 @@ import {
|
||||
import { resolveConnectionSecretDraft } from "../utils/connectionSecretDraft";
|
||||
import { getCustomConnectionDsnValidationMessage } from "../utils/customConnectionDsn";
|
||||
import { mergeParsedUriValuesForForm } from "../utils/connectionUriMerge";
|
||||
import { buildRpcConnectionConfig } from "../utils/connectionRpcConfig";
|
||||
import { CUSTOM_CONNECTION_DRIVER_HELP } from "../utils/driverImportGuidance";
|
||||
import {
|
||||
applyNoAutoCapAttributes,
|
||||
@@ -121,6 +122,14 @@ const OCEANBASE_PROTOCOL_OPTIONS: Array<{
|
||||
{ value: "mysql", label: "MySQL" },
|
||||
{ value: "oracle", label: "Oracle" },
|
||||
];
|
||||
const OCEANBASE_PROTOCOL_PARAM_KEYS = [
|
||||
"protocol",
|
||||
"oceanBaseProtocol",
|
||||
"oceanbaseProtocol",
|
||||
"tenantMode",
|
||||
"compatMode",
|
||||
"mode",
|
||||
];
|
||||
|
||||
const normalizeClickHouseProtocolValue = (
|
||||
value: unknown,
|
||||
@@ -140,6 +149,47 @@ const normalizeOceanBaseProtocolValue = (
|
||||
.toLowerCase();
|
||||
return text === "oracle" ? "oracle" : "mysql";
|
||||
};
|
||||
const resolveOceanBaseProtocolValue = (
|
||||
value: unknown,
|
||||
): OceanBaseProtocolChoice | undefined => {
|
||||
const text = String(value || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!text) return undefined;
|
||||
return ["oracle", "oracle-mode", "oracle_mode", "oboracle"].includes(text)
|
||||
? "oracle"
|
||||
: "mysql";
|
||||
};
|
||||
const resolveOceanBaseProtocolFromQueryText = (
|
||||
value: unknown,
|
||||
): OceanBaseProtocolChoice | undefined => {
|
||||
let text = String(value || "").trim();
|
||||
if (!text) return undefined;
|
||||
const queryIndex = text.indexOf("?");
|
||||
if (queryIndex >= 0) {
|
||||
text = text.slice(queryIndex + 1);
|
||||
}
|
||||
const hashIndex = text.indexOf("#");
|
||||
if (hashIndex >= 0) {
|
||||
text = text.slice(0, hashIndex);
|
||||
}
|
||||
const params = new URLSearchParams(text.replace(/^[?&]+/, ""));
|
||||
for (const key of OCEANBASE_PROTOCOL_PARAM_KEYS) {
|
||||
const protocol = resolveOceanBaseProtocolValue(params.get(key));
|
||||
if (protocol) return protocol;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const resolveOceanBaseProtocolForConfig = (
|
||||
config: Partial<ConnectionConfig>,
|
||||
): OceanBaseProtocolChoice => {
|
||||
return (
|
||||
resolveOceanBaseProtocolValue(config.oceanBaseProtocol) ||
|
||||
resolveOceanBaseProtocolFromQueryText(config.connectionParams) ||
|
||||
resolveOceanBaseProtocolFromQueryText(config.uri) ||
|
||||
"mysql"
|
||||
);
|
||||
};
|
||||
type ConnectionSecretKey =
|
||||
| "primaryPassword"
|
||||
| "sshPassword"
|
||||
@@ -1125,6 +1175,18 @@ const ConnectionModal: React.FC<{
|
||||
return cloned.toString().slice(0, MAX_CONNECTION_PARAMS_LENGTH);
|
||||
};
|
||||
|
||||
const normalizeOceanBaseConnectionParamsText = (
|
||||
rawParams: unknown,
|
||||
selectedProtocol: OceanBaseProtocolChoice,
|
||||
) => {
|
||||
const params = new URLSearchParams(normalizeConnectionParamsText(rawParams));
|
||||
for (const key of OCEANBASE_PROTOCOL_PARAM_KEYS) {
|
||||
params.delete(key);
|
||||
}
|
||||
params.set("protocol", selectedProtocol);
|
||||
return params.toString().slice(0, MAX_CONNECTION_PARAMS_LENGTH);
|
||||
};
|
||||
|
||||
const mergeConnectionParams = (
|
||||
params: URLSearchParams,
|
||||
rawParams: unknown,
|
||||
@@ -2260,7 +2322,7 @@ const ConnectionModal: React.FC<{
|
||||
: "auto",
|
||||
oceanBaseProtocol:
|
||||
configType === "oceanbase"
|
||||
? normalizeOceanBaseProtocolValue(config.oceanBaseProtocol)
|
||||
? resolveOceanBaseProtocolForConfig(config)
|
||||
: "mysql",
|
||||
includeDatabases: initialValues.includeDatabases,
|
||||
includeRedisDatabases: initialValues.includeRedisDatabases,
|
||||
@@ -2780,12 +2842,14 @@ const ConnectionModal: React.FC<{
|
||||
// Use different API for Redis / JVM
|
||||
const isRedisType = values.type === "redis";
|
||||
const isJVMType = values.type === "jvm";
|
||||
const dbTestConfig =
|
||||
!isRedisType && !isJVMType ? buildRpcConnectionConfig(config as any) : config;
|
||||
const res = await withClientTimeout(
|
||||
isJVMType
|
||||
? TestJVMConnection(config as any)
|
||||
: isRedisType
|
||||
? RedisConnect(config as any)
|
||||
: TestConnection(config as any),
|
||||
: TestConnection(dbTestConfig as any),
|
||||
rpcTimeoutMs,
|
||||
`连接测试超时(>${timeoutSeconds} 秒),请检查网络/代理/SSH配置后重试`,
|
||||
);
|
||||
@@ -2798,7 +2862,7 @@ const ConnectionModal: React.FC<{
|
||||
} else if (!isJVMType) {
|
||||
// Other databases: fetch database list
|
||||
const dbRes = await withClientTimeout(
|
||||
DBGetDatabases(config as any),
|
||||
DBGetDatabases(dbTestConfig as any),
|
||||
rpcTimeoutMs,
|
||||
`连接成功但拉取数据库列表超时(>${timeoutSeconds} 秒)`,
|
||||
);
|
||||
@@ -3297,6 +3361,14 @@ const ConnectionModal: React.FC<{
|
||||
}
|
||||
|
||||
const keepPassword = !forPersist || savePassword;
|
||||
const normalizedConnectionParams = supportsConnectionParamsForType(type)
|
||||
? type === "oceanbase"
|
||||
? normalizeOceanBaseConnectionParamsText(
|
||||
mergedValues.connectionParams,
|
||||
selectedOceanBaseProtocol,
|
||||
)
|
||||
: normalizeConnectionParamsText(mergedValues.connectionParams)
|
||||
: "";
|
||||
|
||||
return {
|
||||
type: mergedValues.type,
|
||||
@@ -3318,9 +3390,7 @@ const ConnectionModal: React.FC<{
|
||||
httpTunnel: httpTunnelConfig,
|
||||
driver: mergedValues.driver,
|
||||
dsn: mergedValues.dsn,
|
||||
connectionParams: supportsConnectionParamsForType(type)
|
||||
? normalizeConnectionParamsText(mergedValues.connectionParams)
|
||||
: "",
|
||||
connectionParams: normalizedConnectionParams,
|
||||
timeout: Number(mergedValues.timeout || 30),
|
||||
redisDB: Number.isFinite(Number(mergedValues.redisDB))
|
||||
? Math.max(0, Math.min(15, Math.trunc(Number(mergedValues.redisDB))))
|
||||
|
||||
@@ -91,6 +91,20 @@ type DriverStatusSnapshot = {
|
||||
message?: string;
|
||||
};
|
||||
|
||||
const buildConnectionReloadSignature = (conn?: SavedConnection | null): string => {
|
||||
if (!conn) return '';
|
||||
return JSON.stringify({
|
||||
config: conn.config || {},
|
||||
includeDatabases: conn.includeDatabases || [],
|
||||
includeRedisDatabases: conn.includeRedisDatabases || [],
|
||||
});
|
||||
};
|
||||
|
||||
const isConnectionTreeKey = (key: React.Key, connectionId: string): boolean => {
|
||||
const text = String(key);
|
||||
return text === connectionId || text.startsWith(`${connectionId}-`);
|
||||
};
|
||||
|
||||
const DRIVER_STATUS_CACHE_TTL_MS = 30_000;
|
||||
|
||||
const normalizeDriverType = (value: string): string => {
|
||||
@@ -248,6 +262,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const driverStatusCacheRef = useRef<{ fetchedAt: number; items: Record<string, DriverStatusSnapshot> } | null>(null);
|
||||
const driverUpdateWarningKeysRef = useRef<Set<string>>(new Set());
|
||||
const connectionReloadSignaturesRef = useRef<Record<string, string>>({});
|
||||
const [contextMenu, setContextMenu] = useState<{ x: number, y: number, items: MenuProps['items'] } | null>(null);
|
||||
|
||||
// Virtual Scroll State
|
||||
@@ -375,6 +390,47 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
}, [autoFetchVisible, externalSQLDirectories, savedQueries]);
|
||||
|
||||
useEffect(() => {
|
||||
const previousSignatures = connectionReloadSignaturesRef.current;
|
||||
const nextSignatures: Record<string, string> = {};
|
||||
const staleConnectionIds = new Set<string>();
|
||||
|
||||
connections.forEach((conn) => {
|
||||
const signature = buildConnectionReloadSignature(conn);
|
||||
nextSignatures[conn.id] = signature;
|
||||
if (previousSignatures[conn.id] && previousSignatures[conn.id] !== signature) {
|
||||
staleConnectionIds.add(conn.id);
|
||||
}
|
||||
});
|
||||
connectionReloadSignaturesRef.current = nextSignatures;
|
||||
|
||||
if (staleConnectionIds.size > 0) {
|
||||
const staleIds = Array.from(staleConnectionIds);
|
||||
setLoadedKeys((prev) =>
|
||||
prev.filter((key) => !staleIds.some((id) => isConnectionTreeKey(key, id))),
|
||||
);
|
||||
setExpandedKeys((prev) =>
|
||||
prev.filter((key) => !staleIds.some((id) => isConnectionTreeKey(key, id))),
|
||||
);
|
||||
setConnectionStates((prev) => {
|
||||
const next = { ...prev };
|
||||
staleIds.forEach((id) => {
|
||||
Object.keys(next).forEach((key) => {
|
||||
if (isConnectionTreeKey(key, id)) {
|
||||
delete next[key];
|
||||
}
|
||||
});
|
||||
});
|
||||
return next;
|
||||
});
|
||||
staleIds.forEach((id) => {
|
||||
Array.from(loadingNodesRef.current).forEach((key) => {
|
||||
if (key === `dbs-${id}` || key.startsWith(`tables-${id}-`)) {
|
||||
loadingNodesRef.current.delete(key);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setTreeData((prev) => {
|
||||
const prevMap = new Map<string, TreeNode>();
|
||||
|
||||
@@ -395,6 +451,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
const existing = prevMap.get(conn.id);
|
||||
const iconType = resolveConnectionIconType(conn);
|
||||
const iconColor = resolveConnectionAccentColor(conn);
|
||||
const preserveChildren = existing && !staleConnectionIds.has(conn.id);
|
||||
return {
|
||||
title: conn.name,
|
||||
key: conn.id,
|
||||
@@ -402,7 +459,7 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
|
||||
type: 'connection',
|
||||
dataRef: conn,
|
||||
isLeaf: false,
|
||||
children: existing?.children,
|
||||
children: preserveChildren ? existing.children : undefined,
|
||||
} as TreeNode;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user