From fac826b335f745896acc8492d17d2416e009b219 Mon Sep 17 00:00:00 2001 From: Syngnat Date: Wed, 27 May 2026 20:13:19 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(sidebar):=20=E9=9A=90?= =?UTF-8?q?=E8=97=8F=E8=BE=BE=E6=A2=A6=E7=AD=89=E6=95=B0=E6=8D=AE=E6=BA=90?= =?UTF-8?q?=E4=B8=8D=E6=94=AF=E6=8C=81=E7=9A=84=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增数据库级 DDL 能力判定,统一收敛新建库、重命名库、删库菜单显示 - 修正 Sidebar V1/V2 右键菜单,避免达梦和 Oracle-like 数据源暴露误导入口 - 补充能力与菜单回归测试,覆盖达梦、Oracle 和 OceanBase Oracle 协议 Refs #496 --- .../Sidebar.locate-toolbar.test.tsx | 25 ++++++++++ frontend/src/components/Sidebar.tsx | 26 +++++++---- .../src/components/V2TableContextMenu.tsx | 12 +++-- .../src/utils/dataSourceCapabilities.test.ts | 24 ++++++++++ frontend/src/utils/dataSourceCapabilities.ts | 46 +++++++++++++++++++ 5 files changed, 122 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/Sidebar.locate-toolbar.test.tsx b/frontend/src/components/Sidebar.locate-toolbar.test.tsx index 94b0694..06b19dd 100644 --- a/frontend/src/components/Sidebar.locate-toolbar.test.tsx +++ b/frontend/src/components/Sidebar.locate-toolbar.test.tsx @@ -762,6 +762,31 @@ describe('Sidebar locate toolbar', () => { expect(markup).toContain('删除连接'); }); + it('omits unsupported database management actions for Oracle-like connection and database menus', () => { + const connectionMarkup = renderToStaticMarkup( + , + ); + const databaseMarkup = renderToStaticMarkup( + , + ); + + expect(connectionMarkup).not.toContain('新建数据库'); + expect(databaseMarkup).not.toContain('重命名数据库'); + expect(databaseMarkup).not.toContain('删除数据库 · DROP'); + expect(databaseMarkup).toContain('刷新对象树'); + expect(databaseMarkup).toContain('关闭数据库'); + }); + it('renders the v2 table group menu with sort state', () => { const markup = renderToStaticMarkup( { const dialect = getMetadataDialect(node.dataRef as SavedConnection); + const capabilities = getDataSourceCapabilities((node.dataRef as SavedConnection)?.config); return ( { setContextMenu(null); handleV2DatabaseContextMenuAction(node, action); @@ -5438,6 +5442,7 @@ const Sidebar: React.FC<{ const renderV2ConnectionContextMenu = (node: any) => { const conn = node.dataRef as SavedConnection; + const capabilities = getDataSourceCapabilities(conn?.config); const currentTagId = connectionTags.find((tag) => tag.connectionIds.includes(String(conn.id || node.key)))?.id || ''; return ( ({ id: tag.id, name: tag.name, @@ -6056,8 +6062,9 @@ const Sidebar: React.FC<{ }); // Regular database connection menu + const connectionCapabilities = getDataSourceCapabilities((node.dataRef as SavedConnection)?.config); return [ - { + ...(connectionCapabilities.supportsCreateDatabase ? [{ key: 'new-db', label: '新建数据库', icon: , @@ -6065,7 +6072,7 @@ const Sidebar: React.FC<{ setTargetConnection(node); setIsCreateDbModalOpen(true); } - }, + }] : []), { key: 'refresh', label: '刷新', @@ -6235,8 +6242,11 @@ const Sidebar: React.FC<{ } ]; } else if (node.type === 'database') { - const isStarRocks = getMetadataDialect(node.dataRef as SavedConnection) === 'starrocks'; - const supportsSchemaActions = isPostgresSchemaDialect(getMetadataDialect(node.dataRef as SavedConnection)); + const databaseConn = node.dataRef as SavedConnection; + const dialect = getMetadataDialect(databaseConn); + const capabilities = getDataSourceCapabilities(databaseConn?.config); + const isStarRocks = dialect === 'starrocks'; + const supportsSchemaActions = isPostgresSchemaDialect(dialect); return [ { key: 'new-table', @@ -6266,13 +6276,13 @@ const Sidebar: React.FC<{ onClick: () => openCreateStarRocksExternalCatalog(node) }, ] : []), - { + ...(capabilities.supportsRenameDatabase ? [{ key: 'rename-db', label: '重命名数据库', icon: , onClick: () => handleV2DatabaseContextMenuAction(node, 'rename-db') - }, - { + }] : []), + ...(capabilities.supportsDropDatabase ? [{ key: 'danger-zone', label: '危险操作', icon: , @@ -6285,7 +6295,7 @@ const Sidebar: React.FC<{ onClick: () => handleV2DatabaseContextMenuAction(node, 'drop-db') } ] - }, + }] : []), { key: 'refresh', label: '刷新', diff --git a/frontend/src/components/V2TableContextMenu.tsx b/frontend/src/components/V2TableContextMenu.tsx index 8fd19cc..5f8465d 100644 --- a/frontend/src/components/V2TableContextMenu.tsx +++ b/frontend/src/components/V2TableContextMenu.tsx @@ -304,12 +304,16 @@ export const V2DatabaseContextMenuView: React.FC<{ dialect?: string; supportsSchemaActions?: boolean; supportsStarRocksActions?: boolean; + supportsRenameDatabase?: boolean; + supportsDropDatabase?: boolean; onAction?: (action: V2DatabaseContextMenuActionKey) => void; }> = ({ dbName, dialect, supportsSchemaActions = false, supportsStarRocksActions = false, + supportsRenameDatabase = true, + supportsDropDatabase = true, onAction, }) => { const renderItems = (items: V2TableContextMenuItemConfig[]) => renderV2ContextMenuItems( @@ -346,7 +350,7 @@ export const V2DatabaseContextMenuView: React.FC<{
维护
{renderItems([ - { action: 'rename-db', icon: , title: '重命名数据库', kbd: 'F2' }, + ...(supportsRenameDatabase ? [{ action: 'rename-db', icon: , title: '重命名数据库', kbd: 'F2' }] : []), { action: 'refresh', icon: , title: '刷新对象树' }, { action: 'disconnect-db', icon: , title: '关闭数据库' }, ])} @@ -358,7 +362,7 @@ export const V2DatabaseContextMenuView: React.FC<{ ])}
- {renderItems([ + {supportsDropDatabase && renderItems([ { action: 'drop-db', icon: , title: '删除数据库 · DROP', tone: 'danger', kbd: '⌫' }, ])}
@@ -431,6 +435,7 @@ export const V2ConnectionContextMenuView: React.FC<{ hostSummary?: string; driverLabel?: string; isRedis?: boolean; + supportsCreateDatabase?: boolean; tags?: V2ConnectionContextMenuTagItem[]; onAction?: (action: V2ConnectionContextMenuActionKey) => void; }> = ({ @@ -438,6 +443,7 @@ export const V2ConnectionContextMenuView: React.FC<{ hostSummary, driverLabel, isRedis = false, + supportsCreateDatabase = true, tags = [], onAction, }) => { @@ -466,7 +472,7 @@ export const V2ConnectionContextMenuView: React.FC<{ { action: 'new-command', icon: , title: '新建命令窗口', featured: true }, { action: 'open-monitor', icon: , title: 'Redis 实例监控' }, ]) : renderItems([ - { action: 'new-db', icon: , title: '新建数据库', kbd: '⌘N', featured: true }, + ...(supportsCreateDatabase ? [{ action: 'new-db' as const, icon: , title: '新建数据库', kbd: '⌘N', featured: true }] : []), { action: 'refresh', icon: , title: '刷新连接', kbd: '⌘R' }, { action: 'new-query', icon: , title: '新建查询' }, { action: 'open-sql-file', icon: , title: '运行外部 SQL 文件' }, diff --git a/frontend/src/utils/dataSourceCapabilities.test.ts b/frontend/src/utils/dataSourceCapabilities.test.ts index adea747..0dcee63 100644 --- a/frontend/src/utils/dataSourceCapabilities.test.ts +++ b/frontend/src/utils/dataSourceCapabilities.test.ts @@ -76,4 +76,28 @@ describe('dataSourceCapabilities', () => { supportsApproximateTableCount: true, }); }); + + it('hides database-level DDL actions for Dameng and Oracle-like datasources', () => { + expect(getDataSourceCapabilities({ type: 'dameng' })).toMatchObject({ + type: 'dameng', + supportsCreateDatabase: false, + supportsRenameDatabase: false, + supportsDropDatabase: false, + }); + expect(getDataSourceCapabilities({ type: 'oracle' })).toMatchObject({ + type: 'oracle', + supportsCreateDatabase: false, + supportsRenameDatabase: false, + supportsDropDatabase: false, + }); + expect(getDataSourceCapabilities({ + type: 'oceanbase', + oceanBaseProtocol: 'oracle', + })).toMatchObject({ + type: 'oracle', + supportsCreateDatabase: false, + supportsRenameDatabase: false, + supportsDropDatabase: false, + }); + }); }); diff --git a/frontend/src/utils/dataSourceCapabilities.ts b/frontend/src/utils/dataSourceCapabilities.ts index 5de93ba..a82a543 100644 --- a/frontend/src/utils/dataSourceCapabilities.ts +++ b/frontend/src/utils/dataSourceCapabilities.ts @@ -99,12 +99,55 @@ export type DataSourceCapabilities = { supportsQueryEditor: boolean; supportsSqlQueryExport: boolean; supportsCopyInsert: boolean; + supportsCreateDatabase: boolean; + supportsRenameDatabase: boolean; + supportsDropDatabase: boolean; forceReadOnlyQueryResult: boolean; preferManualTotalCount: boolean; supportsApproximateTableCount: boolean; supportsApproximateTotalPages: boolean; }; +const CREATE_DATABASE_TYPES = new Set([ + 'mysql', + 'mariadb', + 'oceanbase', + 'diros', + 'starrocks', + 'postgres', + 'kingbase', + 'highgo', + 'vastbase', + 'opengauss', + 'sqlserver', + 'tdengine', + 'clickhouse', +]); + +const RENAME_DATABASE_TYPES = new Set([ + 'diros', + 'postgres', + 'kingbase', + 'highgo', + 'vastbase', + 'opengauss', +]); + +const DROP_DATABASE_TYPES = new Set([ + 'mysql', + 'mariadb', + 'oceanbase', + 'diros', + 'starrocks', + 'postgres', + 'kingbase', + 'highgo', + 'vastbase', + 'opengauss', + 'tdengine', + 'clickhouse', +]); + export const getDataSourceCapabilities = (config: ConnectionLike): DataSourceCapabilities => { const type = resolveDataSourceType(config); return { @@ -112,6 +155,9 @@ export const getDataSourceCapabilities = (config: ConnectionLike): DataSourceCap supportsQueryEditor: !QUERY_EDITOR_DISABLED_TYPES.has(type), supportsSqlQueryExport: SQL_QUERY_EXPORT_TYPES.has(type), supportsCopyInsert: COPY_INSERT_TYPES.has(type), + supportsCreateDatabase: CREATE_DATABASE_TYPES.has(type), + supportsRenameDatabase: RENAME_DATABASE_TYPES.has(type), + supportsDropDatabase: DROP_DATABASE_TYPES.has(type), forceReadOnlyQueryResult: FORCE_READ_ONLY_QUERY_TYPES.has(type), preferManualTotalCount: MANUAL_TOTAL_COUNT_TYPES.has(type), supportsApproximateTableCount: APPROXIMATE_TABLE_COUNT_TYPES.has(type),