import SidebarConnectionRail from './sidebar/SidebarConnectionRail'; import SidebarSearchPanel, { type SidebarSearchPanelProps } from './sidebar/SidebarSearchPanel'; import SlowQueryRailButton from './sidebar/SlowQueryRailButton'; import { buildSidebarLegacyNodeMenuItems } from './sidebar/sidebarLegacyNodeMenu'; import { getMetadataDialect, shouldHideSchemaPrefix, splitQualifiedName, } from './sidebar/sidebarMetadataLoaders'; import { useSidebarBatchExport, } from './sidebar/useSidebarBatchExport'; import { SidebarBatchExportModals } from './sidebar/SidebarBatchExportModals'; import { SidebarEntityModals } from './sidebar/SidebarEntityModals'; import { renderSidebarV2TreeTitle } from './sidebar/SidebarTreeTitle'; import { useSidebarV2ContextMenu, } from './sidebar/useSidebarV2ContextMenu'; import { useSidebarObjectActions, type SidebarMessagePublishTarget, } from './sidebar/useSidebarObjectActions'; import { useSidebarSearchModel } from './sidebar/useSidebarSearchModel'; import { useSidebarV2ActionHandlers } from './sidebar/useSidebarV2ActionHandlers'; import { useSidebarCommandSearchRunner } from './sidebar/useSidebarCommandSearchRunner'; import { useSidebarTitleRender } from './sidebar/useSidebarTitleRender'; import { normalizeDriverType, useSidebarTreeLoaders, } from './sidebar/useSidebarTreeLoaders'; export { formatSidebarDriverAgentUpdateWarning } from './sidebar/useSidebarTreeLoaders'; import { ExternalSQLFileModal, SQLFileExecutionModal, useSidebarExternalSqlWorkflow, } from './sidebar/SidebarExternalSqlWorkflow'; export { buildSQLFileExecutionFooter, SQLFileExecutionProgressContent, } from './sidebar/SidebarExternalSqlWorkflow'; export type { SQLFileExecutionProgressState, SQLFileExecutionStatus, } from './sidebar/SidebarExternalSqlWorkflow'; import { V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID, formatSidebarRowCount, hasSidebarLazyChildren, shouldClearSidebarActiveContextOnEmptySelect, shouldLoadSidebarNodeOnExpand, getV2RailConnectionGroupBadgeText, type V2ExplorerFilter, } from './sidebar/sidebarHelpers'; // 重新导出,保持外部测试文件的 `from './Sidebar'` 兼容 export { V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID, formatSidebarRowCount, hasSidebarLazyChildren, shouldClearSidebarActiveContextOnEmptySelect, shouldLoadSidebarNodeOnExpand, getV2RailConnectionGroupBadgeText, isV2SidebarObjectNode, resolveV2ObjectGroupTitle, resolveSidebarTableNameForCopy, parseV2CommandSearchQuery, } from './sidebar/sidebarHelpers'; import React, { useEffect, useState, useMemo, useRef, useCallback, useDeferredValue } from 'react'; import { createPortal } from 'react-dom'; import { Tree, message, Dropdown, MenuProps, Input, Button, Form, Popover, Tooltip } from 'antd'; import { DatabaseOutlined, TableOutlined, ConsoleSqlOutlined, HddOutlined, FolderOutlined, FolderOpenOutlined, FileTextOutlined, CopyOutlined, ExportOutlined, FolderAddOutlined, SaveOutlined, EditOutlined, SearchOutlined, KeyOutlined, ThunderboltOutlined, UnorderedListOutlined, FunctionOutlined, LinkOutlined, FileAddOutlined, PlusOutlined, ReloadOutlined, SendOutlined, DeleteOutlined, DisconnectOutlined, CheckSquareOutlined, FilterOutlined, DashboardOutlined, WarningOutlined, AimOutlined, MoreOutlined, ToolOutlined, SettingOutlined, BarsOutlined } from '@ant-design/icons'; import { buildSidebarRootConnectionToken, buildSidebarRootTagToken, resolveSidebarRootOrderTokens, useStore, } from '../store'; import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme'; import { SavedConnection, SavedQuery, ExternalSQLDirectory, ExternalSQLTreeEntry } from '../types'; import { getDbIcon } from './DatabaseIcons'; import { ListSQLDirectory } from '../../wailsjs/go/app/App'; import { supportsTableTruncateAction } from './tableDataDangerActions'; import { EventsOn } from '../../wailsjs/runtime/runtime'; import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance'; import { useAutoFetchVisibility } from '../utils/autoFetchVisibility'; import FindInDatabaseModal from './FindInDatabaseModal'; import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig'; import { resolveDataSourceType } from '../utils/dataSourceCapabilities'; import { noAutoCapInputProps } from '../utils/inputAutoCap'; import { resolveSidebarRuntimeDatabase, } from '../utils/sidebarMetadata'; import { findSidebarNodePathByKey, findSidebarNodePathForLocate, normalizeSidebarLocateObjectRequest, normalizeSidebarLocateObjectRequestFromTab, resolveSidebarLocateTarget, type SidebarLocateTreeNodeLike, } from '../utils/sidebarLocate'; import { resolveConnectionAccentColor, resolveConnectionIconType } from '../utils/connectionVisual'; import { buildJVMTabTitle } from '../utils/jvmRuntimePresentation'; import { buildJVMDiagnosticActionDescriptor, buildJVMMonitoringActionDescriptors } from '../utils/jvmSidebarActions'; import { buildBatchDatabaseExportWorkbenchTab, buildBatchTableExportWorkbenchTab, } from '../utils/tableExportTab'; import { useExportProgressDialog } from './ExportProgressModal'; import { getShortcutPlatform, resolveShortcutDisplay } from '../utils/shortcuts'; import { buildExternalSQLRootNode, type ExternalSQLTreeNode } from '../utils/externalSqlTree'; import { t } from '../i18n'; import MessagePublishModal from './MessagePublishModal'; import { SIDEBAR_CONTEXT_MENU_FALLBACK_HEIGHT, SIDEBAR_CONTEXT_MENU_FALLBACK_WIDTH, resolveSidebarContextMenuPosition, type SearchScope, } from './sidebarCoreUtils'; export { resolveSidebarContextMenuPosition } from './sidebarCoreUtils'; export type { ExternalSQLFileModalMode, SearchScope } from './sidebarCoreUtils'; import { buildSidebarTableChildrenForUi, buildV2RailConnectionGroups, buildV2SidebarTableSectionedChildren, estimateV2TreeHorizontalScrollWidth, filterV2CommandSearchTreeItems, filterV2ExplorerTreeByKind, isSidebarTablePinned, normalizeSidebarTreeRelativeDropPosition, resolveSidebarConnectionIdFromKey, resolveSidebarDropInsertBefore, resolveSidebarDropNodeFromDomEvent, resolveSidebarDropTargetMetricsFromDomEvent, resolveSidebarNodeConnectionId, resolveSidebarTagDropInsertBefore, resolveV2ActiveConnectionId, resolveV2CommandSearchPersistentFilter, shouldSkipSidebarLoadOnExpandWhileDragging, shouldSkipSidebarSelectWhileDragging, shouldCloseV2CommandSearchOnGlobalKey, shouldRunV2CommandSearchEnter, sortSidebarTableEntries, type SidebarTreeNode as TreeNode, type V2CommandSearchItem, } from './sidebarV2Utils'; export { buildSidebarTableChildrenForUi, buildV2RailConnectionGroups, buildV2SidebarTableSectionedChildren, estimateV2TreeHorizontalScrollWidth, filterV2CommandSearchTreeItems, filterV2ExplorerTreeByKind, isSidebarTablePinned, normalizeSidebarTreeRelativeDropPosition, resolveSidebarConnectionIdFromKey, resolveSidebarDropInsertBefore, resolveSidebarDropNodeFromDomEvent, resolveSidebarDropTargetMetricsFromDomEvent, resolveSidebarNodeConnectionId, resolveSidebarTagDropInsertBefore, resolveV2ActiveConnectionId, resolveV2CommandSearchPersistentFilter, shouldSkipSidebarLoadOnExpandWhileDragging, shouldSkipSidebarSelectWhileDragging, shouldCloseV2CommandSearchOnGlobalKey, shouldRunV2CommandSearchEnter, sortSidebarTableEntries, }; export type { V2CommandSearchItem, V2RailConnectionGroup } from './sidebarV2Utils'; const { Search } = Input; const SIDEBAR_LOCATE_LOAD_WAIT_INTERVAL_MS = 50; const SIDEBAR_LOCATE_LOAD_WAIT_ATTEMPTS = 160; // resolveV2ObjectGroupTitle 已迁移到 ./sidebar/sidebarHelpers // shouldLoadSidebarNodeOnExpand 已迁移到 ./sidebar/sidebarHelpers // resolveSidebarTableNameForCopy 已迁移到 ./sidebar/sidebarHelpers const buildConnectionRootQueryTabTitle = () => t('query.new'); const buildConnectionRootRedisCommandTabTitle = (redisDbLabel = 'db0') => t('sidebar.tab.redis_command', { database: redisDbLabel }); const buildConnectionRootRedisMonitorTabTitle = (redisDbLabel = 'db0') => t('sidebar.tab.redis_monitor', { database: redisDbLabel }); const V2_EXPLORER_FILTER_OPTIONS: Array<{ key: V2ExplorerFilter; labelKey: string }> = [ { key: 'all', labelKey: 'sidebar.command_search.object_kind.all' }, { key: 'tables', labelKey: 'sidebar.command_search.object_kind.tables' }, { key: 'views', labelKey: 'sidebar.command_search.object_kind.views' }, { key: 'routines', labelKey: 'sidebar.command_search.object_kind.routines' }, { key: 'events', labelKey: 'sidebar.command_search.object_kind.events' }, ]; const buildConnectionReloadSignature = (conn?: SavedConnection | null): string => { if (!conn) return ''; return JSON.stringify({ config: conn.config || {}, includeDatabases: conn.includeDatabases || [], includeRedisDatabases: conn.includeRedisDatabases || [], }); }; const isConnectionTreeKey = (key: React.Key, connectionId: string): boolean => { const text = String(key); return text === connectionId || text.startsWith(`${connectionId}-`); }; const isPostgresSchemaDialect = (dialect: string): boolean => ( ['postgres', 'kingbase', 'highgo', 'vastbase', 'opengauss'].includes(normalizeDriverType(dialect)) ); const isSavedQueryUnmatchedForConnectionIds = (query: SavedQuery, connectionIds: Set): boolean => ( query.bindingStatus === 'orphan' || !connectionIds.has(query.connectionId) ); export const buildAllSavedQueriesTreeNode = ( savedQueries: SavedQuery[], connections: SavedConnection[], ): TreeNode | null => { if (savedQueries.length === 0) { return null; } const connectionIds = new Set(connections.map((conn) => conn.id)); const unmatchedSavedQueries = savedQueries.filter((query) => isSavedQueryUnmatchedForConnectionIds(query, connectionIds)); const unmatchedIds = new Set(unmatchedSavedQueries.map((query) => query.id)); const createQueryNode = (query: SavedQuery): TreeNode => ({ title: query.name || t('sidebar.tree.untitled_query'), key: `all-saved-query-${query.id}`, icon: , type: 'saved-query', dataRef: query, isLeaf: true, }); const buildDatabaseGroups = (queries: SavedQuery[], keyPrefix: string): TreeNode[] => { const groupedByDatabase = new Map(); queries.forEach((query) => { const dbName = String(query.dbName || '').trim() || t('sidebar.tree.default_database'); groupedByDatabase.set(dbName, [...(groupedByDatabase.get(dbName) || []), query]); }); return Array.from(groupedByDatabase.entries()).map(([dbName, items]) => ({ title: dbName, key: `${keyPrefix}-db-${encodeURIComponent(dbName)}`, icon: , type: 'saved-query-group', selectable: false, isLeaf: false, children: items.map(createQueryNode), })); }; const groupedByConnection = new Map(); savedQueries.forEach((query) => { if (unmatchedIds.has(query.id)) { return; } groupedByConnection.set(query.connectionId, [ ...(groupedByConnection.get(query.connectionId) || []), query, ]); }); const children: TreeNode[] = []; connections.forEach((conn) => { const connectionQueries = groupedByConnection.get(conn.id); if (!connectionQueries || connectionQueries.length === 0) { return; } const iconType = resolveConnectionIconType(conn); const iconColor = resolveConnectionAccentColor(conn); children.push({ title: conn.name || conn.id, key: `all-saved-queries-connection-${conn.id}`, icon: getDbIcon(iconType, iconColor, 22), type: 'saved-query-group', selectable: false, isLeaf: false, children: buildDatabaseGroups(connectionQueries, `all-saved-queries-connection-${conn.id}`), }); }); if (unmatchedSavedQueries.length > 0) { const groupedByOriginalConnection = new Map(); unmatchedSavedQueries.forEach((query) => { const originalConnectionId = String(query.originalConnectionId || query.connectionId || t('sidebar.tree.unknown_connection')).trim() || t('sidebar.tree.unknown_connection'); groupedByOriginalConnection.set(originalConnectionId, [ ...(groupedByOriginalConnection.get(originalConnectionId) || []), query, ]); }); children.push({ title: t('sidebar.tree.unmatched_saved_queries'), key: 'all-saved-queries-unmatched', icon: , type: 'saved-query-group', selectable: false, isLeaf: false, children: Array.from(groupedByOriginalConnection.entries()).map(([connectionLabel, items]) => ({ title: connectionLabel, key: `all-saved-queries-unmatched-${encodeURIComponent(connectionLabel)}`, icon: , type: 'saved-query-group', selectable: false, isLeaf: false, children: buildDatabaseGroups(items, `all-saved-queries-unmatched-${encodeURIComponent(connectionLabel)}`), })), }); } return { title: t('sidebar.tree.all_saved_queries'), key: 'all-saved-queries', icon: , type: 'all-saved-queries', isLeaf: false, selectable: false, children, }; }; const Sidebar: React.FC<{ onCreateConnection?: () => void; onEditConnection?: (conn: SavedConnection) => void; onOpenTools?: () => void; onOpenSettings?: () => void; onToggleAI?: () => void; onToggleLogPanel?: () => void; sqlLogCount?: number; uiVersion?: 'legacy' | 'v2'; onFocusCommandSearch?: () => void; }> = React.memo(({ onCreateConnection, onEditConnection, onOpenTools, onOpenSettings, onToggleAI, onToggleLogPanel, sqlLogCount = 0, uiVersion, onFocusCommandSearch, }) => { const connections = useStore(state => state.connections); const savedQueries = useStore(state => state.savedQueries); const externalSQLDirectories = useStore(state => state.externalSQLDirectories); const saveQuery = useStore(state => state.saveQuery); const deleteQuery = useStore(state => state.deleteQuery); const saveExternalSQLDirectory = useStore(state => state.saveExternalSQLDirectory); const deleteExternalSQLDirectory = useStore(state => state.deleteExternalSQLDirectory); const addConnection = useStore(state => state.addConnection); const addTab = useStore(state => state.addTab); const updateQueryTabDraft = useStore(state => state.updateQueryTabDraft); const tabs = useStore(state => state.tabs); const activeTabId = useStore(state => state.activeTabId); const setActiveContext = useStore(state => state.setActiveContext); const removeConnection = useStore(state => state.removeConnection); const connectionTags = useStore(state => state.connectionTags); const sidebarRootOrder = useStore(state => state.sidebarRootOrder); const addConnectionTag = useStore(state => state.addConnectionTag); const updateConnectionTag = useStore(state => state.updateConnectionTag); const removeConnectionTag = useStore(state => state.removeConnectionTag); const moveConnectionToTag = useStore(state => state.moveConnectionToTag); const reorderConnections = useStore(state => state.reorderConnections); const reorderTags = useStore(state => state.reorderTags); const reorderSidebarRoot = useStore(state => state.reorderSidebarRoot); const closeTabsByConnection = useStore(state => state.closeTabsByConnection); const closeTabsByDatabase = useStore(state => state.closeTabsByDatabase); const theme = useStore(state => state.theme); const appearance = useStore(state => state.appearance); const activeContext = useStore(state => state.activeContext); const tableAccessCount = useStore(state => state.tableAccessCount); const tableSortPreference = useStore(state => state.tableSortPreference); const pinnedSidebarTables = useStore(state => state.pinnedSidebarTables); const recordTableAccess = useStore(state => state.recordTableAccess); const setTableSortPreference = useStore(state => state.setTableSortPreference); const setSidebarTablePinned = useStore(state => state.setSidebarTablePinned); const addSqlLog = useStore(state => state.addSqlLog); const sqlLogs = useStore(state => state.sqlLogs) || []; const shortcutOptions = useStore(state => state.shortcutOptions); const languagePreference = useStore(state => state.languagePreference); const setAppearance = useStore(state => state.setAppearance); const setAIPanelVisible = useStore(state => state.setAIPanelVisible); const addAIContext = useStore(state => state.addAIContext); void languagePreference; const darkMode = theme === 'dark'; const resolvedAppearance = resolveAppearanceValues(appearance); const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity); const { exportProgressModal, runExportWithProgress } = useExportProgressDialog(); const disableLocalBackdropFilter = isMacLikePlatform(); const autoFetchVisible = useAutoFetchVisibility(); const activeShortcutPlatform = getShortcutPlatform(isMacLikePlatform()); const focusSidebarSearchShortcut = resolveShortcutDisplay(shortcutOptions, 'focusSidebarSearch', activeShortcutPlatform); const focusSidebarSearchShortcutTokens = focusSidebarSearchShortcut === '-' ? [] : focusSidebarSearchShortcut.match(/Ctrl|Alt|Shift|Esc|Space|[⌘⌃⌥⇧↵↑↓←→]|[^+]/g) ?? []; const isV2Ui = (uiVersion ?? appearance.uiVersion) === 'v2'; const [treeData, setTreeData] = useState([]); const activeTab = useMemo(() => tabs.find(tab => tab.id === activeTabId) || null, [tabs, activeTabId]); const activeTabLocateRequest = useMemo(() => normalizeSidebarLocateObjectRequestFromTab(activeTab), [activeTab]); const canLocateActiveTab = !!activeTabLocateRequest; // Background Helper (Duplicate logic for now, ideally shared) const getBg = (darkHex: string) => { if (!darkMode) return `rgba(255, 255, 255, ${opacity})`; const hex = darkHex.replace('#', ''); const r = parseInt(hex.substring(0, 2), 16); const g = parseInt(hex.substring(2, 4), 16); const b = parseInt(hex.substring(4, 6), 16); return `rgba(${r}, ${g}, ${b}, ${opacity})`; }; const bgMain = getBg('#141414'); const overlayTheme = useMemo( () => buildOverlayWorkbenchTheme(darkMode, { disableBackdropFilter: disableLocalBackdropFilter }), [darkMode, disableLocalBackdropFilter, appearance.uiVersion], ); const modalPanelStyle = useMemo(() => ({ background: overlayTheme.shellBg, border: overlayTheme.shellBorder, boxShadow: overlayTheme.shellShadow, backdropFilter: overlayTheme.shellBackdropFilter, }), [overlayTheme]); const modalSectionStyle = useMemo(() => ({ padding: 14, borderRadius: 14, border: overlayTheme.sectionBorder, background: overlayTheme.sectionBg, }), [overlayTheme]); const modalScrollSectionStyle = useMemo(() => ({ maxHeight: 400, overflow: 'auto' as const, border: overlayTheme.sectionBorder, borderRadius: 14, padding: 12, background: overlayTheme.sectionBg, }), [overlayTheme]); const modalHintTextStyle = useMemo(() => ({ color: overlayTheme.mutedText, fontSize: 12, lineHeight: 1.6, }), [overlayTheme]); const renderSidebarModalTitle = (icon: React.ReactNode, title: string, description: string) => (
{icon}
{title}
{description}
); const v2SidebarSearchMode = appearance.v2SidebarSearchMode ?? 'command'; const v2UseLegacySidebarFilter = isV2Ui && v2SidebarSearchMode === 'filter'; const v2CommandSearchPersistentFilterEnabled = appearance.v2CommandSearchPersistentFilterEnabled === true; const v2PersistedSidebarFilter = appearance.v2SidebarPersistedFilter ?? ''; const [searchValue, setSearchValue] = useState(v2PersistedSidebarFilter); const deferredSearchValue = useDeferredValue(searchValue); const [searchScopes, setSearchScopes] = useState(['smart']); const [v2ExplorerFilter, setV2ExplorerFilter] = useState('all'); const [isSearchScopePopoverOpen, setIsSearchScopePopoverOpen] = useState(false); const searchInputRef = useRef(null); const commandSearchInputRef = useRef(null); const [isV2CommandSearchOpen, setIsV2CommandSearchOpen] = useState(false); const [v2CommandSearchValue, setV2CommandSearchValue] = useState(''); const deferredV2CommandSearchValue = useDeferredValue(v2CommandSearchValue); const [v2CommandActiveIndex, setV2CommandActiveIndex] = useState(0); const [expandedKeys, setExpandedKeys] = useState([]); const [autoExpandParent, setAutoExpandParent] = useState(true); const [loadedKeys, setLoadedKeys] = useState([]); const [selectedKeys, setSelectedKeys] = useState([]); const selectedNodesRef = useRef([]); const loadingNodesRef = useRef>(new Set()); const clickTimerRef = useRef | null>(null); const treeDragSelectSuppressUntilRef = useRef(0); const treeDragSelectionSnapshotRef = useRef<{ selectedKeys: React.Key[]; selectedNodes: any[]; activeContext: { connectionId: string; dbName: string } | null; }>({ selectedKeys: [], selectedNodes: [], activeContext: null, }); const connectionReloadSignaturesRef = useRef>({}); const connectionIds = useMemo(() => connections.map((conn) => conn.id), [connections]); const connectionIdSet = useMemo(() => new Set(connectionIds), [connectionIds]); const unmatchedSavedQueries = useMemo( () => savedQueries.filter((query) => isSavedQueryUnmatchedForConnectionIds(query, connectionIdSet)), [connectionIdSet, savedQueries], ); const allSavedQueriesNode = useMemo(() => { return buildAllSavedQueriesTreeNode(savedQueries, connections); }, [connections, savedQueries]); const snapshotTreeSelectionBeforeDrag = useCallback(() => { treeDragSelectionSnapshotRef.current = { selectedKeys: [...selectedKeys], selectedNodes: [...selectedNodesRef.current], activeContext: activeContext ? { ...activeContext } : null, }; }, [activeContext, selectedKeys]); const restoreTreeSelectionAfterDrag = useCallback(() => { const snapshot = treeDragSelectionSnapshotRef.current; treeDragSelectSuppressUntilRef.current = Date.now() + 1000; setSelectedKeys(snapshot.selectedKeys); selectedNodesRef.current = snapshot.selectedNodes; setActiveContext(snapshot.activeContext); }, [setActiveContext]); const openV2CommandSearch = useCallback(() => { setIsV2CommandSearchOpen(true); setV2CommandActiveIndex(0); }, []); const commitV2CommandSearchPersistentFilter = useCallback((value = v2CommandSearchValue) => { if (!v2CommandSearchPersistentFilterEnabled) { return; } const nextFilter = value.trim(); setSearchValue(nextFilter); if (nextFilter !== v2PersistedSidebarFilter) { setAppearance({ v2SidebarPersistedFilter: nextFilter }); } }, [setAppearance, v2CommandSearchPersistentFilterEnabled, v2CommandSearchValue, v2PersistedSidebarFilter]); const closeV2CommandSearch = useCallback(() => { commitV2CommandSearchPersistentFilter(); setIsV2CommandSearchOpen(false); setV2CommandSearchValue(''); setV2CommandActiveIndex(0); }, [commitV2CommandSearchPersistentFilter]); useEffect(() => { setSearchValue(v2PersistedSidebarFilter); }, [v2PersistedSidebarFilter]); useEffect(() => { if (!v2UseLegacySidebarFilter) { return; } const nextFilter = searchValue.trim(); if (nextFilter !== v2PersistedSidebarFilter) { setAppearance({ v2SidebarPersistedFilter: nextFilter }); } }, [searchValue, setAppearance, v2PersistedSidebarFilter, v2UseLegacySidebarFilter]); const handleV2CommandSearchValueChange = useCallback((value: string) => { setV2CommandSearchValue(value); }, []); useEffect(() => { if (!v2CommandSearchPersistentFilterEnabled) { return; } if (!isV2CommandSearchOpen) { return; } const nextFilter = resolveV2CommandSearchPersistentFilter({ commandSearchValue: deferredV2CommandSearchValue, persistedFilter: v2PersistedSidebarFilter, enabled: v2CommandSearchPersistentFilterEnabled, isOpen: isV2CommandSearchOpen, }); setSearchValue(nextFilter); const timer = window.setTimeout(() => { setAppearance({ v2SidebarPersistedFilter: nextFilter }); }, 160); return () => window.clearTimeout(timer); }, [deferredV2CommandSearchValue, isV2CommandSearchOpen, setAppearance, v2CommandSearchPersistentFilterEnabled, v2PersistedSidebarFilter]); const toggleV2CommandSearchPersistentFilter = useCallback((enabled: boolean) => { const nextFilter = enabled ? v2CommandSearchValue.trim() : ''; setSearchValue(nextFilter); setAppearance({ v2CommandSearchPersistentFilterEnabled: enabled, v2SidebarPersistedFilter: nextFilter, }); message.success( enabled ? t('sidebar.message.sidebar_filter_sync_enabled') : t('sidebar.message.sidebar_filter_sync_disabled'), ); }, [setAppearance, v2CommandSearchValue]); const resetV2SidebarFilter = useCallback(() => { setSearchValue(''); setAppearance({ v2CommandSearchPersistentFilterEnabled: false, v2SidebarPersistedFilter: '', }); message.success(t('sidebar.message.sidebar_filter_reset')); }, [setAppearance]); // Virtual Scroll State const [treeHeight, setTreeHeight] = useState(500); const [treeViewportWidth, setTreeViewportWidth] = useState(0); const treeContainerRef = useRef(null); const treeRef = useRef(null); const treeDataRef = useRef([]); const externalSQLDirectoryTreesRef = useRef>({}); const findTreeNodeByKeyRef = useRef<(nodes: TreeNode[], targetKey: React.Key) => TreeNode | null>(() => null); const expandConnectionFromRailRef = useRef<(connectionId: string) => void>(() => {}); useEffect(() => { treeDataRef.current = treeData; }, [treeData]); useEffect(() => { if (!treeContainerRef.current) return; const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { setTreeHeight(entry.contentRect.height); setTreeViewportWidth(entry.contentRect.width); } }); resizeObserver.observe(treeContainerRef.current); return () => resizeObserver.disconnect(); }, []); useEffect(() => { const handleFocusSidebarSearch = () => { if (isV2Ui && !v2UseLegacySidebarFilter) { openV2CommandSearch(); return; } const inputEl = searchInputRef.current?.input as HTMLInputElement | undefined; if (!inputEl) { return; } inputEl.focus(); inputEl.select(); }; window.addEventListener('gonavi:focus-sidebar-search', handleFocusSidebarSearch as EventListener); return () => { window.removeEventListener('gonavi:focus-sidebar-search', handleFocusSidebarSearch as EventListener); }; }, [isV2Ui, openV2CommandSearch, v2UseLegacySidebarFilter]); useEffect(() => { if (!isV2CommandSearchOpen) return; const timer = window.setTimeout(() => { const inputEl = commandSearchInputRef.current?.input as HTMLInputElement | undefined; inputEl?.focus(); inputEl?.select(); }, 0); return () => window.clearTimeout(timer); }, [isV2CommandSearchOpen]); useEffect(() => { if (!isV2CommandSearchOpen) return; const handleV2CommandSearchGlobalKeyDown = (event: KeyboardEvent) => { if (!shouldCloseV2CommandSearchOnGlobalKey({ key: event.key, isOpen: isV2CommandSearchOpen })) { return; } event.preventDefault(); event.stopPropagation(); closeV2CommandSearch(); }; window.addEventListener('keydown', handleV2CommandSearchGlobalKeyDown, true); return () => window.removeEventListener('keydown', handleV2CommandSearchGlobalKeyDown, true); }, [closeV2CommandSearch, isV2CommandSearchOpen]); // Connection Status State: key -> 'success' | 'error' const [connectionStates, setConnectionStates] = useState>({}); const [isTreeDragging, setIsTreeDragging] = useState(false); // Create Database Modal const [isCreateDbModalOpen, setIsCreateDbModalOpen] = useState(false); const [createDbForm] = Form.useForm(); const [targetConnection, setTargetConnection] = useState(null); const [isCreateSchemaModalOpen, setIsCreateSchemaModalOpen] = useState(false); const [createSchemaForm] = Form.useForm(); const [createSchemaTarget, setCreateSchemaTarget] = useState(null); const [isRenameSchemaModalOpen, setIsRenameSchemaModalOpen] = useState(false); const [renameSchemaForm] = Form.useForm(); const [renameSchemaTarget, setRenameSchemaTarget] = useState(null); const [isRenameDbModalOpen, setIsRenameDbModalOpen] = useState(false); const [renameDbForm] = Form.useForm(); const [renameDbTarget, setRenameDbTarget] = useState(null); const [isRenameTableModalOpen, setIsRenameTableModalOpen] = useState(false); const [renameTableForm] = Form.useForm(); const [renameTableTarget, setRenameTableTarget] = useState(null); const [messagePublishTarget, setMessagePublishTarget] = useState(null); const [isRenameViewModalOpen, setIsRenameViewModalOpen] = useState(false); const [renameViewForm] = Form.useForm(); const [renameViewTarget, setRenameViewTarget] = useState(null); const [isRenameSavedQueryModalOpen, setIsRenameSavedQueryModalOpen] = useState(false); const [renameSavedQueryForm] = Form.useForm(); const [renameSavedQueryTarget, setRenameSavedQueryTarget] = useState(null); // Connection Tag Modals const [isCreateTagModalOpen, setIsCreateTagModalOpen] = useState(false); const [createTagForm] = Form.useForm(); const { isBatchModalOpen, setIsBatchModalOpen, batchTables, checkedTableKeys, setCheckedTableKeys, selectedConnection, selectedDatabase, availableDatabases, batchFilterKeyword, setBatchFilterKeyword, batchFilterType, setBatchFilterType, batchSelectionScope, setBatchSelectionScope, filteredBatchObjects, groupedBatchObjects, selectionScopeTargetKeys, isBatchDbModalOpen, setIsBatchDbModalOpen, batchDatabases, checkedDbKeys, setCheckedDbKeys, selectedDbConnection, handleExportDatabaseSQL, handleExportSchemaSQL, openBatchOperationModal, openBatchTableExportWorkbench, handleConnectionChange, handleDatabaseChange, handleBatchExport, handleBatchClear, handleCheckAll, handleInvertSelection, openBatchDatabaseModal, openBatchDatabaseExportWorkbench, handleDbConnectionChange, handleBatchDbExport, handleCheckAllDb, handleInvertSelectionDb, } = useSidebarBatchExport({ connections, selectedNodesRef, addTab, addSqlLog, }); // Find in Database Modal const [findInDbContext, setFindInDbContext] = useState<{ open: boolean; connectionId: string; dbName: string }>({ open: false, connectionId: '', dbName: '' }); useEffect(() => { if (!autoFetchVisible) { return; } expandedKeys.forEach(key => { const node = findTreeNodeByKey(treeData, key); if (node && node.type === 'database') { loadTables(node); } }); }, [autoFetchVisible, savedQueries]); useEffect(() => { const previousSignatures = connectionReloadSignaturesRef.current; const nextSignatures: Record = {}; const staleConnectionIds = new Set(); connections.forEach((conn) => { const signature = buildConnectionReloadSignature(conn); nextSignatures[conn.id] = signature; if (previousSignatures[conn.id] && previousSignatures[conn.id] !== signature) { staleConnectionIds.add(conn.id); } }); connectionReloadSignaturesRef.current = nextSignatures; if (staleConnectionIds.size > 0) { const staleIds = Array.from(staleConnectionIds); setLoadedKeys((prev) => prev.filter((key) => !staleIds.some((id) => isConnectionTreeKey(key, id))), ); setExpandedKeys((prev) => prev.filter((key) => !staleIds.some((id) => isConnectionTreeKey(key, id))), ); setConnectionStates((prev) => { const next = { ...prev }; staleIds.forEach((id) => { Object.keys(next).forEach((key) => { if (isConnectionTreeKey(key, id)) { delete next[key]; } }); }); return next; }); staleIds.forEach((id) => { Array.from(loadingNodesRef.current).forEach((key) => { if (key === `dbs-${id}` || key.startsWith(`tables-${id}-`)) { loadingNodesRef.current.delete(key); } }); }); } setTreeData((prev) => { const prevMap = new Map(); // We need to recursively extract connections from old tag structures // so if a user expands a connection that was tagged, the state remains const recurseCollect = (nodes: TreeNode[]) => { nodes.forEach((node) => { if (node.type === 'tag') { if (node.children) recurseCollect(node.children); } else if (node.type === 'connection') { prevMap.set(String(node.key), node); } }); }; recurseCollect(prev); const buildConnectionNode = (conn: SavedConnection): TreeNode => { const existing = prevMap.get(conn.id); const iconType = resolveConnectionIconType(conn); const iconColor = resolveConnectionAccentColor(conn); const preserveChildren = existing && !staleConnectionIds.has(conn.id); return { title: conn.name, key: conn.id, icon: getDbIcon(iconType, iconColor, 22), type: 'connection', dataRef: conn, isLeaf: false, children: preserveChildren ? existing.children : undefined, } as TreeNode; }; const taggedConnIds = new Set(); const tagNodesById = new Map(); connectionTags.forEach((tag) => { tag.connectionIds.forEach(id => taggedConnIds.add(id)); tagNodesById.set(tag.id, { title: tag.name, key: `tag-${tag.id}`, icon: ( ), type: 'tag', dataRef: tag, isLeaf: false, children: tag.connectionIds .map(cid => connections.find(c => c.id === cid)) .filter(Boolean) .map(conn => buildConnectionNode(conn!)), } as TreeNode); }); const ungroupedNodesById = new Map(); connections .filter(c => !taggedConnIds.has(c.id)) .forEach((conn) => { ungroupedNodesById.set(conn.id, buildConnectionNode(conn)); }); const orderedRootTokens = resolveSidebarRootOrderTokens( sidebarRootOrder, connectionTags, connections, ); const orderedNodes: TreeNode[] = []; orderedRootTokens.forEach((token) => { if (token.startsWith('tag:')) { const tagNode = tagNodesById.get(token.slice('tag:'.length)); if (!tagNode) return; orderedNodes.push(tagNode); tagNodesById.delete(token.slice('tag:'.length)); return; } if (token.startsWith('connection:')) { const connectionNode = ungroupedNodesById.get(token.slice('connection:'.length)); if (!connectionNode) return; orderedNodes.push(connectionNode); ungroupedNodesById.delete(token.slice('connection:'.length)); } }); orderedNodes.push(...Array.from(tagNodesById.values())); orderedNodes.push(...Array.from(ungroupedNodesById.values())); if (allSavedQueriesNode) { orderedNodes.push(allSavedQueriesNode); } const externalSQLRootNode = prev.find((node) => node.type === 'external-sql-root'); return externalSQLRootNode ? [...orderedNodes, externalSQLRootNode] : orderedNodes; }); }, [connections, connectionTags, sidebarRootOrder, allSavedQueriesNode]); const handleDuplicateConnection = async (conn: SavedConnection) => { if (!conn?.id) return; const backendApp = (window as any).go?.app?.App; if (typeof backendApp?.DuplicateConnection !== 'function') { message.error(t('connection.sidebar.duplicate.backendUnavailable')); return; } try { const duplicatedConnection = await backendApp.DuplicateConnection(conn.id); if (!duplicatedConnection) { throw new Error(t('connection.sidebar.duplicate.noResult')); } addConnection(duplicatedConnection); message.success(t('connection.sidebar.duplicate.success', { name: duplicatedConnection.name, })); } catch (error: any) { message.error(error?.message || t('connection.sidebar.duplicate.failureFallback')); } }; const updateTreeData = (list: TreeNode[], key: React.Key, children: TreeNode[] | undefined): TreeNode[] => { return list.map(node => { if (node.key === key) { return { ...node, children }; } if (node.children) { return { ...node, children: updateTreeData(node.children, key, children) }; } return node; }); }; const findTreeNodeByKey = (nodes: TreeNode[], targetKey: React.Key): TreeNode | null => { for (const node of nodes) { if (node.key === targetKey) { return node; } if (node.children) { const child = findTreeNodeByKey(node.children, targetKey); if (child) { return child; } } } return null; }; findTreeNodeByKeyRef.current = findTreeNodeByKey; const replaceTreeNodeChildren = (key: React.Key, children: TreeNode[] | undefined): TreeNode[] => { const nextTreeData = updateTreeData(treeDataRef.current, key, children); treeDataRef.current = nextTreeData; setTreeData(nextTreeData); return nextTreeData; }; const mergeExpandedTreeKeys = (requiredKeys: React.Key[]) => { setExpandedKeys(prev => { const merged = [...prev]; requiredKeys.forEach(key => { if (!merged.includes(key)) merged.push(key); }); return merged; }); setAutoExpandParent(true); }; const scrollSidebarTreeToKey = (key: React.Key) => { const runAfterFrame = typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function' ? window.requestAnimationFrame.bind(window) : (callback: FrameRequestCallback) => window.setTimeout(() => callback(Date.now()), 0); runAfterFrame(() => { treeRef.current?.scrollTo?.({ key, align: 'auto' }); runAfterFrame(() => { const selectedNode = treeContainerRef.current?.querySelector('.ant-tree-treenode-selected') as HTMLElement | null; selectedNode?.scrollIntoView?.({ block: 'nearest', inline: 'nearest' }); }); }); }; const decorateExternalSQLTreeNode = (node: ExternalSQLTreeNode): TreeNode => { const icon = (() => { switch (node.type) { case 'external-sql-root': return ; case 'external-sql-directory': return ; case 'external-sql-folder': return ; default: return ; } })(); return { ...node, icon, children: node.children?.map((child) => decorateExternalSQLTreeNode(child)), }; }; const buildExternalSQLRootTreeNode = useCallback(( directories: ExternalSQLDirectory[] = externalSQLDirectories, directoryTrees: Record = externalSQLDirectoryTreesRef.current, ): TreeNode => decorateExternalSQLTreeNode(buildExternalSQLRootNode({ directories, directoryTrees, })), [externalSQLDirectories]); const refreshGlobalExternalSQLRootNode = useCallback(async ( showSuccess = false, directoriesOverride?: ExternalSQLDirectory[], ) => { const targetDirectories = directoriesOverride || externalSQLDirectories; const directoryTrees: Record = {}; await Promise.all(targetDirectories.map(async (directory) => { const directoryRes = await ListSQLDirectory(directory.path); if (!directoryRes.success) { message.warning({ key: `external-sql-${directory.id}`, content: t('sidebar.message.external_sql_directory_read_failed', { name: directory.name, error: directoryRes.message, }), }); directoryTrees[directory.id] = []; return; } directoryTrees[directory.id] = Array.isArray(directoryRes.data) ? directoryRes.data as ExternalSQLTreeEntry[] : []; })); externalSQLDirectoryTreesRef.current = directoryTrees; const rootNode = buildExternalSQLRootTreeNode(targetDirectories, directoryTrees); setTreeData((prev) => { const withoutExternalRoot = prev.filter((node) => node.type !== 'external-sql-root'); const nextTreeData = [...withoutExternalRoot, rootNode]; treeDataRef.current = nextTreeData; return nextTreeData; }); if (showSuccess) { message.success(t('sidebar.message.external_sql_directory_refreshed')); } }, [buildExternalSQLRootTreeNode, externalSQLDirectories]); useEffect(() => { void refreshGlobalExternalSQLRootNode(false); }, [refreshGlobalExternalSQLRootNode]); const { handleRunSQLFile, handleOpenSQLFileFromToolbar, openExternalSQLFile, openCreateExternalSQLFileModal, openRenameExternalSQLFileModal, openCreateExternalSQLDirectoryModal, openRenameExternalSQLDirectoryModal, handleDeleteExternalSQLFile, handleDeleteExternalSQLDirectory, handleAddExternalSQLDirectory, handleRemoveExternalSQLDirectory, handleRefreshExternalSQLDirectory, externalSQLFileModalProps, sqlFileExecutionModalProps, } = useSidebarExternalSqlWorkflow({ connections, externalSQLDirectories, activeTab, connectionIds, selectedNodesRef, addTab, saveExternalSQLDirectory, deleteExternalSQLDirectory, refreshGlobalExternalSQLRootNode, setExpandedKeys, setAutoExpandParent, getActiveContext: () => useStore.getState().activeContext, }); const getNodeDatabaseContext = (node: any): { connectionId: string; dbName: string; dbNodeKey: string } | null => { if (!node) return null; if (node.type === 'database') { return { connectionId: String(node?.dataRef?.id || '').trim(), dbName: String(node?.dataRef?.dbName || '').trim(), dbNodeKey: String(node.key || '').trim(), }; } if ( node.type === 'external-sql-root' || node.type === 'external-sql-directory' || node.type === 'external-sql-folder' || node.type === 'external-sql-file' ) { return { connectionId: String(node?.dataRef?.connectionId || '').trim(), dbName: String(node?.dataRef?.dbName || '').trim(), dbNodeKey: String(node?.dataRef?.dbNodeKey || '').trim(), }; } return null; }; const locateObjectInSidebarRef = useRef<(detail: unknown) => Promise>(async () => {}); const waitForSidebarLoadKey = async (loadKey: string): Promise => { for (let attempt = 0; attempt < SIDEBAR_LOCATE_LOAD_WAIT_ATTEMPTS && loadingNodesRef.current.has(loadKey); attempt += 1) { await new Promise(resolve => window.setTimeout(resolve, SIDEBAR_LOCATE_LOAD_WAIT_INTERVAL_MS)); } return !loadingNodesRef.current.has(loadKey); }; const locateObjectInSidebar = async (detail: unknown) => { const request = normalizeSidebarLocateObjectRequest(detail); if (!request) { message.warning(t('sidebar.message.locate_current_table_unavailable')); return; } if (request.objectGroup === 'externalSqlFiles') { await refreshGlobalExternalSQLRootNode(false); const target = resolveSidebarLocateTarget(request, { groupBySchema: false }); const path = findSidebarNodePathForLocate(treeDataRef.current as SidebarLocateTreeNodeLike[], target); if (!path) { message.warning(t('sidebar.message.locate_external_sql_file_not_found', { path: request.filePath })); return; } const targetKey = path[path.length - 1]; const targetNode = findTreeNodeByKey(treeDataRef.current, targetKey); setSearchValue(''); mergeExpandedTreeKeys(path.slice(0, -1)); setSelectedKeys([targetKey]); selectedNodesRef.current = targetNode ? [targetNode] : []; const connectionId = String(request.connectionId || activeContext?.connectionId || activeTab?.connectionId || '').trim(); const dbName = String(request.dbName || activeContext?.dbName || activeTab?.dbName || '').trim(); if (connectionId) { setActiveContext({ connectionId, dbName }); } scrollSidebarTreeToKey(targetKey); return; } const conn = connections.find(item => item.id === request.connectionId); if (!conn) { message.warning(t('sidebar.message.locate_connection_not_found_for_object')); return; } const target = resolveSidebarLocateTarget(request, { groupBySchema: shouldHideSchemaPrefix(conn), }); const objectLabel = request.objectGroup === 'materializedViews' ? t('sidebar.locate.object.materialized_view') : request.objectGroup === 'views' ? t('sidebar.locate.object.view') : request.objectGroup === 'triggers' ? t('sidebar.locate.object.trigger') : request.objectGroup === 'routines' ? t('sidebar.locate.object.routine') : t('sidebar.locate.object.table'); let path = findSidebarNodePathForLocate(treeDataRef.current as SidebarLocateTreeNodeLike[], target); const dbLoadKey = `dbs-${request.connectionId}`; const tableLoadKey = `tables-${request.connectionId}-${request.dbName}`; if (!path && !findSidebarNodePathByKey(treeDataRef.current as SidebarLocateTreeNodeLike[], target.databaseKey)) { const connectionNode = findTreeNodeByKey(treeDataRef.current, target.connectionKey); if (!connectionNode) { message.warning(t('sidebar.message.locate_connection_not_in_tree')); return; } if (loadingNodesRef.current.has(dbLoadKey)) { const loaded = await waitForSidebarLoadKey(dbLoadKey); if (!loaded) { message.info(t('sidebar.message.locate_database_loading', { database: request.dbName })); return; } } else { await loadDatabases(connectionNode); } } const dbNode = findTreeNodeByKey(treeDataRef.current, target.databaseKey); if (!dbNode) { message.warning(t('sidebar.message.locate_database_not_found', { database: request.dbName })); return; } path = findSidebarNodePathForLocate(treeDataRef.current as SidebarLocateTreeNodeLike[], target); if (!path) { if (loadingNodesRef.current.has(tableLoadKey)) { const loaded = await waitForSidebarLoadKey(tableLoadKey); if (!loaded) { message.info(t('sidebar.message.locate_object_loading', { object: objectLabel, database: request.dbName, })); return; } } else { await loadTables(dbNode); } path = findSidebarNodePathForLocate(treeDataRef.current as SidebarLocateTreeNodeLike[], target); } if (!path) { message.warning(t('sidebar.message.locate_object_not_found', { object: objectLabel, name: request.tableName, })); return; } const targetKey = path[path.length - 1]; const targetNode = findTreeNodeByKey(treeDataRef.current, targetKey); setSearchValue(''); mergeExpandedTreeKeys(path.slice(0, -1)); setSelectedKeys([targetKey]); selectedNodesRef.current = targetNode ? [targetNode] : []; setActiveContext({ connectionId: request.connectionId, dbName: request.dbName }); scrollSidebarTreeToKey(targetKey); }; const handleLocateActiveTabInSidebar = () => { if (!activeTabLocateRequest) { message.warning(t('sidebar.message.locate_current_table_unavailable')); return; } void locateObjectInSidebar(activeTabLocateRequest); }; useEffect(() => { locateObjectInSidebarRef.current = locateObjectInSidebar; }); useEffect(() => { const handleLocateSidebarObject = (event: Event) => { void locateObjectInSidebarRef.current((event as CustomEvent).detail); }; window.addEventListener('gonavi:locate-sidebar-object', handleLocateSidebarObject as EventListener); return () => { window.removeEventListener('gonavi:locate-sidebar-object', handleLocateSidebarObject as EventListener); }; }, []); useEffect(() => { const handleSidebarTablePinChanged = (event: Event) => { const detail = (event as CustomEvent).detail || {}; const connectionId = String(detail.connectionId || '').trim(); const dbName = String(detail.dbName || '').trim(); if (!connectionId || !dbName) return; const dbNode = findTreeNodeByKeyRef.current(treeDataRef.current, `${connectionId}-${dbName}`); if (dbNode) { void loadTables(dbNode); } }; window.addEventListener('gonavi:sidebar-table-pin-changed', handleSidebarTablePinChanged as EventListener); return () => { window.removeEventListener('gonavi:sidebar-table-pin-changed', handleSidebarTablePinChanged as EventListener); }; }, []); const onLoadData = async ({ key, children, dataRef, type }: any) => { if (type === 'tag' || type === 'all-saved-queries' || type === 'saved-query-group' || type === 'unmatched-saved-queries') return; if (hasSidebarLazyChildren(children)) return; if (type === 'connection') { await loadDatabases({ key, dataRef }); } else if (type === 'jvm-mode' || type === 'jvm-resource') { await loadJVMResources({ key, dataRef }); } else if (type === 'database') { await loadTables({ key, dataRef }); } else if (type === 'external-sql-root') { await refreshGlobalExternalSQLRootNode(false); } else if (type === 'table') { // Expand table to show object categories const conn = dataRef; const folders: TreeNode[] = [ { title: t('sidebar.table_folder.columns'), key: `${key}-columns`, icon: , type: 'folder-columns', isLeaf: true, dataRef: conn }, { title: t('sidebar.table_folder.indexes'), key: `${key}-indexes`, icon: , type: 'folder-indexes', isLeaf: true, dataRef: conn }, { title: t('sidebar.table_folder.foreign_keys'), key: `${key}-fks`, icon: , type: 'folder-fks', isLeaf: true, dataRef: conn }, { title: t('sidebar.table_folder.triggers'), key: `${key}-triggers`, icon: , type: 'folder-triggers', isLeaf: true, dataRef: conn } ]; replaceTreeNodeChildren(key, folders); } }; const isStructureOnlyDbType = (connectionId: string): boolean => { const conn = connections.find(c => c.id === connectionId); if (!conn) return false; const dbType = resolveDataSourceType(conn.config); return dbType === 'elasticsearch' || dbType === 'mongodb' || dbType === 'redis' || dbType === 'iotdb'; }; const openDesign = (node: any, initialTab: string, readOnly: boolean = false) => { const { tableName, dbName, id } = node.dataRef; const forceReadOnly = readOnly || isStructureOnlyDbType(id); addTab({ id: `design-${id}-${dbName}-${tableName}`, title: forceReadOnly ? t('sidebar.tab.table_structure', { table: tableName }) : t('sidebar.tab.design_table', { table: tableName }), type: 'design', connectionId: id, dbName: dbName, tableName: tableName, initialTab: initialTab, readOnly: forceReadOnly }); }; const openNewTableDesign = (node: any) => { const { dbName, id } = node.dataRef; if (isStructureOnlyDbType(id)) { message.warning(t('sidebar.message.visual_new_table_unsupported')); return; } addTab({ id: `new-table-${id}-${dbName}-${Date.now()}`, title: t('sidebar.tab.new_table', { database: dbName }), type: 'design', connectionId: id, dbName: dbName, tableName: '', // Empty tableName signals creation mode initialTab: 'columns', readOnly: false }); }; const onSelect = (keys: React.Key[], info: any) => { if (isV2Ui && info?.node?.type === 'v2-table-section') { return; } if (Date.now() < treeDragSelectSuppressUntilRef.current) { return; } if (isTreeDragging) { return; } setSelectedKeys(keys); selectedNodesRef.current = info.selectedNodes || []; if (keys.length === 0) { if (shouldClearSidebarActiveContextOnEmptySelect(isV2Ui)) { setActiveContext(null); } return; } if (shouldSkipSidebarSelectWhileDragging(isTreeDragging, info)) return; const { type, dataRef, key, title } = info.node; const nodeConnectionId = resolveSidebarNodeConnectionId(info.node, connectionIds); // Update active context if (type === 'connection') { setActiveContext({ connectionId: key, dbName: '' }); } else if (type === 'database') { setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName }); } else if (type === 'table') { setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName }); } else if (type === 'jvm-mode' || type === 'jvm-resource' || type === 'jvm-diagnostic' || type === 'jvm-monitoring') { setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: '' }); } else if (type === 'view' || type === 'materialized-view' || type === 'db-trigger' || type === 'db-event' || type === 'routine') { setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName }); } else if (type === 'saved-query') { setActiveContext({ connectionId: dataRef.connectionId, dbName: dataRef.dbName }); } else if (type === 'redis-db') { setActiveContext({ connectionId: dataRef.id, dbName: `db${dataRef.redisDB}` }); } if (type === 'folder-columns') openDesign(info.node, 'columns', false); else if (type === 'folder-indexes') openDesign(info.node, 'indexes', false); else if (type === 'folder-fks') openDesign(info.node, 'foreignKeys', false); else if (type === 'folder-triggers') openDesign(info.node, 'triggers', false); else if (type === 'object-group' && dataRef?.groupKey === 'tables') { // 单击延迟打开表概览,双击时会取消此定时器 if (clickTimerRef.current) clearTimeout(clickTimerRef.current); const { id, dbName: gDbName, schemaName } = dataRef; clickTimerRef.current = setTimeout(() => { clickTimerRef.current = null; addTab({ id: `table-overview-${id}-${gDbName}${schemaName ? `-${schemaName}` : ''}`, title: t('sidebar.tab.table_overview', { database: gDbName, schema: schemaName ? ` (${schemaName})` : '', }), type: 'table-overview' as any, connectionId: id, dbName: gDbName, schemaName, } as any); }, 250); } }; const onExpand = (newExpandedKeys: React.Key[], info?: any) => { setExpandedKeys(newExpandedKeys); setAutoExpandParent(false); if (!shouldSkipSidebarLoadOnExpandWhileDragging(isTreeDragging, info)) { void onLoadData(info.node); } }; const onDoubleClick = (e: any, node: any) => { // 双击时取消单击延迟动作(如表概览打开),让双击只触发展开/折叠 if (clickTimerRef.current) { clearTimeout(clickTimerRef.current); clickTimerRef.current = null; } const { type, dataRef, key: nodeKey } = node; if (isV2Ui && type === 'v2-table-section') { return; } const nodeConnectionId = resolveSidebarNodeConnectionId(node, connectionIds); if (type === 'connection') { setSelectedKeys([nodeKey]); selectedNodesRef.current = [node]; setActiveContext({ connectionId: nodeKey, dbName: '' }); } else if (type === 'database') { setSelectedKeys([nodeKey]); selectedNodesRef.current = [node]; setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName }); } else if (type === 'jvm-mode' || type === 'jvm-resource' || type === 'jvm-diagnostic' || type === 'jvm-monitoring') { setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: '' }); } else if (type === 'table' || type === 'view' || type === 'materialized-view' || type === 'db-trigger' || type === 'db-event' || type === 'routine') { setActiveContext({ connectionId: nodeConnectionId || dataRef.id, dbName: dataRef.dbName }); } else if (type === 'saved-query') setActiveContext({ connectionId: dataRef.connectionId, dbName: dataRef.dbName }); else if (type === 'redis-db') setActiveContext({ connectionId: dataRef.id, dbName: `db${dataRef.redisDB}` }); if (node.type === 'table') { const { tableName, dbName, id } = node.dataRef; // 记录表访问 recordTableAccess(id, dbName, tableName); addTab({ id: node.key, title: tableName, type: 'table', connectionId: id, dbName, tableName, objectType: 'table', }); return; } else if (node.type === 'view' || node.type === 'materialized-view') { const { viewName, dbName, id, schemaName } = node.dataRef; addTab({ id: node.key, title: viewName, type: 'table', connectionId: id, dbName, tableName: viewName, objectType: node.type === 'materialized-view' ? 'materialized-view' : 'view', schemaName, sidebarLocateKey: String(node.key || ''), }); return; } else if (node.type === 'saved-query') { const q = node.dataRef; addTab({ id: q.id, title: resolveSavedQueryDisplayName(q.name), type: 'query', connectionId: q.connectionId, dbName: q.dbName, query: q.sql, savedQueryId: q.id, }); return; } else if (node.type === 'external-sql-file') { void openExternalSQLFile(node); return; } else if (node.type === 'redis-db') { const { id, redisDB } = node.dataRef; addTab({ id: `redis-keys-${id}-db${redisDB}`, title: `db${redisDB}`, type: 'redis-keys', connectionId: id, redisDB: redisDB }); return; } else if (node.type === 'db-trigger') { const { triggerName, triggerTableName, schemaName, dbName, id } = node.dataRef; addTab({ id: `trigger-${node.key}`, title: t('sidebar.tab.trigger', { name: triggerName }), type: 'trigger', connectionId: id, dbName, triggerName, triggerTableName, schemaName, sidebarLocateKey: String(node.key || ''), }); return; } else if (node.type === 'db-event') { openEventDefinition(node); return; } else if (node.type === 'routine') { const { routineName, routineType, dbName, id } = node.dataRef; const typeLabel = t(routineType === 'PROCEDURE' ? 'sidebar.object.procedure' : 'sidebar.object.function'); addTab({ id: `routine-def-${node.key}`, title: t('sidebar.tab.routine_definition', { type: typeLabel, name: routineName }), type: 'routine-def', connectionId: id, dbName, routineName, routineType }); return; } else if (node.type === 'jvm-mode') { const { providerMode, id } = node.dataRef; const conn = (connections.find((item) => item.id === id) || node.dataRef) as SavedConnection; openJVMOverviewTab(conn, providerMode); return; } else if (node.type === 'jvm-resource') { const { providerMode, resourcePath, resourceKind, id } = node.dataRef; const conn = (connections.find((item) => item.id === id) || node.dataRef) as SavedConnection; openJVMResourceTab(conn, providerMode, resourcePath, resourceKind); return; } else if (node.type === 'jvm-monitoring') { const { providerMode, id } = node.dataRef; const conn = (connections.find((item) => item.id === id) || node.dataRef) as SavedConnection; openJVMMonitoringTab(conn, providerMode); return; } else if (node.type === 'jvm-diagnostic') { const conn = (connections.find((item) => item.id === node.dataRef.id) || node.dataRef) as SavedConnection; openJVMDiagnosticTab(conn); return; } const key = node.key; const isExpanded = expandedKeys.includes(key); const newExpandedKeys = isExpanded ? expandedKeys.filter(k => k !== key) : [...expandedKeys, key]; setExpandedKeys(newExpandedKeys); if (!isExpanded) { setAutoExpandParent(false); if (shouldLoadSidebarNodeOnExpand(node)) { void onLoadData(node); } } }; const buildRuntimeConfig = (conn: any, overrideDatabase?: string, clearDatabase: boolean = false) => { return buildRpcConnectionConfig(conn.config, { database: resolveSidebarRuntimeDatabase( conn?.config?.type, conn?.config?.driver, conn?.config?.database, overrideDatabase, clearDatabase, conn?.config?.oceanBaseProtocol, ), }); }; const buildJVMRuntimeConfig = (conn: SavedConnection & { dbName?: string }, providerMode: string) => { const sourceJVM = conn.config.jvm || {}; return buildRpcConnectionConfig(conn.config, { database: '', jvm: { ...sourceJVM, preferredMode: providerMode as 'jmx' | 'endpoint' | 'agent', allowedModes: [providerMode as 'jmx' | 'endpoint' | 'agent'], }, }); }; const openJVMOverviewTab = (conn: SavedConnection, providerMode: string) => { addTab({ id: `jvm-overview-${conn.id}-${providerMode}`, title: buildJVMTabTitle(conn.name, 'overview', providerMode), type: 'jvm-overview', connectionId: conn.id, providerMode: providerMode as 'jmx' | 'endpoint' | 'agent', }); }; const openJVMMonitoringTab = (conn: SavedConnection, providerMode: string) => { addTab({ id: `jvm-monitoring-${conn.id}-${providerMode}`, title: buildJVMTabTitle(conn.name, 'monitoring', providerMode), type: 'jvm-monitoring', connectionId: conn.id, providerMode: providerMode as 'jmx' | 'endpoint' | 'agent', }); }; const buildJVMDiagnosticTreeNodes = (conn: SavedConnection): TreeNode[] => { const descriptor = buildJVMDiagnosticActionDescriptor(conn.id, conn.config.jvm?.diagnostic); if (!descriptor) { return []; } return [{ title: descriptor.title, key: descriptor.key, icon: , type: 'jvm-diagnostic', dataRef: { ...conn, diagnosticTransport: descriptor.transport, }, isLeaf: true, }]; }; const openJVMResourceTab = (conn: SavedConnection, providerMode: string, resourcePath: string, resourceKind?: string) => { const trimmedResourcePath = String(resourcePath || '').trim(); addTab({ id: `jvm-resource-${conn.id}-${providerMode}-${encodeURIComponent(trimmedResourcePath)}`, title: trimmedResourcePath ? `${buildJVMTabTitle(conn.name, 'resource', providerMode)} · ${trimmedResourcePath}` : buildJVMTabTitle(conn.name, 'resource', providerMode), type: 'jvm-resource', connectionId: conn.id, providerMode: providerMode as 'jmx' | 'endpoint' | 'agent', resourcePath: trimmedResourcePath, resourceKind, }); }; const openJVMDiagnosticTab = (conn: SavedConnection) => { const transport = conn.config.jvm?.diagnostic?.transport || 'agent-bridge'; addTab({ id: `jvm-diagnostic-${conn.id}`, title: buildJVMTabTitle(conn.name, 'diagnostic', transport), type: 'jvm-diagnostic', connectionId: conn.id, }); }; const getConnectionNodeRef = (connRef: any) => { const latestConn = connections.find(c => c.id === connRef.id); return { key: connRef.id, dataRef: latestConn || connRef }; }; const getDatabaseNodeRef = (connRef: any, dbName: string) => { const latestConn = connections.find(c => c.id === connRef.id); return { key: `${connRef.id}-${dbName}`, dataRef: { ...(latestConn || connRef), dbName } }; }; const extractObjectName = (fullName: string) => { return splitQualifiedName(String(fullName || '').trim()).objectName || String(fullName || '').trim(); }; const resolveSavedQueryDisplayName = (name: string | null | undefined) => { const rawName = String(name || '').trim(); return rawName || t('query_editor.save_modal.unnamed'); }; const { loadDatabases, loadJVMResources, loadTables, } = useSidebarTreeLoaders({ savedQueries, externalSQLDirectories, tableSortPreference, tableAccessCount, pinnedSidebarTables, isV2Ui, loadingNodesRef, setConnectionStates, setLoadedKeys, replaceTreeNodeChildren, buildRuntimeConfig, buildJVMRuntimeConfig, buildJVMDiagnosticTreeNodes, resolveSavedQueryDisplayName, decorateExternalSQLTreeNode, }); const { handleCopyStructure, handleCopyTableName, handleExport, openExportDialog, handleCopyTableAsInsert, openTableDdlInDesigner, openTableInERView, injectTablePromptToAI, handleCreateDatabase, openCreateSchemaModal, handleCreateSchema, openRenameSchemaModal, handleRenameSchema, handleDeleteSchema, handleRenameDatabase, handleDeleteDatabase, handleRenameTable, handleDeleteTable, handleTableDataDangerAction, openViewDefinition, openEditView, openCreateView, openCreateStarRocksMaterializedView, openCreateStarRocksExternalCatalog, openCreateStarRocksRollup, handleDropView, handleRenameView, openRenameSavedQueryModal, handleRenameSavedQuery, isSavedQueryUnmatched, handleRebindSavedQuery, openRoutineDefinition, openEventDefinition, openEditRoutine, openCreateRoutine, handleDropRoutine, resolveMessagePublishTarget, openMessagePublishModal, handleMessagePublishSuccess, } = useSidebarObjectActions({ connections, connectionIds, connectionIdSet, tabs, treeDataRef, setTreeData, setExpandedKeys, setLoadedKeys, addTab, updateQueryTabDraft, saveQuery, addSqlLog, closeTabsByDatabase, createDbForm, targetConnection, setIsCreateDbModalOpen, createSchemaForm, createSchemaTarget, setCreateSchemaTarget, setIsCreateSchemaModalOpen, renameSchemaForm, renameSchemaTarget, setRenameSchemaTarget, setIsRenameSchemaModalOpen, renameDbForm, renameDbTarget, setRenameDbTarget, setIsRenameDbModalOpen, renameTableForm, renameTableTarget, setRenameTableTarget, setIsRenameTableModalOpen, renameViewForm, renameViewTarget, setRenameViewTarget, setIsRenameViewModalOpen, renameSavedQueryForm, renameSavedQueryTarget, setRenameSavedQueryTarget, setIsRenameSavedQueryModalOpen, setMessagePublishTarget, buildRuntimeConfig, getConnectionNodeRef, getDatabaseNodeRef, extractObjectName, isPostgresSchemaDialect, loadDatabases, loadTables, openDesign, onDoubleClick, runExportWithProgress, setAIPanelVisible, addAIContext, }); const refreshV2TableContextMenuStatsRef = useRef<(node: any) => void>(() => {}); const { getConnectionNodeForAction, toggleSidebarTablePinned, handleV2TableContextMenuAction, handleTableGroupSortAction, handleV2TableGroupContextMenuAction, handleV2DatabaseContextMenuAction, disconnectConnectionNode, deleteConnectionNode, handleV2ConnectionContextMenuAction, handleV2ConnectionGroupContextMenuAction, } = useSidebarV2ActionHandlers({ connections, connectionTags, pinnedSidebarTables, loadingNodesRef, treeDataRef, findTreeNodeByKeyRef, refreshV2TableContextMenuStatsRef, setConnectionStates, setExpandedKeys, setLoadedKeys, setTargetConnection, setIsCreateDbModalOpen, setRenameDbTarget, setIsRenameDbModalOpen, setRenameTableTarget, setIsRenameTableModalOpen, setRenameViewTarget, setIsCreateTagModalOpen, renameDbForm, renameTableForm, createTagForm, addTab, closeTabsByDatabase, closeTabsByConnection, removeConnection, removeConnectionTag, moveConnectionToTag, setSidebarTablePinned, setTableSortPreference, replaceTreeNodeChildren, loadDatabases, loadTables, getDatabaseNodeRef, extractObjectName, openDesign, openNewTableDesign, onDoubleClick, openMessagePublishModal, openTableDdlInDesigner, openTableInERView, handleCopyTableName, handleCopyStructure, handleCopyTableAsInsert, openCreateStarRocksRollup, handleExport, openExportDialog, injectTablePromptToAI, handleTableDataDangerAction, handleDeleteTable, openCreateSchemaModal, openCreateStarRocksMaterializedView, openCreateStarRocksExternalCatalog, handleExportDatabaseSQL, handleRunSQLFile, handleDeleteDatabase, onEditConnection, handleDuplicateConnection, buildConnectionRootQueryTabTitle, buildConnectionRootRedisCommandTabTitle, buildConnectionRootRedisMonitorTabTitle, }); const { onSearch, searchScopeSummary, searchScopePopoverContent, displayTreeData, v2CommandSearchObjectMode, v2CommandSearchAiMode, filteredCommandSearchTreeItems, filteredCommandSearchActionItems, filteredCommandSearchRecentItems, commandSearchAiItem, commandSearchFlatItems, flattenConnectionNodes, activeConnection, activeConnectionDisplayName, activeDatabaseDisplayName, v2VisibleTreeData, v2TreeHorizontalScrollWidth, effectiveTreeHeight, v2TreeMetrics, activeConnectionObjectCount, } = useSidebarSearchModel({ searchScopes, setSearchScopes, setSearchValue, deferredSearchValue, deferredV2CommandSearchValue, v2CommandSearchValue, setV2CommandActiveIndex, v2ExplorerFilter, treeData, treeViewportWidth, treeHeight, isV2Ui, connections, connectionIds, selectedKeys, selectedNodesRef, activeContext, activeTab, sqlLogs, shortcutOptions, activeShortcutPlatform, overlayTheme, darkMode, onCreateConnection, onToggleAI, onToggleLogPanel, setAIPanelVisible, extractObjectName, }); const legacyToolbarButtonColor = darkMode ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.65)'; const legacyToolbarStyle: React.CSSProperties = { padding: '6px 16px', display: 'grid', gridTemplateColumns: 'repeat(5, minmax(0, 1fr))', gap: 8, alignItems: 'center', justifyItems: 'center', borderTop: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}`, borderBottom: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}`, background: darkMode ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.015)', }; const legacyToolbarItemStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', justifyContent: 'center', minWidth: 0, }; const legacyToolbarDisabledWrapStyle: React.CSSProperties = { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', }; 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, }); refreshV2TableContextMenuStatsRef.current = refreshV2TableContextMenuStats; const renderV2TreeTitle = (node: any, hoverTitle: string, statusBadge: React.ReactNode) => renderSidebarV2TreeTitle({ node, hoverTitle, statusBadge, getV2TreeMetaText, toggleSidebarTablePinned, snapshotTreeSelectionBeforeDrag, restoreTreeSelectionAfterDrag, treeDragSelectSuppressUntilRef, setIsTreeDragging, }); const { selectConnectionFromRail, runCommandSearchItem, handleV2CommandSearchKeyDown, } = useSidebarCommandSearchRunner({ activeContext, activeTab, addTab, closeV2CommandSearch, commandSearchFlatItems, connectionIds, findTreeNodeByKeyRef, locateObjectInSidebar, loadDatabases, mergeExpandedTreeKeys, onDoubleClick, scrollSidebarTreeToKey, selectedNodesRef, setActiveContext, setSelectedKeys, setV2CommandActiveIndex, treeDataRef, v2CommandActiveIndex, }); expandConnectionFromRailRef.current = (connectionId: string) => { const conn = connections.find((item) => item.id === connectionId); if (conn) { selectConnectionFromRail(conn); } }; const getNodeMenuItems = (node: any): MenuProps['items'] => buildSidebarLegacyNodeMenuItems(node, { addTab, getMetadataDialect, handleV2DatabaseContextMenuAction, isPostgresSchemaDialect, handleExportSchemaSQL, openRenameSchemaModal, loadTables, getDatabaseNodeRef, handleDeleteSchema, tableSortPreference, isStructureOnlyDbType, openNewTableDesign, handleTableGroupSortAction, openCreateView, openCreateStarRocksMaterializedView, openCreateRoutine, createTagForm, setRenameViewTarget, setIsCreateTagModalOpen, removeConnectionTag, setExpandedKeys, setLoadedKeys, loadingNodesRef, loadDatabases, buildConnectionRootRedisCommandTabTitle, buildConnectionRootRedisMonitorTabTitle, onEditConnection, handleDuplicateConnection, disconnectConnectionNode, deleteConnectionNode, connectionTags, moveConnectionToTag, setTargetConnection, setIsCreateDbModalOpen, buildConnectionRootQueryTabTitle, handleRunSQLFile, openCreateStarRocksExternalCatalog, openEditView, renameViewForm, setIsRenameViewModalOpen, handleDropView, onDoubleClick, openViewDefinition, openRoutineDefinition, openEditRoutine, handleDropRoutine, openEventDefinition, resolveMessagePublishTarget, openMessagePublishModal, openDesign, openCreateStarRocksRollup, handleCopyTableName, handleCopyStructure, handleExport, setRenameTableTarget, renameTableForm, setIsRenameTableModalOpen, handleTableDataDangerAction, handleDeleteTable, openExportDialog, isSavedQueryUnmatched, connections, handleRebindSavedQuery, openRenameSavedQueryModal, resolveSavedQueryDisplayName, deleteQuery, treeDataRef, setTreeData, handleAddExternalSQLDirectory, openCreateExternalSQLFileModal, openCreateExternalSQLDirectoryModal, openRenameExternalSQLDirectoryModal, handleRefreshExternalSQLDirectory, handleDeleteExternalSQLDirectory, handleRemoveExternalSQLDirectory, openExternalSQLFile, openRenameExternalSQLFileModal, handleDeleteExternalSQLFile, extractObjectName, }); const titleRender = useSidebarTitleRender({ connectionStates, isV2Ui, renderV2TreeTitle, handleAddExternalSQLDirectory, snapshotTreeSelectionBeforeDrag, restoreTreeSelectionAfterDrag, treeDragSelectSuppressUntilRef, setIsTreeDragging, }); const handleDrop = (info: any) => { setIsTreeDragging(false); const dropPosition = normalizeSidebarTreeRelativeDropPosition( Number(info.dropPosition || 0), info?.node?.pos, ); const domDropNode = resolveSidebarDropNodeFromDomEvent(info?.event); const dropTargetMetrics = resolveSidebarDropTargetMetricsFromDomEvent(info?.event); const insertBefore = resolveSidebarDropInsertBefore(dropPosition, dropTargetMetrics ? { clientY: info?.event?.clientY, top: dropTargetMetrics.top, height: dropTargetMetrics.height, } : null); const dragNode = info.dragNode; const dropNode = domDropNode && domDropNode.key === String(info?.node?.key || '') ? info.node : (domDropNode ? findTreeNodeByKeyRef.current(treeDataRef.current, domDropNode.key) || info.node : info.node); const getDropRootToken = (node: any): string => { if (!node) return ''; if (node.type === 'tag') { return buildSidebarRootTagToken(String(node?.dataRef?.id || '')); } if (node.type === 'connection') { const groupedTagId = connectionTags.find((tag) => tag.connectionIds.includes(String(node.key)), )?.id || ''; return groupedTagId ? buildSidebarRootTagToken(groupedTagId) : buildSidebarRootConnectionToken(String(node.key)); } return ''; }; // Root tag or ungrouped connection reordering if (dragNode.type === 'tag') { if (dropNode.type === 'tag' || dropNode.type === 'connection') { const currentTagOrder = connectionTags.map(t => t.id); const dragTagId = dragNode.dataRef.id; const dropTagId = dropNode.type === 'tag' ? dropNode.dataRef.id : (connectionTags.find(t => t.connectionIds.includes(String(dropNode.key)))?.id || ''); const dragRootToken = buildSidebarRootTagToken(String(dragTagId)); const dropRootToken = getDropRootToken(dropNode); if (dropRootToken && dropRootToken !== dragRootToken) { if (dropTagId) { const resolvedInsertBefore = resolveSidebarTagDropInsertBefore({ currentTagOrder, dragTagId, dropTagId, relativeDropPosition: dropPosition, fallbackInsertBefore: insertBefore, metrics: dropTargetMetrics ? { clientY: info?.event?.clientY, top: dropTargetMetrics.top, height: dropTargetMetrics.height, } : null, }); reorderSidebarRoot(dragRootToken, dropRootToken, resolvedInsertBefore); } else { reorderSidebarRoot(dragRootToken, dropRootToken, insertBefore); } return; } const newOrder = currentTagOrder.filter(id => id !== dragTagId); let insertIndex = newOrder.length; if (dropTagId) { const dropIndex = newOrder.indexOf(dropTagId); const resolvedInsertBefore = resolveSidebarTagDropInsertBefore({ currentTagOrder, dragTagId, dropTagId, relativeDropPosition: dropPosition, fallbackInsertBefore: insertBefore, metrics: dropTargetMetrics ? { clientY: info?.event?.clientY, top: dropTargetMetrics.top, height: dropTargetMetrics.height, } : null, }); if (resolvedInsertBefore) { insertIndex = dropIndex; } else { insertIndex = dropIndex + 1; } } else { // Dropped onto an ungrouped root connection, usually meaning moving to the end of tags // Since tags are always displayed before ungrouped connections, just put it at the end insertIndex = newOrder.length; } newOrder.splice(insertIndex, 0, dragTagId); reorderTags(newOrder); } return; } if (dragNode.type === 'connection') { const dragTagId = connectionTags.find((tag) => tag.connectionIds.includes(String(dragNode.key)), )?.id || ''; const dragIsUngroupedRoot = !dragTagId; const dropRootToken = getDropRootToken(dropNode); if (dragIsUngroupedRoot && dropNode.type === 'connection' && dropRootToken) { reorderSidebarRoot( buildSidebarRootConnectionToken(String(dragNode.key)), dropRootToken, insertBefore, ); return; } } // Connection moving to tag (any drop position on a tag node counts as "into") if (dragNode.type === 'connection' && dropNode.type === 'tag') { moveConnectionToTag(dragNode.key, dropNode.dataRef.id); return; } // Connection reordering against another connection if (dragNode.type === 'connection' && dropNode.type === 'connection') { const targetTag = connectionTags.find(t => t.connectionIds.includes(dropNode.key)); reorderConnections( String(dragNode.key), String(dropNode.key), targetTag?.id || null, insertBefore, ); return; } }; const onRightClick = ({ event, node }: any) => { if (isV2Ui && node?.type === 'v2-table-section') { event.preventDefault(); event.stopPropagation(); return; } if (isV2Ui && node?.type === 'connection') { openV2ConnectionContextMenu(event, node); return; } if (isV2Ui && node?.type === 'database') { const position = resolveSidebarContextMenuPosition(event.clientX, event.clientY); setContextMenu({ x: position.x, y: position.y, sourceX: event.clientX, sourceY: event.clientY, items: [], kind: 'v2-database', node, rootClassName: 'gn-v2-table-context-menu-popup', overlayStyle: { width: 264, maxWidth: 'calc(100vw - 24px)' }, maxHeight: position.maxHeight, }); return; } if ( isV2Ui && node?.type === 'object-group' && node?.dataRef?.groupKey === 'schema' && isPostgresSchemaDialect(getMetadataDialect(node.dataRef as SavedConnection)) && String(node?.dataRef?.schemaName || '').trim() ) { const position = resolveSidebarContextMenuPosition(event.clientX, event.clientY); setContextMenu({ x: position.x, y: position.y, sourceX: event.clientX, sourceY: event.clientY, items: [], kind: 'v2-schema', node, rootClassName: 'gn-v2-table-context-menu-popup', overlayStyle: { width: 264, maxWidth: 'calc(100vw - 24px)' }, maxHeight: position.maxHeight, }); return; } if (isV2Ui && node?.type === 'object-group' && node?.dataRef?.groupKey === 'tables') { const position = resolveSidebarContextMenuPosition(event.clientX, event.clientY); setContextMenu({ x: position.x, y: position.y, sourceX: event.clientX, sourceY: event.clientY, items: [], kind: 'v2-table-group', node, rootClassName: 'gn-v2-table-context-menu-popup', overlayStyle: { width: 264, maxWidth: 'calc(100vw - 24px)' }, maxHeight: position.maxHeight, }); return; } if (isV2Ui && node?.type === 'table') { const position = resolveSidebarContextMenuPosition(event.clientX, event.clientY); setContextMenu({ x: position.x, y: position.y, sourceX: event.clientX, sourceY: event.clientY, items: [], kind: 'v2-table', node, rootClassName: 'gn-v2-table-context-menu-popup', overlayStyle: { width: 264, maxWidth: 'calc(100vw - 24px)' }, maxHeight: position.maxHeight, }); return; } const items = getNodeMenuItems(node); if (items && items.length > 0) { setContextMenu({ x: event.clientX, y: event.clientY, items }); } }; const v2RailObjectActionsLabel = t('sidebar.rail.object_actions'); const v2RailSystemActionsLabel = t('sidebar.rail.system_actions'); const v2NewGroupLabel = t('sidebar.action.new_group'); const v2BatchTablesLabel = t('sidebar.action.batch_tables'); const v2BatchDatabasesLabel = t('sidebar.action.batch_databases'); const v2OpenExternalSqlFileLabel = t('sidebar.sql_file_exec.title'); const v2LocateCurrentTableLabel = t('sidebar.action.locate_current_table'); const v2LocateCurrentTableUnavailableLabel = t('sidebar.message.locate_current_table_unavailable'); const v2AiAssistantLabel = t('app.sidebar.ai_assistant'); const v2ToolsLabel = t('app.sidebar.tools'); const v2SettingsLabel = t('app.sidebar.settings'); const v2ActiveConnectionHeaderLabel = t('sidebar.active_connection.current_host_database'); const v2NoDatabaseSelectedLabel = t('sidebar.active_connection.no_database_selected'); const v2ConnectionActionsLabel = t('sidebar.active_connection.actions'); const v2CommandSearchLabel = t('sidebar.command_search.label'); const v2CommandSearchPlaceholder = t('sidebar.command_search.placeholder'); const v2CommandSearchPanelProps: SidebarSearchPanelProps = { isOpen: isV2CommandSearchOpen, searchValue: v2CommandSearchValue, activeIndex: v2CommandActiveIndex, label: v2CommandSearchLabel, placeholder: v2CommandSearchPlaceholder, persistedFilter: v2PersistedSidebarFilter, persistentFilterEnabled: v2CommandSearchPersistentFilterEnabled, aiMode: v2CommandSearchAiMode, objectMode: v2CommandSearchObjectMode, flatItems: commandSearchFlatItems, sections: { goTo: filteredCommandSearchTreeItems, ai: commandSearchAiItem, actions: filteredCommandSearchActionItems, recent: filteredCommandSearchRecentItems, }, inputRef: commandSearchInputRef, handlers: { onSearchValueChange: handleV2CommandSearchValueChange, onKeyDown: handleV2CommandSearchKeyDown, onClose: closeV2CommandSearch, onItemSelect: (item: V2CommandSearchItem) => runCommandSearchItem(item), onItemHover: (key: string) => setV2CommandActiveIndex(commandSearchFlatItems.findIndex((entry) => entry.key === key)), onTogglePersistentFilter: toggleV2CommandSearchPersistentFilter, onResetFilter: resetV2SidebarFilter, }, }; // V2 Connection Rail 子组件 props(从原 renderV2ConnectionRail 抽出,保留所有原行为) const v2ConnectionRailProps = { labels: { railSystemActions: v2RailSystemActionsLabel, railObjectActions: v2RailObjectActionsLabel, newGroup: v2NewGroupLabel, batchTables: v2BatchTablesLabel, batchDatabases: v2BatchDatabasesLabel, openExternalSqlFile: v2OpenExternalSqlFileLabel, locateCurrentTable: v2LocateCurrentTableLabel, locateCurrentTableUnavailable: v2LocateCurrentTableUnavailableLabel, aiAssistant: v2AiAssistantLabel, tools: v2ToolsLabel, settings: v2SettingsLabel, }, handlers: { openCreateTagModal: () => { setRenameViewTarget(null); createTagForm.resetFields(); setIsCreateTagModalOpen(true); }, openBatchTableExport: () => openBatchTableExportWorkbench(), openBatchDatabaseExport: () => openBatchDatabaseExportWorkbench(), openExternalSqlFile: handleOpenSQLFileFromToolbar, locateActiveTab: handleLocateActiveTabInSidebar, toggleAI: onToggleAI ?? (() => {}), openTools: onOpenTools ?? (() => {}), openSettings: onOpenSettings ?? (() => {}), }, canLocateActiveTab, }; return (
{exportProgressModal} {isV2Ui && }
{isV2Ui && (
{activeConnectionDisplayName} {activeDatabaseDisplayName || v2NoDatabaseSelectedLabel}
{onCreateConnection && (
)}
{isV2Ui && !v2UseLegacySidebarFilter ? (
) : isV2Ui ? (
} />
) : ( } style={{ borderRadius: 6, border: 'none', background: darkMode ? 'rgba(0,0,0,0.25)' : 'rgba(0,0,0,0.03)', boxShadow: 'none', padding: '4px 8px', color: darkMode ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.85)', }} suffix={(
{ if (!isSearchScopePopoverOpen) { e.currentTarget.style.background = darkMode ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.04)'; e.currentTarget.style.color = darkMode ? 'rgba(255,255,255,0.85)' : 'rgba(0,0,0,0.65)'; } }} onMouseLeave={(e) => { if (!isSearchScopePopoverOpen) { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = searchScopes.includes('smart') ? (darkMode ? '#ffd666' : '#1677ff') : (darkMode ? 'rgba(255,255,255,0.45)' : 'rgba(0,0,0,0.45)'); } }} > {searchScopes.includes('smart') ? t('sidebar.command_search.scope.compact_smart') : searchScopes.length}
)} /> )}
{isV2Ui && (
{V2_EXPLORER_FILTER_OPTIONS.map((item) => ( ))}
)} {/* Toolbar */} {!isV2Ui && (
)}
node.type === 'connection' || node.type === 'tag' }} onDragStart={() => { snapshotTreeSelectionBeforeDrag(); treeDragSelectSuppressUntilRef.current = Date.now() + 600; setIsTreeDragging(true); }} onDragEnter={() => { treeDragSelectSuppressUntilRef.current = Date.now() + 600; setIsTreeDragging(true); }} onDragEnd={() => { restoreTreeSelectionAfterDrag(); setIsTreeDragging(false); }} onDrop={handleDrop} loadData={onLoadData} treeData={isV2Ui ? v2VisibleTreeData : displayTreeData} onDoubleClick={onDoubleClick} onSelect={onSelect} titleRender={titleRender} expandedKeys={expandedKeys} onExpand={onExpand} loadedKeys={loadedKeys} onLoad={setLoadedKeys} autoExpandParent={autoExpandParent} selectedKeys={selectedKeys} blockNode height={effectiveTreeHeight} scrollWidth={isV2Ui ? v2TreeHorizontalScrollWidth : undefined} onRightClick={onRightClick} />
{isV2Ui && (
)}
{contextMenu?.kind && typeof document !== 'undefined' && createPortal(
event.stopPropagation()} onClick={(event) => event.stopPropagation()} onContextMenu={(event) => event.preventDefault()} > {renderV2SidebarContextMenuContent(contextMenu)}
, document.body, )} {contextMenu && !contextMenu.kind && ( { if (!open) setContextMenu(null); }} trigger={['contextMenu']} rootClassName={contextMenu.rootClassName} overlayStyle={contextMenu.overlayStyle} >
)} , "批量操作表", "按对象批量导出结构、数据或完整备份。")} databaseModalTitle={renderSidebarModalTitle(, "批量操作库", "按数据库批量导出结构,或生成结构加数据的备份。")} isBatchModalOpen={isBatchModalOpen} setIsBatchModalOpen={setIsBatchModalOpen} selectedConnection={selectedConnection} selectedDatabase={selectedDatabase} availableDatabases={availableDatabases} batchTables={batchTables} checkedTableKeys={checkedTableKeys} setCheckedTableKeys={setCheckedTableKeys} batchFilterKeyword={batchFilterKeyword} setBatchFilterKeyword={setBatchFilterKeyword} batchFilterType={batchFilterType} setBatchFilterType={setBatchFilterType} batchSelectionScope={batchSelectionScope} setBatchSelectionScope={setBatchSelectionScope} filteredBatchObjects={filteredBatchObjects} groupedBatchObjects={groupedBatchObjects} selectionScopeTargetKeys={selectionScopeTargetKeys} handleConnectionChange={handleConnectionChange} handleDatabaseChange={handleDatabaseChange} handleBatchClear={handleBatchClear} handleBatchExport={handleBatchExport} handleCheckAll={handleCheckAll} handleInvertSelection={handleInvertSelection} isBatchDbModalOpen={isBatchDbModalOpen} setIsBatchDbModalOpen={setIsBatchDbModalOpen} selectedDbConnection={selectedDbConnection} batchDatabases={batchDatabases} checkedDbKeys={checkedDbKeys} setCheckedDbKeys={setCheckedDbKeys} handleDbConnectionChange={handleDbConnectionChange} handleBatchDbExport={handleBatchDbExport} handleCheckAllDb={handleCheckAllDb} handleInvertSelectionDb={handleInvertSelectionDb} /> setFindInDbContext({ open: false, connectionId: '', dbName: '' })} connectionId={findInDbContext.connectionId} dbName={findInDbContext.dbName} /> setMessagePublishTarget(null)} onSuccess={handleMessagePublishSuccess} />
); }); export default Sidebar;