mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-30 18:01:23 +08:00
♻️ refactor(sidebar): 抽出 V2 右键菜单逻辑
This commit is contained in:
@@ -71,6 +71,7 @@ const readSidebarSource = () => [
|
||||
readSourceFile('./sidebar/useSidebarTreeLoaders.tsx'),
|
||||
readSourceFile('./sidebar/SidebarEntityModals.tsx'),
|
||||
readSourceFile('./sidebar/SidebarTreeTitle.tsx'),
|
||||
readSourceFile('./sidebar/useSidebarV2ContextMenu.tsx'),
|
||||
readSourceFile('./sidebarV2Utils.ts'),
|
||||
].join('\n');
|
||||
const readLegacyNodeMenuSource = () => readSourceFile('./sidebar/sidebarLegacyNodeMenu.tsx');
|
||||
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
import { SidebarBatchExportModals } from './sidebar/SidebarBatchExportModals';
|
||||
import { SidebarEntityModals } from './sidebar/SidebarEntityModals';
|
||||
import { renderSidebarV2TreeTitle } from './sidebar/SidebarTreeTitle';
|
||||
import {
|
||||
useSidebarV2ContextMenu,
|
||||
type SidebarContextMenuState,
|
||||
} from './sidebar/useSidebarV2ContextMenu';
|
||||
import {
|
||||
normalizeDriverType,
|
||||
useSidebarTreeLoaders,
|
||||
@@ -239,19 +243,6 @@ export {
|
||||
export type { V2CommandSearchItem, V2RailConnectionGroup } from './sidebarV2Utils';
|
||||
|
||||
const { Search } = Input;
|
||||
type SidebarContextMenuState = {
|
||||
x: number;
|
||||
y: number;
|
||||
sourceX?: number;
|
||||
sourceY?: number;
|
||||
items: MenuProps['items'];
|
||||
kind?: 'v2-table' | 'v2-database' | 'v2-schema' | 'v2-table-group' | 'v2-connection' | 'v2-connection-group';
|
||||
node?: any;
|
||||
rootClassName?: string;
|
||||
overlayStyle?: React.CSSProperties;
|
||||
maxHeight?: number;
|
||||
};
|
||||
|
||||
const SIDEBAR_LOCATE_LOAD_WAIT_INTERVAL_MS = 50;
|
||||
const SIDEBAR_LOCATE_LOAD_WAIT_ATTEMPTS = 160;
|
||||
|
||||
@@ -593,9 +584,6 @@ const Sidebar: React.FC<{
|
||||
activeContext: null,
|
||||
});
|
||||
const connectionReloadSignaturesRef = useRef<Record<string, string>>({});
|
||||
const [contextMenu, setContextMenu] = useState<SidebarContextMenuState | null>(null);
|
||||
const contextMenuPortalRef = useRef<HTMLDivElement | null>(null);
|
||||
const [v2TableContextMenuStats, setV2TableContextMenuStats] = useState<Record<string, V2TableContextMenuStats>>({});
|
||||
const connectionIds = useMemo(() => connections.map((conn) => conn.id), [connections]);
|
||||
const connectionIdSet = useMemo(() => new Set(connectionIds), [connectionIds]);
|
||||
const unmatchedSavedQueries = useMemo(
|
||||
@@ -3744,411 +3732,42 @@ const Sidebar: React.FC<{
|
||||
justifyContent: 'center',
|
||||
};
|
||||
|
||||
const connectionStatusMap = useMemo(() => {
|
||||
const statusMap = new Map<string, 'live' | 'error' | 'idle'>();
|
||||
const sortedConnectionIds = connections
|
||||
.map((conn) => conn.id)
|
||||
.sort((a, b) => b.length - a.length);
|
||||
connections.forEach((conn) => {
|
||||
statusMap.set(conn.id, 'idle');
|
||||
});
|
||||
Object.entries(connectionStates).forEach(([key, value]) => {
|
||||
const ownState = statusMap.get(key);
|
||||
if (ownState !== undefined) {
|
||||
statusMap.set(key, value === 'success' ? 'live' : 'error');
|
||||
return;
|
||||
}
|
||||
if (value !== 'success') return;
|
||||
const ownerId = sortedConnectionIds.find((id) => key.startsWith(`${id}-`));
|
||||
if (ownerId && statusMap.get(ownerId) === 'idle') {
|
||||
statusMap.set(ownerId, 'live');
|
||||
}
|
||||
});
|
||||
return statusMap;
|
||||
}, [connectionStates, connections]);
|
||||
|
||||
const buildRailConnectionStatus = useCallback((connectionId: string): 'live' | 'error' | 'idle' => {
|
||||
return connectionStatusMap.get(connectionId) || 'idle';
|
||||
}, [connectionStatusMap]);
|
||||
|
||||
const openV2ConnectionContextMenu = (
|
||||
event: React.MouseEvent,
|
||||
connOrNode: SavedConnection | TreeNode,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const node = (connOrNode as TreeNode).type === 'connection'
|
||||
? connOrNode as TreeNode
|
||||
: getConnectionNodeForAction(connOrNode as SavedConnection);
|
||||
if (!node?.key || !node?.dataRef) return;
|
||||
const position = resolveSidebarContextMenuPosition(event.clientX, event.clientY);
|
||||
setContextMenu({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
sourceX: event.clientX,
|
||||
sourceY: event.clientY,
|
||||
items: [],
|
||||
kind: 'v2-connection',
|
||||
node,
|
||||
rootClassName: 'gn-v2-table-context-menu-popup',
|
||||
overlayStyle: { width: 264, maxWidth: 'calc(100vw - 24px)' },
|
||||
maxHeight: position.maxHeight,
|
||||
});
|
||||
};
|
||||
|
||||
const getV2TreeMetaText = (node: any): string => {
|
||||
if (node.type === 'tag') {
|
||||
const count = flattenConnectionNodes(node.children || []).length;
|
||||
return count > 0 ? count.toLocaleString() : '';
|
||||
}
|
||||
if (node.type === 'database') {
|
||||
const count = v2TreeMetrics.databaseTableCounts.get(node.key) || 0;
|
||||
return count > 0 ? count.toLocaleString() : '';
|
||||
}
|
||||
if (node.type === 'object-group') {
|
||||
const count = v2TreeMetrics.objectGroupCounts.get(node.key) || 0;
|
||||
return count > 0 ? count.toLocaleString() : '';
|
||||
}
|
||||
if (node.type === 'redis-db') {
|
||||
const match = String(node.title || '').match(/\((\d+)\)/);
|
||||
return match?.[1] || '';
|
||||
}
|
||||
if (node.type === 'table') {
|
||||
const rowCount = Number(node?.dataRef?.rowCount);
|
||||
return Number.isFinite(rowCount) && rowCount >= 0 ? formatSidebarRowCount(rowCount) : '';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const getV2TableContextMenuStatsKey = (node: any): string => {
|
||||
const id = String(node?.dataRef?.id || '');
|
||||
const dbName = String(node?.dataRef?.dbName || '');
|
||||
const tableName = String(node?.dataRef?.tableName || node?.title || '');
|
||||
return `${id}::${dbName}::${tableName}`;
|
||||
};
|
||||
|
||||
const readNumericMetadataValue = (row: Record<string, any>, keys: string[]): number | undefined => {
|
||||
const value = getCaseInsensitiveRawValue(row, keys);
|
||||
if (value === undefined || value === null || value === '') return undefined;
|
||||
const normalized = Number(String(value).replace(/,/g, ''));
|
||||
return Number.isFinite(normalized) ? normalized : undefined;
|
||||
};
|
||||
|
||||
const buildV2TableStatusSQL = (node: any): string => {
|
||||
const conn = node.dataRef as SavedConnection & { dbName?: string; tableName?: string; schemaName?: string };
|
||||
const dialect = getMetadataDialect(conn);
|
||||
const dbName = String(conn?.dbName || '').trim();
|
||||
const tableName = String(conn?.tableName || node?.title || '').trim();
|
||||
const objectName = extractObjectName(tableName);
|
||||
const schemaName = String(conn?.schemaName || splitQualifiedName(tableName).schemaName || '').trim();
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
return [
|
||||
'SELECT TABLE_ROWS AS table_rows, DATA_LENGTH AS data_length, INDEX_LENGTH AS index_length, ENGINE AS engine',
|
||||
'FROM information_schema.tables',
|
||||
`WHERE table_schema = '${escapeSQLLiteral(dbName)}'`,
|
||||
`AND table_name = '${escapeSQLLiteral(objectName)}'`,
|
||||
'LIMIT 1',
|
||||
].join('\n');
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'vastbase':
|
||||
case 'highgo':
|
||||
case 'opengauss':
|
||||
case 'gaussdb': {
|
||||
const schema = schemaName || 'public';
|
||||
return [
|
||||
"SELECT c.reltuples::bigint AS table_rows, pg_total_relation_size(c.oid) AS data_length, pg_indexes_size(c.oid) AS index_length, 'heap' AS engine",
|
||||
'FROM pg_class c',
|
||||
'JOIN pg_namespace n ON n.oid = c.relnamespace',
|
||||
"WHERE c.relkind = 'r'",
|
||||
`AND n.nspname = '${escapeSQLLiteral(schema)}'`,
|
||||
`AND c.relname = '${escapeSQLLiteral(objectName)}'`,
|
||||
'LIMIT 1',
|
||||
].join('\n');
|
||||
}
|
||||
case 'sqlserver': {
|
||||
const safeTable = tableName.replace(/'/g, "''");
|
||||
return [
|
||||
'SELECT SUM(p.rows) AS table_rows, SUM(a.total_pages) * 8 * 1024 AS data_length, SUM(a.used_pages) * 8 * 1024 AS index_length, NULL AS engine',
|
||||
'FROM sys.tables t',
|
||||
'JOIN sys.indexes i ON t.object_id = i.object_id',
|
||||
'JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id',
|
||||
'JOIN sys.allocation_units a ON p.partition_id = a.container_id',
|
||||
`WHERE t.object_id = OBJECT_ID('${safeTable}')`,
|
||||
].join('\n');
|
||||
}
|
||||
case 'clickhouse':
|
||||
return [
|
||||
'SELECT total_rows AS table_rows, total_bytes AS data_length, 0 AS index_length, engine AS engine',
|
||||
'FROM system.tables',
|
||||
`WHERE database = '${escapeSQLLiteral(dbName)}'`,
|
||||
`AND name = '${escapeSQLLiteral(objectName)}'`,
|
||||
'LIMIT 1',
|
||||
].join('\n');
|
||||
case 'oracle':
|
||||
case 'dm': {
|
||||
const owner = (schemaName || dbName || '').toUpperCase();
|
||||
return [
|
||||
'SELECT num_rows AS table_rows, 0 AS data_length, 0 AS index_length, NULL AS engine',
|
||||
'FROM all_tables',
|
||||
`WHERE owner = '${escapeSQLLiteral(owner)}'`,
|
||||
`AND table_name = '${escapeSQLLiteral(objectName.toUpperCase())}'`,
|
||||
'FETCH FIRST 1 ROWS ONLY',
|
||||
].join('\n');
|
||||
}
|
||||
case 'sqlite':
|
||||
case 'duckdb':
|
||||
return `SELECT COUNT(*) AS table_rows, 0 AS data_length, 0 AS index_length, NULL AS engine FROM ${tableName}`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const renderV2TableContextMenu = (node: any) => {
|
||||
const tableName = String(node?.dataRef?.tableName || node?.title || '').trim();
|
||||
const statsKey = getV2TableContextMenuStatsKey(node);
|
||||
const stats = v2TableContextMenuStats[statsKey];
|
||||
const isStarRocks = getMetadataDialect(node.dataRef as SavedConnection) === 'starrocks';
|
||||
const supportsMessagePublish = Boolean(resolveMessagePublishTarget(node));
|
||||
const isPinned = isSidebarTablePinned(
|
||||
pinnedSidebarTables,
|
||||
String(node?.dataRef?.id || ''),
|
||||
String(node?.dataRef?.dbName || ''),
|
||||
tableName,
|
||||
String(node?.dataRef?.schemaName || ''),
|
||||
);
|
||||
return (
|
||||
<V2TableContextMenuView
|
||||
tableName={tableName}
|
||||
shortcutPlatform={activeShortcutPlatform}
|
||||
stats={stats}
|
||||
isPinned={isPinned}
|
||||
supportsTruncate={supportsTableTruncateAction(node.dataRef?.config?.type, node.dataRef?.config?.driver)}
|
||||
supportsStarRocksRollup={isStarRocks}
|
||||
supportsMessagePublish={supportsMessagePublish}
|
||||
onAction={(action) => {
|
||||
setContextMenu(null);
|
||||
handleV2TableContextMenuAction(node, action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderV2TableGroupContextMenu = (node: any) => {
|
||||
const groupData = node.dataRef || {};
|
||||
const sortPreferenceKey = `${groupData.id}-${groupData.dbName}`;
|
||||
const currentSort = tableSortPreference[sortPreferenceKey] || 'name';
|
||||
return (
|
||||
<V2TableGroupContextMenuView
|
||||
shortcutPlatform={activeShortcutPlatform}
|
||||
dbName={String(groupData.dbName || '')}
|
||||
count={Array.isArray(node.children) ? node.children.length : 0}
|
||||
currentSort={currentSort}
|
||||
onAction={(action) => {
|
||||
setContextMenu(null);
|
||||
handleV2TableGroupContextMenuAction(node, action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 || '')}
|
||||
shortcutPlatform={activeShortcutPlatform}
|
||||
dialect={dialect}
|
||||
supportsSchemaActions={isPostgresSchemaDialect(dialect)}
|
||||
supportsStarRocksActions={dialect === 'starrocks'}
|
||||
supportsRenameDatabase={capabilities.supportsRenameDatabase}
|
||||
supportsDropDatabase={capabilities.supportsDropDatabase}
|
||||
onAction={(action) => {
|
||||
setContextMenu(null);
|
||||
handleV2DatabaseContextMenuAction(node, action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const handleV2SchemaContextMenuAction = (node: any, action: V2SchemaContextMenuActionKey) => {
|
||||
switch (action) {
|
||||
case 'rename-schema':
|
||||
openRenameSchemaModal(node);
|
||||
return;
|
||||
case 'refresh-schema':
|
||||
void loadTables(getDatabaseNodeRef(node?.dataRef, String(node?.dataRef?.dbName || '').trim()));
|
||||
return;
|
||||
case 'export-schema':
|
||||
void handleExportSchemaSQL(node, false);
|
||||
return;
|
||||
case 'backup-schema-sql':
|
||||
void handleExportSchemaSQL(node, true);
|
||||
return;
|
||||
case 'drop-schema':
|
||||
handleDeleteSchema(node);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const renderV2SchemaContextMenu = (node: any) => (
|
||||
<V2SchemaContextMenuView
|
||||
dbName={String(node?.dataRef?.dbName || '')}
|
||||
schemaName={String(node?.dataRef?.schemaName || node?.title || '')}
|
||||
shortcutPlatform={activeShortcutPlatform}
|
||||
onAction={(action) => {
|
||||
setContextMenu(null);
|
||||
handleV2SchemaContextMenuAction(node, action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
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
|
||||
connectionName={String(conn?.name || node.title || t('connection.unnamed'))}
|
||||
shortcutPlatform={activeShortcutPlatform}
|
||||
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,
|
||||
selected: tag.id === currentTagId,
|
||||
}))}
|
||||
onAction={(action) => {
|
||||
setContextMenu(null);
|
||||
handleV2ConnectionContextMenuAction(node, action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderV2ConnectionGroupContextMenu = (group: V2RailConnectionGroup) => (
|
||||
<V2ConnectionGroupContextMenuView
|
||||
groupName={group.name}
|
||||
count={group.connections.length}
|
||||
onAction={(action) => {
|
||||
setContextMenu(null);
|
||||
handleV2ConnectionGroupContextMenuAction(group, action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderV2SidebarContextMenuContent = (menu: SidebarContextMenuState) => {
|
||||
if (!menu.node) return null;
|
||||
if (menu.kind === 'v2-table') return renderV2TableContextMenu(menu.node);
|
||||
if (menu.kind === 'v2-database') return renderV2DatabaseContextMenu(menu.node);
|
||||
if (menu.kind === 'v2-schema') return renderV2SchemaContextMenu(menu.node);
|
||||
if (menu.kind === 'v2-table-group') return renderV2TableGroupContextMenu(menu.node);
|
||||
if (menu.kind === 'v2-connection') return renderV2ConnectionContextMenu(menu.node);
|
||||
if (menu.kind === 'v2-connection-group') return renderV2ConnectionGroupContextMenu(menu.node);
|
||||
return null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!contextMenu?.kind) return;
|
||||
const onPointerDown = (event: MouseEvent) => {
|
||||
const target = event.target instanceof Node ? event.target : null;
|
||||
if (target && contextMenuPortalRef.current?.contains(target)) return;
|
||||
setContextMenu(null);
|
||||
};
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') setContextMenu(null);
|
||||
};
|
||||
document.addEventListener('mousedown', onPointerDown);
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onPointerDown);
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [contextMenu?.kind]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!contextMenu?.kind) return;
|
||||
const frame = requestAnimationFrame(() => {
|
||||
const portal = contextMenuPortalRef.current;
|
||||
if (!portal) return;
|
||||
const rect = portal.getBoundingClientRect();
|
||||
const content = portal.querySelector('.gn-v2-table-context-menu') as HTMLElement | null;
|
||||
const measuredHeight = Math.max(rect.height, content?.scrollHeight || 0);
|
||||
const position = resolveSidebarContextMenuPosition(contextMenu.sourceX ?? contextMenu.x, contextMenu.sourceY ?? contextMenu.y, {
|
||||
width: rect.width || SIDEBAR_CONTEXT_MENU_FALLBACK_WIDTH,
|
||||
height: measuredHeight || SIDEBAR_CONTEXT_MENU_FALLBACK_HEIGHT,
|
||||
});
|
||||
setContextMenu(prev => {
|
||||
if (!prev?.kind) return prev;
|
||||
if (prev.x === position.x && prev.y === position.y && prev.maxHeight === position.maxHeight) return prev;
|
||||
return { ...prev, x: position.x, y: position.y, maxHeight: position.maxHeight };
|
||||
});
|
||||
});
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [contextMenu?.kind, contextMenu?.x, contextMenu?.y]);
|
||||
|
||||
const fetchV2TableContextMenuStats = async (node: any) => {
|
||||
const statsKey = getV2TableContextMenuStatsKey(node);
|
||||
if (!statsKey || v2TableContextMenuStats[statsKey]?.loading) return;
|
||||
const sql = buildV2TableStatusSQL(node);
|
||||
if (!sql) {
|
||||
setV2TableContextMenuStats(prev => ({ ...prev, [statsKey]: { unavailable: true } }));
|
||||
return;
|
||||
}
|
||||
|
||||
setV2TableContextMenuStats(prev => ({ ...prev, [statsKey]: { ...prev[statsKey], loading: true } }));
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const conn = node.dataRef;
|
||||
const res = await DBQuery(buildRuntimeConfig(conn, conn.dbName) as any, conn.dbName || '', sql);
|
||||
if (!res.success || !Array.isArray(res.data) || res.data.length === 0) {
|
||||
setV2TableContextMenuStats(prev => ({ ...prev, [statsKey]: { unavailable: true } }));
|
||||
return;
|
||||
}
|
||||
const row = res.data[0] as Record<string, any>;
|
||||
setV2TableContextMenuStats(prev => ({
|
||||
...prev,
|
||||
[statsKey]: {
|
||||
rowCount: readNumericMetadataValue(row, ['table_rows', 'TABLE_ROWS', 'rows', 'num_rows', 'reltuples', 'total_rows']),
|
||||
dataLength: readNumericMetadataValue(row, ['data_length', 'DATA_LENGTH', 'total_bytes']),
|
||||
indexLength: readNumericMetadataValue(row, ['index_length', 'INDEX_LENGTH']),
|
||||
engine: getCaseInsensitiveValue(row, ['engine', 'ENGINE']),
|
||||
},
|
||||
}));
|
||||
addSqlLog({
|
||||
id: `${Date.now()}-table-stats`,
|
||||
timestamp: Date.now(),
|
||||
sql,
|
||||
status: 'success',
|
||||
duration: Date.now() - startTime,
|
||||
dbName: conn.dbName,
|
||||
});
|
||||
} catch (error: any) {
|
||||
setV2TableContextMenuStats(prev => ({ ...prev, [statsKey]: { unavailable: true } }));
|
||||
addSqlLog({
|
||||
id: `${Date.now()}-table-stats-error`,
|
||||
timestamp: Date.now(),
|
||||
sql,
|
||||
status: 'error',
|
||||
duration: Date.now() - startTime,
|
||||
message: error?.message || String(error),
|
||||
dbName: node?.dataRef?.dbName,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const refreshV2TableContextMenuStats = (node: any) => {
|
||||
const statsKey = getV2TableContextMenuStatsKey(node);
|
||||
setV2TableContextMenuStats(prev => ({ ...prev, [statsKey]: { loading: true } }));
|
||||
void fetchV2TableContextMenuStats(node);
|
||||
};
|
||||
const {
|
||||
contextMenu,
|
||||
setContextMenu,
|
||||
contextMenuPortalRef,
|
||||
buildRailConnectionStatus,
|
||||
openV2ConnectionContextMenu,
|
||||
getV2TreeMetaText,
|
||||
renderV2SidebarContextMenuContent,
|
||||
fetchV2TableContextMenuStats,
|
||||
refreshV2TableContextMenuStats,
|
||||
} = useSidebarV2ContextMenu({
|
||||
connections,
|
||||
connectionStates,
|
||||
connectionTags,
|
||||
activeShortcutPlatform,
|
||||
flattenConnectionNodes,
|
||||
v2TreeMetrics,
|
||||
tableSortPreference,
|
||||
pinnedSidebarTables,
|
||||
getConnectionNodeForAction,
|
||||
buildRuntimeConfig,
|
||||
extractObjectName,
|
||||
isPostgresSchemaDialect,
|
||||
loadTables,
|
||||
getDatabaseNodeRef,
|
||||
handleExportSchemaSQL,
|
||||
handleDeleteSchema,
|
||||
openRenameSchemaModal,
|
||||
resolveMessagePublishTarget,
|
||||
addSqlLog,
|
||||
handleV2TableContextMenuAction,
|
||||
handleV2TableGroupContextMenuAction,
|
||||
handleV2DatabaseContextMenuAction,
|
||||
handleV2ConnectionContextMenuAction,
|
||||
handleV2ConnectionGroupContextMenuAction,
|
||||
});
|
||||
|
||||
const renderV2TreeTitle = (node: any, hoverTitle: string, statusBadge: React.ReactNode) => renderSidebarV2TreeTitle({
|
||||
node,
|
||||
|
||||
524
frontend/src/components/sidebar/useSidebarV2ContextMenu.tsx
Normal file
524
frontend/src/components/sidebar/useSidebarV2ContextMenu.tsx
Normal file
@@ -0,0 +1,524 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
V2DatabaseContextMenuView,
|
||||
V2ConnectionGroupContextMenuView,
|
||||
V2ConnectionContextMenuView,
|
||||
V2SchemaContextMenuView,
|
||||
V2TableContextMenuView,
|
||||
V2TableGroupContextMenuView,
|
||||
type V2DatabaseContextMenuActionKey,
|
||||
type V2ConnectionGroupContextMenuActionKey,
|
||||
type V2ConnectionContextMenuActionKey,
|
||||
type V2SchemaContextMenuActionKey,
|
||||
type V2TableContextMenuActionKey,
|
||||
type V2TableContextMenuStats,
|
||||
type V2TableGroupContextMenuActionKey,
|
||||
} from '../V2TableContextMenu';
|
||||
import type { SavedConnection } from '../../types';
|
||||
import { t } from '../../i18n';
|
||||
import { DBQuery } from '../../../wailsjs/go/app/App';
|
||||
import { getCaseInsensitiveRawValue, getCaseInsensitiveValue, getMetadataDialect, splitQualifiedName, buildSidebarTableStatusSQL, escapeSQLLiteral } from './sidebarMetadataLoaders';
|
||||
import { getDataSourceCapabilities } from '../../utils/dataSourceCapabilities';
|
||||
import { resolveConnectionHostSummary } from '../../utils/tabDisplay';
|
||||
import { resolveConnectionIconType } from '../../utils/connectionVisual';
|
||||
import { formatSidebarRowCount } from './sidebarHelpers';
|
||||
import { isSidebarTablePinned, type SidebarTreeNode as TreeNode, type V2RailConnectionGroup } from '../sidebarV2Utils';
|
||||
import { getTableDataDangerActionMeta, supportsTableTruncateAction } from '../tableDataDangerActions';
|
||||
import {
|
||||
SIDEBAR_CONTEXT_MENU_FALLBACK_HEIGHT,
|
||||
SIDEBAR_CONTEXT_MENU_FALLBACK_WIDTH,
|
||||
resolveSidebarContextMenuPosition,
|
||||
} from '../sidebarCoreUtils';
|
||||
|
||||
export type SidebarContextMenuState = {
|
||||
x: number;
|
||||
y: number;
|
||||
sourceX?: number;
|
||||
sourceY?: number;
|
||||
items: any;
|
||||
kind?: 'v2-table' | 'v2-database' | 'v2-schema' | 'v2-table-group' | 'v2-connection' | 'v2-connection-group';
|
||||
node?: any;
|
||||
rootClassName?: string;
|
||||
overlayStyle?: React.CSSProperties;
|
||||
maxHeight?: number;
|
||||
};
|
||||
|
||||
type SidebarV2ContextMenuOptions = {
|
||||
connections: SavedConnection[];
|
||||
connectionStates: Record<string, 'success' | 'error'>;
|
||||
connectionTags: Array<{ id: string; name: string; connectionIds: string[] }>;
|
||||
activeShortcutPlatform: any;
|
||||
flattenConnectionNodes: (nodes: TreeNode[]) => TreeNode[];
|
||||
v2TreeMetrics: {
|
||||
databaseTableCounts: Map<React.Key, number>;
|
||||
objectGroupCounts: Map<React.Key, number>;
|
||||
};
|
||||
tableSortPreference: Record<string, any>;
|
||||
pinnedSidebarTables: any[];
|
||||
getConnectionNodeForAction: (conn: SavedConnection) => TreeNode;
|
||||
buildRuntimeConfig: (conn: any, overrideDatabase?: string, clearDatabase?: boolean) => any;
|
||||
extractObjectName: (fullName: string) => string;
|
||||
isPostgresSchemaDialect: (dialect: string) => boolean;
|
||||
loadTables: (node: any) => Promise<void>;
|
||||
getDatabaseNodeRef: (connRef: any, dbName: string) => any;
|
||||
handleExportSchemaSQL: (node: any, includeData: boolean) => Promise<void>;
|
||||
handleDeleteSchema: (node: any) => void;
|
||||
openRenameSchemaModal: (node: any) => void;
|
||||
resolveMessagePublishTarget: (node: any) => unknown;
|
||||
addSqlLog: (log: any) => void;
|
||||
handleV2TableContextMenuAction: (node: any, action: V2TableContextMenuActionKey) => void;
|
||||
handleV2TableGroupContextMenuAction: (node: any, action: V2TableGroupContextMenuActionKey) => void;
|
||||
handleV2DatabaseContextMenuAction: (node: any, action: V2DatabaseContextMenuActionKey) => void;
|
||||
handleV2ConnectionContextMenuAction: (node: any, action: V2ConnectionContextMenuActionKey) => void;
|
||||
handleV2ConnectionGroupContextMenuAction: (group: V2RailConnectionGroup, action: V2ConnectionGroupContextMenuActionKey) => void;
|
||||
};
|
||||
|
||||
export const useSidebarV2ContextMenu = ({
|
||||
connections,
|
||||
connectionStates,
|
||||
connectionTags,
|
||||
activeShortcutPlatform,
|
||||
flattenConnectionNodes,
|
||||
v2TreeMetrics,
|
||||
tableSortPreference,
|
||||
pinnedSidebarTables,
|
||||
getConnectionNodeForAction,
|
||||
buildRuntimeConfig,
|
||||
extractObjectName,
|
||||
isPostgresSchemaDialect,
|
||||
loadTables,
|
||||
getDatabaseNodeRef,
|
||||
handleExportSchemaSQL,
|
||||
handleDeleteSchema,
|
||||
openRenameSchemaModal,
|
||||
resolveMessagePublishTarget,
|
||||
addSqlLog,
|
||||
handleV2TableContextMenuAction,
|
||||
handleV2TableGroupContextMenuAction,
|
||||
handleV2DatabaseContextMenuAction,
|
||||
handleV2ConnectionContextMenuAction,
|
||||
handleV2ConnectionGroupContextMenuAction,
|
||||
}: SidebarV2ContextMenuOptions) => {
|
||||
const [contextMenu, setContextMenu] = useState<SidebarContextMenuState | null>(null);
|
||||
const contextMenuPortalRef = useRef<HTMLDivElement | null>(null);
|
||||
const [v2TableContextMenuStats, setV2TableContextMenuStats] = useState<Record<string, V2TableContextMenuStats>>({});
|
||||
|
||||
const connectionStatusMap = useMemo(() => {
|
||||
const statusMap = new Map<string, 'live' | 'error' | 'idle'>();
|
||||
const sortedConnectionIds = connections
|
||||
.map((conn) => conn.id)
|
||||
.sort((a, b) => b.length - a.length);
|
||||
connections.forEach((conn) => {
|
||||
statusMap.set(conn.id, 'idle');
|
||||
});
|
||||
Object.entries(connectionStates).forEach(([key, value]) => {
|
||||
const ownState = statusMap.get(key);
|
||||
if (ownState !== undefined) {
|
||||
statusMap.set(key, value === 'success' ? 'live' : 'error');
|
||||
return;
|
||||
}
|
||||
if (value !== 'success') return;
|
||||
const ownerId = sortedConnectionIds.find((id) => key.startsWith(`${id}-`));
|
||||
if (ownerId && statusMap.get(ownerId) === 'idle') {
|
||||
statusMap.set(ownerId, 'live');
|
||||
}
|
||||
});
|
||||
return statusMap;
|
||||
}, [connectionStates, connections]);
|
||||
|
||||
const buildRailConnectionStatus = useCallback((connectionId: string): 'live' | 'error' | 'idle' => {
|
||||
return connectionStatusMap.get(connectionId) || 'idle';
|
||||
}, [connectionStatusMap]);
|
||||
|
||||
const openV2ConnectionContextMenu = (
|
||||
event: React.MouseEvent,
|
||||
connOrNode: SavedConnection | TreeNode,
|
||||
) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const node = (connOrNode as TreeNode).type === 'connection'
|
||||
? connOrNode as TreeNode
|
||||
: getConnectionNodeForAction(connOrNode as SavedConnection);
|
||||
if (!node?.key || !node?.dataRef) return;
|
||||
const position = resolveSidebarContextMenuPosition(event.clientX, event.clientY);
|
||||
setContextMenu({
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
sourceX: event.clientX,
|
||||
sourceY: event.clientY,
|
||||
items: [],
|
||||
kind: 'v2-connection',
|
||||
node,
|
||||
rootClassName: 'gn-v2-table-context-menu-popup',
|
||||
overlayStyle: { width: 264, maxWidth: 'calc(100vw - 24px)' },
|
||||
maxHeight: position.maxHeight,
|
||||
});
|
||||
};
|
||||
|
||||
const getV2TreeMetaText = (node: any): string => {
|
||||
if (node.type === 'tag') {
|
||||
const count = flattenConnectionNodes(node.children || []).length;
|
||||
return count > 0 ? count.toLocaleString() : '';
|
||||
}
|
||||
if (node.type === 'database') {
|
||||
const count = v2TreeMetrics.databaseTableCounts.get(node.key) || 0;
|
||||
return count > 0 ? count.toLocaleString() : '';
|
||||
}
|
||||
if (node.type === 'object-group') {
|
||||
const count = v2TreeMetrics.objectGroupCounts.get(node.key) || 0;
|
||||
return count > 0 ? count.toLocaleString() : '';
|
||||
}
|
||||
if (node.type === 'redis-db') {
|
||||
const match = String(node.title || '').match(/\((\d+)\)/);
|
||||
return match?.[1] || '';
|
||||
}
|
||||
if (node.type === 'table') {
|
||||
const rowCount = Number(node?.dataRef?.rowCount);
|
||||
return Number.isFinite(rowCount) && rowCount >= 0 ? formatSidebarRowCount(rowCount) : '';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const getV2TableContextMenuStatsKey = (node: any): string => {
|
||||
const id = String(node?.dataRef?.id || '');
|
||||
const dbName = String(node?.dataRef?.dbName || '');
|
||||
const tableName = String(node?.dataRef?.tableName || node?.title || '');
|
||||
return `${id}::${dbName}::${tableName}`;
|
||||
};
|
||||
|
||||
const readNumericMetadataValue = (row: Record<string, any>, keys: string[]): number | undefined => {
|
||||
const value = getCaseInsensitiveRawValue(row, keys);
|
||||
if (value === undefined || value === null || value === '') return undefined;
|
||||
const normalized = Number(String(value).replace(/,/g, ''));
|
||||
return Number.isFinite(normalized) ? normalized : undefined;
|
||||
};
|
||||
|
||||
const buildV2TableStatusSQL = (node: any): string => {
|
||||
const conn = node.dataRef as SavedConnection & { dbName?: string; tableName?: string; schemaName?: string };
|
||||
const dialect = getMetadataDialect(conn);
|
||||
const dbName = String(conn?.dbName || '').trim();
|
||||
const tableName = String(conn?.tableName || node?.title || '').trim();
|
||||
const objectName = extractObjectName(tableName);
|
||||
const schemaName = String(conn?.schemaName || splitQualifiedName(tableName).schemaName || '').trim();
|
||||
switch (dialect) {
|
||||
case 'mysql':
|
||||
case 'starrocks':
|
||||
return [
|
||||
'SELECT TABLE_ROWS AS table_rows, DATA_LENGTH AS data_length, INDEX_LENGTH AS index_length, ENGINE AS engine',
|
||||
'FROM information_schema.tables',
|
||||
`WHERE table_schema = '${escapeSQLLiteral(dbName)}'`,
|
||||
`AND table_name = '${escapeSQLLiteral(objectName)}'`,
|
||||
'LIMIT 1',
|
||||
].join('\n');
|
||||
case 'postgres':
|
||||
case 'kingbase':
|
||||
case 'vastbase':
|
||||
case 'highgo':
|
||||
case 'opengauss':
|
||||
case 'gaussdb': {
|
||||
const schema = schemaName || 'public';
|
||||
return [
|
||||
"SELECT c.reltuples::bigint AS table_rows, pg_total_relation_size(c.oid) AS data_length, pg_indexes_size(c.oid) AS index_length, 'heap' AS engine",
|
||||
'FROM pg_class c',
|
||||
'JOIN pg_namespace n ON n.oid = c.relnamespace',
|
||||
"WHERE c.relkind = 'r'",
|
||||
`AND n.nspname = '${escapeSQLLiteral(schema)}'`,
|
||||
`AND c.relname = '${escapeSQLLiteral(objectName)}'`,
|
||||
'LIMIT 1',
|
||||
].join('\n');
|
||||
}
|
||||
case 'sqlserver': {
|
||||
const safeTable = tableName.replace(/'/g, "''");
|
||||
return [
|
||||
'SELECT SUM(p.rows) AS table_rows, SUM(a.total_pages) * 8 * 1024 AS data_length, SUM(a.used_pages) * 8 * 1024 AS index_length, NULL AS engine',
|
||||
'FROM sys.tables t',
|
||||
'JOIN sys.indexes i ON t.object_id = i.object_id',
|
||||
'JOIN sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id',
|
||||
'JOIN sys.allocation_units a ON p.partition_id = a.container_id',
|
||||
`WHERE t.object_id = OBJECT_ID('${safeTable}')`,
|
||||
].join('\n');
|
||||
}
|
||||
case 'clickhouse':
|
||||
return [
|
||||
'SELECT total_rows AS table_rows, total_bytes AS data_length, 0 AS index_length, engine AS engine',
|
||||
'FROM system.tables',
|
||||
`WHERE database = '${escapeSQLLiteral(dbName)}'`,
|
||||
`AND name = '${escapeSQLLiteral(objectName)}'`,
|
||||
'LIMIT 1',
|
||||
].join('\n');
|
||||
case 'oracle':
|
||||
case 'dm': {
|
||||
const owner = (schemaName || dbName || '').toUpperCase();
|
||||
return [
|
||||
'SELECT num_rows AS table_rows, 0 AS data_length, 0 AS index_length, NULL AS engine',
|
||||
'FROM all_tables',
|
||||
`WHERE owner = '${escapeSQLLiteral(owner)}'`,
|
||||
`AND table_name = '${escapeSQLLiteral(objectName.toUpperCase())}'`,
|
||||
'FETCH FIRST 1 ROWS ONLY',
|
||||
].join('\n');
|
||||
}
|
||||
case 'sqlite':
|
||||
case 'duckdb':
|
||||
return `SELECT COUNT(*) AS table_rows, 0 AS data_length, 0 AS index_length, NULL AS engine FROM ${tableName}`;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const renderV2TableContextMenu = (node: any) => {
|
||||
const tableName = String(node?.dataRef?.tableName || node?.title || '').trim();
|
||||
const statsKey = getV2TableContextMenuStatsKey(node);
|
||||
const stats = v2TableContextMenuStats[statsKey];
|
||||
const isStarRocks = getMetadataDialect(node.dataRef as SavedConnection) === 'starrocks';
|
||||
const supportsMessagePublish = Boolean(resolveMessagePublishTarget(node));
|
||||
const isPinned = isSidebarTablePinned(
|
||||
pinnedSidebarTables,
|
||||
String(node?.dataRef?.id || ''),
|
||||
String(node?.dataRef?.dbName || ''),
|
||||
tableName,
|
||||
String(node?.dataRef?.schemaName || ''),
|
||||
);
|
||||
return (
|
||||
<V2TableContextMenuView
|
||||
tableName={tableName}
|
||||
shortcutPlatform={activeShortcutPlatform}
|
||||
stats={stats}
|
||||
isPinned={isPinned}
|
||||
supportsTruncate={supportsTableTruncateAction(node.dataRef?.config?.type, node.dataRef?.config?.driver)}
|
||||
supportsStarRocksRollup={isStarRocks}
|
||||
supportsMessagePublish={supportsMessagePublish}
|
||||
onAction={(action) => {
|
||||
setContextMenu(null);
|
||||
handleV2TableContextMenuAction(node, action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderV2TableGroupContextMenu = (node: any) => {
|
||||
const groupData = node.dataRef || {};
|
||||
const sortPreferenceKey = `${groupData.id}-${groupData.dbName}`;
|
||||
const currentSort = tableSortPreference[sortPreferenceKey] || 'name';
|
||||
return (
|
||||
<V2TableGroupContextMenuView
|
||||
shortcutPlatform={activeShortcutPlatform}
|
||||
dbName={String(groupData.dbName || '')}
|
||||
count={Array.isArray(node.children) ? node.children.length : 0}
|
||||
currentSort={currentSort}
|
||||
onAction={(action) => {
|
||||
setContextMenu(null);
|
||||
handleV2TableGroupContextMenuAction(node, action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 || '')}
|
||||
shortcutPlatform={activeShortcutPlatform}
|
||||
dialect={dialect}
|
||||
supportsSchemaActions={isPostgresSchemaDialect(dialect)}
|
||||
supportsStarRocksActions={dialect === 'starrocks'}
|
||||
supportsRenameDatabase={capabilities.supportsRenameDatabase}
|
||||
supportsDropDatabase={capabilities.supportsDropDatabase}
|
||||
onAction={(action) => {
|
||||
setContextMenu(null);
|
||||
handleV2DatabaseContextMenuAction(node, action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const handleV2SchemaContextMenuAction = (node: any, action: V2SchemaContextMenuActionKey) => {
|
||||
switch (action) {
|
||||
case 'rename-schema':
|
||||
openRenameSchemaModal(node);
|
||||
return;
|
||||
case 'refresh-schema':
|
||||
void loadTables(getDatabaseNodeRef(node?.dataRef, String(node?.dataRef?.dbName || '').trim()));
|
||||
return;
|
||||
case 'export-schema':
|
||||
void handleExportSchemaSQL(node, false);
|
||||
return;
|
||||
case 'backup-schema-sql':
|
||||
void handleExportSchemaSQL(node, true);
|
||||
return;
|
||||
case 'drop-schema':
|
||||
handleDeleteSchema(node);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const renderV2SchemaContextMenu = (node: any) => (
|
||||
<V2SchemaContextMenuView
|
||||
dbName={String(node?.dataRef?.dbName || '')}
|
||||
schemaName={String(node?.dataRef?.schemaName || node?.title || '')}
|
||||
shortcutPlatform={activeShortcutPlatform}
|
||||
onAction={(action) => {
|
||||
setContextMenu(null);
|
||||
handleV2SchemaContextMenuAction(node, action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
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
|
||||
connectionName={String(conn?.name || node.title || t('connection.unnamed'))}
|
||||
shortcutPlatform={activeShortcutPlatform}
|
||||
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,
|
||||
selected: tag.id === currentTagId,
|
||||
}))}
|
||||
onAction={(action) => {
|
||||
setContextMenu(null);
|
||||
handleV2ConnectionContextMenuAction(node, action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderV2ConnectionGroupContextMenu = (group: V2RailConnectionGroup) => (
|
||||
<V2ConnectionGroupContextMenuView
|
||||
groupName={group.name}
|
||||
count={group.connections.length}
|
||||
onAction={(action) => {
|
||||
setContextMenu(null);
|
||||
handleV2ConnectionGroupContextMenuAction(group, action);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderV2SidebarContextMenuContent = (menu: SidebarContextMenuState) => {
|
||||
if (!menu.node) return null;
|
||||
if (menu.kind === 'v2-table') return renderV2TableContextMenu(menu.node);
|
||||
if (menu.kind === 'v2-database') return renderV2DatabaseContextMenu(menu.node);
|
||||
if (menu.kind === 'v2-schema') return renderV2SchemaContextMenu(menu.node);
|
||||
if (menu.kind === 'v2-table-group') return renderV2TableGroupContextMenu(menu.node);
|
||||
if (menu.kind === 'v2-connection') return renderV2ConnectionContextMenu(menu.node);
|
||||
if (menu.kind === 'v2-connection-group') return renderV2ConnectionGroupContextMenu(menu.node);
|
||||
return null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!contextMenu?.kind) return;
|
||||
const onPointerDown = (event: MouseEvent) => {
|
||||
const target = event.target instanceof Node ? event.target : null;
|
||||
if (target && contextMenuPortalRef.current?.contains(target)) return;
|
||||
setContextMenu(null);
|
||||
};
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') setContextMenu(null);
|
||||
};
|
||||
document.addEventListener('mousedown', onPointerDown);
|
||||
document.addEventListener('keydown', onKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', onPointerDown);
|
||||
document.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
}, [contextMenu?.kind]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!contextMenu?.kind) return;
|
||||
const frame = requestAnimationFrame(() => {
|
||||
const portal = contextMenuPortalRef.current;
|
||||
if (!portal) return;
|
||||
const rect = portal.getBoundingClientRect();
|
||||
const content = portal.querySelector('.gn-v2-table-context-menu') as HTMLElement | null;
|
||||
const measuredHeight = Math.max(rect.height, content?.scrollHeight || 0);
|
||||
const position = resolveSidebarContextMenuPosition(contextMenu.sourceX ?? contextMenu.x, contextMenu.sourceY ?? contextMenu.y, {
|
||||
width: rect.width || SIDEBAR_CONTEXT_MENU_FALLBACK_WIDTH,
|
||||
height: measuredHeight || SIDEBAR_CONTEXT_MENU_FALLBACK_HEIGHT,
|
||||
});
|
||||
setContextMenu(prev => {
|
||||
if (!prev?.kind) return prev;
|
||||
if (prev.x === position.x && prev.y === position.y && prev.maxHeight === position.maxHeight) return prev;
|
||||
return { ...prev, x: position.x, y: position.y, maxHeight: position.maxHeight };
|
||||
});
|
||||
});
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [contextMenu?.kind, contextMenu?.x, contextMenu?.y]);
|
||||
|
||||
const fetchV2TableContextMenuStats = async (node: any) => {
|
||||
const statsKey = getV2TableContextMenuStatsKey(node);
|
||||
if (!statsKey || v2TableContextMenuStats[statsKey]?.loading) return;
|
||||
const sql = buildV2TableStatusSQL(node);
|
||||
if (!sql) {
|
||||
setV2TableContextMenuStats(prev => ({ ...prev, [statsKey]: { unavailable: true } }));
|
||||
return;
|
||||
}
|
||||
|
||||
setV2TableContextMenuStats(prev => ({ ...prev, [statsKey]: { ...prev[statsKey], loading: true } }));
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const conn = node.dataRef;
|
||||
const res = await DBQuery(buildRuntimeConfig(conn, conn.dbName) as any, conn.dbName || '', sql);
|
||||
if (!res.success || !Array.isArray(res.data) || res.data.length === 0) {
|
||||
setV2TableContextMenuStats(prev => ({ ...prev, [statsKey]: { unavailable: true } }));
|
||||
return;
|
||||
}
|
||||
const row = res.data[0] as Record<string, any>;
|
||||
setV2TableContextMenuStats(prev => ({
|
||||
...prev,
|
||||
[statsKey]: {
|
||||
rowCount: readNumericMetadataValue(row, ['table_rows', 'TABLE_ROWS', 'rows', 'num_rows', 'reltuples', 'total_rows']),
|
||||
dataLength: readNumericMetadataValue(row, ['data_length', 'DATA_LENGTH', 'total_bytes']),
|
||||
indexLength: readNumericMetadataValue(row, ['index_length', 'INDEX_LENGTH']),
|
||||
engine: getCaseInsensitiveValue(row, ['engine', 'ENGINE']),
|
||||
},
|
||||
}));
|
||||
addSqlLog({
|
||||
id: `${Date.now()}-table-stats`,
|
||||
timestamp: Date.now(),
|
||||
sql,
|
||||
status: 'success',
|
||||
duration: Date.now() - startTime,
|
||||
dbName: conn.dbName,
|
||||
});
|
||||
} catch (error: any) {
|
||||
setV2TableContextMenuStats(prev => ({ ...prev, [statsKey]: { unavailable: true } }));
|
||||
addSqlLog({
|
||||
id: `${Date.now()}-table-stats-error`,
|
||||
timestamp: Date.now(),
|
||||
sql,
|
||||
status: 'error',
|
||||
duration: Date.now() - startTime,
|
||||
message: error?.message || String(error),
|
||||
dbName: node?.dataRef?.dbName,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const refreshV2TableContextMenuStats = (node: any) => {
|
||||
const statsKey = getV2TableContextMenuStatsKey(node);
|
||||
setV2TableContextMenuStats(prev => ({ ...prev, [statsKey]: { loading: true } }));
|
||||
void fetchV2TableContextMenuStats(node);
|
||||
};
|
||||
|
||||
|
||||
return {
|
||||
contextMenu,
|
||||
setContextMenu,
|
||||
contextMenuPortalRef,
|
||||
buildRailConnectionStatus,
|
||||
openV2ConnectionContextMenu,
|
||||
getV2TreeMetaText,
|
||||
renderV2SidebarContextMenuContent,
|
||||
fetchV2TableContextMenuStats,
|
||||
refreshV2TableContextMenuStats,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user