🐛 fix(sidebar): 隐藏达梦等数据源不支持的数据库管理入口

- 新增数据库级 DDL 能力判定,统一收敛新建库、重命名库、删库菜单显示
- 修正 Sidebar V1/V2 右键菜单,避免达梦和 Oracle-like 数据源暴露误导入口
- 补充能力与菜单回归测试,覆盖达梦、Oracle 和 OceanBase Oracle 协议

Refs #496
This commit is contained in:
Syngnat
2026-05-27 20:13:19 +08:00
parent e069ddf8fa
commit fac826b335
5 changed files with 122 additions and 11 deletions

View File

@@ -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(
<V2ConnectionContextMenuView
connectionName="dm-prod"
hostSummary="10.0.0.10:5236"
driverLabel="dameng"
supportsCreateDatabase={false}
/>,
);
const databaseMarkup = renderToStaticMarkup(
<V2DatabaseContextMenuView
dbName="SYSDBA"
dialect="dm"
supportsRenameDatabase={false}
supportsDropDatabase={false}
/>,
);
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(
<V2TableGroupContextMenuView

View File

@@ -55,6 +55,7 @@ import { getTableDataDangerActionMeta, supportsTableTruncateAction, type TableDa
import { useAutoFetchVisibility } from '../utils/autoFetchVisibility';
import FindInDatabaseModal from './FindInDatabaseModal';
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
import { getDataSourceCapabilities } from '../utils/dataSourceCapabilities';
import { noAutoCapInputProps } from '../utils/inputAutoCap';
import { normalizeSidebarViewName, resolveSidebarRuntimeDatabase } from '../utils/sidebarMetadata';
import { buildStarRocksMaterializedViewPreviewSql } from './tableDesignerSchemaSql';
@@ -5422,12 +5423,15 @@ const Sidebar: React.FC<{
const renderV2DatabaseContextMenu = (node: any) => {
const dialect = getMetadataDialect(node.dataRef as SavedConnection);
const capabilities = getDataSourceCapabilities((node.dataRef as SavedConnection)?.config);
return (
<V2DatabaseContextMenuView
dbName={String(node.dataRef?.dbName || node.title || '')}
dialect={dialect}
supportsSchemaActions={isPostgresSchemaDialect(dialect)}
supportsStarRocksActions={dialect === 'starrocks'}
supportsRenameDatabase={capabilities.supportsRenameDatabase}
supportsDropDatabase={capabilities.supportsDropDatabase}
onAction={(action) => {
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 (
<V2ConnectionContextMenuView
@@ -5445,6 +5450,7 @@ const Sidebar: React.FC<{
hostSummary={resolveConnectionHostSummary(conn?.config)}
driverLabel={resolveConnectionIconType(conn)}
isRedis={conn?.config?.type === 'redis'}
supportsCreateDatabase={capabilities.supportsCreateDatabase}
tags={connectionTags.map((tag) => ({
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: <DatabaseOutlined />,
@@ -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: <EditOutlined />,
onClick: () => handleV2DatabaseContextMenuAction(node, 'rename-db')
},
{
}] : []),
...(capabilities.supportsDropDatabase ? [{
key: 'danger-zone',
label: '危险操作',
icon: <WarningOutlined />,
@@ -6285,7 +6295,7 @@ const Sidebar: React.FC<{
onClick: () => handleV2DatabaseContextMenuAction(node, 'drop-db')
}
]
},
}] : []),
{
key: 'refresh',
label: '刷新',

View File

@@ -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<{
<div className="gn-v2-context-menu-section-title"></div>
{renderItems([
{ action: 'rename-db', icon: <EditOutlined />, title: '重命名数据库', kbd: 'F2' },
...(supportsRenameDatabase ? [{ action: 'rename-db', icon: <EditOutlined />, title: '重命名数据库', kbd: 'F2' }] : []),
{ action: 'refresh', icon: <ReloadOutlined />, title: '刷新对象树' },
{ action: 'disconnect-db', icon: <DisconnectOutlined />, title: '关闭数据库' },
])}
@@ -358,7 +362,7 @@ export const V2DatabaseContextMenuView: React.FC<{
])}
<div className="gn-v2-context-menu-divider" />
{renderItems([
{supportsDropDatabase && renderItems([
{ action: 'drop-db', icon: <DeleteOutlined />, title: '删除数据库 · DROP', tone: 'danger', kbd: '⌫' },
])}
</div>
@@ -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: <ConsoleSqlOutlined />, title: '新建命令窗口', featured: true },
{ action: 'open-monitor', icon: <DashboardOutlined />, title: 'Redis 实例监控' },
]) : renderItems([
{ action: 'new-db', icon: <DatabaseOutlined />, title: '新建数据库', kbd: '⌘N', featured: true },
...(supportsCreateDatabase ? [{ action: 'new-db' as const, icon: <DatabaseOutlined />, title: '新建数据库', kbd: '⌘N', featured: true }] : []),
{ action: 'refresh', icon: <ReloadOutlined />, title: '刷新连接', kbd: '⌘R' },
{ action: 'new-query', icon: <ConsoleSqlOutlined />, title: '新建查询' },
{ action: 'open-sql-file', icon: <FileAddOutlined />, title: '运行外部 SQL 文件' },

View File

@@ -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,
});
});
});

View File

@@ -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),