🐛 fix(dameng): 修复达梦数据源侧边栏无法展开数据库节点的问题

- 权限适配:取消对 SYSDBA schema 的默认过滤,并增加 `SELECT USER FROM DUAL` 兜底查询
- 树节点容错:Sidebar 当数据库为空时不再阻断加载状态,允许用户重试刷新并增加明确提示
- 类型修正:修复 RedisMonitor 组件 `NodeJS.Timeout` 在 Vite 下的编译报错
- 测试覆盖:补充达梦 SYSDBA 过滤及兜底查询逻辑的单元测试
This commit is contained in:
Syngnat
2026-03-30 19:46:05 +08:00
parent aa9d8d243a
commit 31644dee6b
5 changed files with 63 additions and 6 deletions

View File

@@ -1 +1 @@
f697e821b4acd5cf614d63d46453e8a4
20168ff7047e0ecea00acb73f413f7db

View File

@@ -51,7 +51,7 @@ const RedisMonitor: React.FC<RedisMonitorProps> = ({ connectionId, redisDB }) =>
// Ref to track if component is mounted to prevent state updates after unmount
const mountedRef = useRef(true);
// Interval ref
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
// Previous ops counter to calculate QPS if instantaneous_ops_per_sec is not enough
const prevMetricsRef = useRef({ prevOps: 0, prevTime: 0 });

View File

@@ -1036,13 +1036,21 @@ const Sidebar: React.FC<{ onEditConnection?: (conn: SavedConnection) => void }>
dbs = dbs.filter(db => conn.includeDatabases!.includes(db.title));
}
setTreeData(origin => updateTreeData(origin, node.key, dbs));
if (dbs.length > 0) {
setTreeData(origin => updateTreeData(origin, node.key, dbs));
} else {
// 空列表:清理 loadedKeys 以允许重新加载,不设置 children = []
setLoadedKeys(prev => prev.filter(k => k !== node.key));
message.warning({ content: '未获取到可见数据库/schema请检查账号权限或右键刷新', key: `conn-${conn.id}-dbs` });
}
} else {
setConnectionStates(prev => ({ ...prev, [conn.id]: 'error' }));
setLoadedKeys(prev => prev.filter(k => k !== node.key));
message.error({ content: res.message, key: `conn-${conn.id}-dbs` });
}
} 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}-dbs` });
} finally {
loadingNodesRef.current.delete(loadKey);

View File

@@ -9,9 +9,9 @@ import (
)
var damengDatabaseQueries = []string{
// 优先使用达梦原生系统表
"SELECT DISTINCT OBJECT_NAME AS DATABASE_NAME FROM SYS.SYSOBJECTS WHERE TYPE$ = 'SCH' AND OBJECT_NAME NOT IN ('SYS','SYSDBA','SYSAUDITOR','SYSSSO','CTISYS','__RECYCLE_USER__') ORDER BY OBJECT_NAME",
"SELECT SCHEMA_NAME AS DATABASE_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN ('SYS','SYSDBA','SYSAUDITOR','SYSSSO','CTISYS','INFORMATION_SCHEMA') ORDER BY SCHEMA_NAME",
// 优先使用达梦原生系统表SYSDBA 保留:作为默认管理员 schema大多数用户在此创建业务表
"SELECT DISTINCT OBJECT_NAME AS DATABASE_NAME FROM SYS.SYSOBJECTS WHERE TYPE$ = 'SCH' AND OBJECT_NAME NOT IN ('SYS','SYSAUDITOR','SYSSSO','CTISYS','__RECYCLE_USER__') ORDER BY OBJECT_NAME",
"SELECT SCHEMA_NAME AS DATABASE_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN ('SYS','SYSAUDITOR','SYSSSO','CTISYS','INFORMATION_SCHEMA') ORDER BY SCHEMA_NAME",
// Oracle 兼容层
"SELECT SYS_CONTEXT('USERENV', 'CURRENT_SCHEMA') AS DATABASE_NAME FROM DUAL",
"SELECT SYS_CONTEXT('USERENV', 'CURRENT_USER') AS DATABASE_NAME FROM DUAL",
@@ -21,6 +21,8 @@ var damengDatabaseQueries = []string{
"SELECT USERNAME AS DATABASE_NAME FROM SYS.DBA_USERS ORDER BY USERNAME",
"SELECT DISTINCT OWNER AS DATABASE_NAME FROM ALL_OBJECTS ORDER BY OWNER",
"SELECT DISTINCT OWNER AS DATABASE_NAME FROM ALL_TABLES ORDER BY OWNER",
// 最终兜底:获取当前连接用户作为 schema 名称
"SELECT USER AS DATABASE_NAME FROM DUAL",
}
type damengQueryFunc func(query string) ([]map[string]interface{}, []string, error)

View File

@@ -71,3 +71,50 @@ func TestCollectDamengDatabaseNames_ReturnsErrorWhenNoNameResolved(t *testing.T)
t.Fatalf("错误不符合预期: %v", err)
}
}
// TestCollectDamengDatabaseNames_IncludesSYSDBA 验证 SYSDBA达梦默认管理员 schema
// 不会被系统 schema 过滤排除。
func TestCollectDamengDatabaseNames_IncludesSYSDBA(t *testing.T) {
t.Parallel()
got, err := collectDamengDatabaseNames(func(query string) ([]map[string]interface{}, []string, error) {
switch query {
case damengDatabaseQueries[0]:
// 查询 0 返回 SYSDBA之前会被排除修复后应该返回
return []map[string]interface{}{{"DATABASE_NAME": "SYSDBA"}}, nil, nil
default:
return nil, nil, errors.New("permission denied")
}
})
if err != nil {
t.Fatalf("collectDamengDatabaseNames 返回错误: %v", err)
}
want := []string{"SYSDBA"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("SYSDBA 应该包含在结果中, got=%v want=%v", got, want)
}
}
// TestCollectDamengDatabaseNames_FallbackToCurrentUser 验证当所有查询都失败时
// 兜底查询 SELECT USER FROM DUAL 能返回当前用户作为 schema。
func TestCollectDamengDatabaseNames_FallbackToCurrentUser(t *testing.T) {
t.Parallel()
lastQuery := damengDatabaseQueries[len(damengDatabaseQueries)-1]
got, err := collectDamengDatabaseNames(func(query string) ([]map[string]interface{}, []string, error) {
if query == lastQuery {
return []map[string]interface{}{{"DATABASE_NAME": "SYSDBA"}}, nil, nil
}
// 前面所有查询要么返回空要么报错
return []map[string]interface{}{}, nil, nil
})
if err != nil {
t.Fatalf("collectDamengDatabaseNames 返回错误: %v", err)
}
want := []string{"SYSDBA"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("兜底查询应该返回当前用户, got=%v want=%v", got, want)
}
}