diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index b8be944..ad6ce0c 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -f697e821b4acd5cf614d63d46453e8a4 \ No newline at end of file +20168ff7047e0ecea00acb73f413f7db \ No newline at end of file diff --git a/frontend/src/components/RedisMonitor.tsx b/frontend/src/components/RedisMonitor.tsx index 95000ea..0e5cc25 100644 --- a/frontend/src/components/RedisMonitor.tsx +++ b/frontend/src/components/RedisMonitor.tsx @@ -51,7 +51,7 @@ const RedisMonitor: React.FC = ({ connectionId, redisDB }) => // Ref to track if component is mounted to prevent state updates after unmount const mountedRef = useRef(true); // Interval ref - const intervalRef = useRef(null); + const intervalRef = useRef | null>(null); // Previous ops counter to calculate QPS if instantaneous_ops_per_sec is not enough const prevMetricsRef = useRef({ prevOps: 0, prevTime: 0 }); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 64a0232..58658f3 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -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); diff --git a/internal/db/dameng_metadata.go b/internal/db/dameng_metadata.go index b0f698b..15d12ad 100644 --- a/internal/db/dameng_metadata.go +++ b/internal/db/dameng_metadata.go @@ -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) diff --git a/internal/db/dameng_metadata_test.go b/internal/db/dameng_metadata_test.go index 5310679..b5b6557 100644 --- a/internal/db/dameng_metadata_test.go +++ b/internal/db/dameng_metadata_test.go @@ -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) + } +}