import SidebarConnectionRail from './sidebar/SidebarConnectionRail'; import Modal from './common/ResizableDraggableModal'; import SidebarSearchPanel, { type SidebarSearchPanelProps } from './sidebar/SidebarSearchPanel'; import SlowQueryRailButton from './sidebar/SlowQueryRailButton'; import SqlAuditRailButton from './sidebar/SqlAuditRailButton'; import { buildSidebarLegacyNodeMenuItems } from './sidebar/sidebarLegacyNodeMenu'; import { getMetadataDialect, loadSchemas, shouldHideSchemaPrefix, splitQualifiedName, } from './sidebar/sidebarMetadataLoaders'; import { useSidebarBatchExport, } from './sidebar/useSidebarBatchExport'; import { SidebarEntityModals } from './sidebar/SidebarEntityModals'; import { SavedQueryGroupModal } from './sidebar/SavedQueryGroupModal'; import { renderSidebarV2TreeTitle } from './sidebar/SidebarTreeTitle'; import { useSidebarV2ContextMenu, } from './sidebar/useSidebarV2ContextMenu'; import { useSidebarObjectActions, type SidebarMessagePublishTarget, } from './sidebar/useSidebarObjectActions'; import { useSidebarSearchModel } from './sidebar/useSidebarSearchModel'; import { useSidebarFilterPersistence } from './sidebar/useSidebarFilterPersistence'; 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, 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, resolveSidebarDatabaseNameForCopy, 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, Radio, Select, Tooltip } from 'antd'; import { APP_POPUP_Z_INDEX } from '../utils/overlayZIndex'; import { CaretDownFilled, DatabaseOutlined, TableOutlined, ConsoleSqlOutlined, HddOutlined, FolderOutlined, FolderOpenOutlined, FileTextOutlined, CopyOutlined, ExportOutlined, FolderAddOutlined, SaveOutlined, EditOutlined, SearchOutlined, KeyOutlined, ThunderboltOutlined, UnorderedListOutlined, FunctionOutlined, LinkOutlined, FileAddOutlined, ImportOutlined, PlusOutlined, ReloadOutlined, SendOutlined, DeleteOutlined, DisconnectOutlined, CheckSquareOutlined, FilterOutlined, DashboardOutlined, WarningOutlined, AimOutlined, MoreOutlined, MenuFoldOutlined, SettingOutlined } from '@ant-design/icons'; import { buildSidebarRootConnectionToken, buildSidebarRootTagToken, useStore, } from '../store'; import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme'; import { selectRecentSidebarSqlLogs, selectSidebarCommandSearchSqlLogs, } from './sidebar/sidebarSqlLogSelector'; import { SavedConnection, SavedQuery, SavedQueryGroup, ExternalSQLDirectory, ExternalSQLTreeEntry, SchemaVisibilityRule } 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 { useWorkbenchTabs } from '../hooks/useWorkbenchTabs'; import FindInDatabaseModal from './FindInDatabaseModal'; import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig'; import { resolveDataSourceType } from '../utils/dataSourceCapabilities'; import { isConnectionStructureEditRestricted } from '../utils/connectionReadOnly'; 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 { getSavedQueryGroupIdFromToken, getSavedQueryGroupOwnerIds, getSavedQueryIdFromGroupToken, isSavedQueryGroupQueryToken, isSavedQueryGroupToken, normalizeSavedQueryGroups, resolveSavedQueryGroupChildOrder, } from '../utils/savedQueryGroups'; import { getSchemaVisibilityRule, moveSchemaVisibilityRule, updateSchemaVisibilityRule, } from '../utils/schemaVisibility'; import { buildJVMTabTitle } from '../utils/jvmRuntimePresentation'; import { buildJVMDiagnosticActionDescriptor, buildJVMMonitoringActionDescriptors } from '../utils/jvmSidebarActions'; import { DATA_IMPORT_WORKBENCH_TAB_ID, resolveDataImportWorkbenchLaunchTab, } from '../utils/dataImportTab'; import { useExportProgressDialog } from './ExportProgressModal'; import { getShortcutPlatform, resolveShortcutDisplay } from '../utils/shortcuts'; import { buildExternalSQLRootNode, type ExternalSQLTreeNode } from '../utils/externalSqlTree'; import { resolveSidebarTableMetadataFields } from '../utils/sidebarTableMetadata'; import { filterSidebarTreeByHiddenObjectGroups } from '../utils/sidebarObjectVisibility'; 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, buildSidebarConnectionTagTree, buildV2RailConnectionGroups, buildV2SidebarTableSectionedChildren, collectSidebarSubtreeKeys, estimateV2TreeHorizontalScrollWidth, filterV2CommandSearchTreeItems, filterV2ExplorerTreeByKind, isSidebarTablePinned, isConnectionTagDescendant, normalizeSidebarTreeRelativeDropPosition, resolveSidebarConnectionIdFromKey, resolveSidebarDropInsertBefore, resolveSidebarDropNodeFromDomEvent, resolveSidebarDropTargetMetricsFromDomEvent, resolveSidebarDatabaseTreePruneKeys, resolveSidebarNodeConnectionId, resolveV2ActiveConnectionId, resolveV2SelectedDatabaseName, resolveV2CommandSearchPersistentFilter, shouldClearSidebarNodeChildrenOnCollapse, shouldSkipSidebarLoadOnExpandWhileDragging, shouldSkipSidebarSelectWhileDragging, shouldCloseV2CommandSearchOnGlobalKey, shouldRunV2CommandSearchEnter, sortSidebarTableEntries, type SidebarConnectionState, type SidebarTreeNode as TreeNode, type V2CommandSearchItem, } from './sidebarV2Utils'; export { buildSidebarTableChildrenForUi, buildSidebarConnectionTagTree, buildV2RailConnectionGroups, buildV2SidebarTableSectionedChildren, collectSidebarSubtreeKeys, estimateV2TreeHorizontalScrollWidth, filterV2CommandSearchTreeItems, filterV2ExplorerTreeByKind, isSidebarTablePinned, isConnectionTagDescendant, normalizeSidebarTreeRelativeDropPosition, resolveSidebarConnectionIdFromKey, resolveSidebarDropInsertBefore, resolveSidebarDropNodeFromDomEvent, resolveSidebarDropTargetMetricsFromDomEvent, resolveSidebarDatabaseTreePruneKeys, resolveSidebarNodeConnectionId, resolveV2ActiveConnectionId, resolveV2CommandSearchPersistentFilter, shouldClearSidebarNodeChildrenOnCollapse, shouldSkipSidebarLoadOnExpandWhileDragging, shouldSkipSidebarSelectWhileDragging, shouldCloseV2CommandSearchOnGlobalKey, shouldRunV2CommandSearchEnter, sortSidebarTableEntries, }; export { resolveSidebarTagDropInsertBefore } from './sidebarV2Utils'; export type { V2CommandSearchItem, V2RailConnectionGroup } from './sidebarV2Utils'; type SidebarTreeSwitcherNodeLike = { key?: React.Key; data?: TreeNode; isLeaf?: boolean; loading?: boolean; }; export const resolveSidebarSwitcherLoadKey = (node: SidebarTreeSwitcherNodeLike | null | undefined): string | null => { const treeNode = node?.data; const dataRef = treeNode?.dataRef; if (!treeNode) { return null; } if (treeNode.type === 'connection') { const connectionId = String(dataRef?.id || treeNode.key || node?.key || '').trim(); return connectionId ? `dbs-${connectionId}` : null; } if (treeNode.type === 'database') { const connectionId = String(dataRef?.id || '').trim(); const dbName = String(dataRef?.dbName || '').trim(); return connectionId && dbName ? `tables-${connectionId}-${dbName}` : null; } if (treeNode.type === 'jvm-mode' || treeNode.type === 'jvm-resource') { const connectionId = String(dataRef?.id || '').trim(); const providerMode = String(dataRef?.providerMode || '').trim().toLowerCase(); const parentPath = treeNode.type === 'jvm-resource' ? String(dataRef?.resourcePath || '').trim() : ''; return connectionId && providerMode ? `jvm-resources-${connectionId}-${providerMode}-${parentPath}` : null; } return null; }; export const shouldKeepSidebarSwitcherCollapsedWhileLoading = ( node: SidebarTreeSwitcherNodeLike | null | undefined, loadingKeys: ReadonlySet, ): boolean => { if (!node || node.isLeaf) { return false; } if (node.loading) { return true; } const loadKey = resolveSidebarSwitcherLoadKey(node); return !!loadKey && loadingKeys.has(loadKey); }; const { Search } = Input; const SIDEBAR_LOCATE_LOAD_WAIT_INTERVAL_MS = 50; const SIDEBAR_LOCATE_LOAD_WAIT_ATTEMPTS = 160; const SIDEBAR_CACHED_DATABASE_TREE_LIMIT = 12; // 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: 'sequences', labelKey: 'sidebar.command_search.object_kind.sequences' }, { key: 'routines', labelKey: 'sidebar.command_search.object_kind.routines' }, { key: 'packages', labelKey: 'sidebar.command_search.object_kind.packages' }, { 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 || [], schemaVisibilityByDatabase: conn.schemaVisibilityByDatabase || {}, }); }; 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[], savedQueryGroups: SavedQueryGroup[] = [], ): TreeNode | null => { const normalizedGroups = normalizeSavedQueryGroups( savedQueryGroups, savedQueries.map((query) => query.id), ); if (savedQueries.length === 0 && normalizedGroups.length === 0) { return null; } 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 buildAutomaticChildren = (queries: SavedQuery[]): TreeNode[] => { const connectionIds = new Set(connections.map((conn) => conn.id)); const unmatchedSavedQueries = queries.filter((query) => isSavedQueryUnmatchedForConnectionIds(query, connectionIds)); const unmatchedIds = new Set(unmatchedSavedQueries.map((query) => query.id)); const groupedByConnection = new Map(); queries.forEach((query) => { if (unmatchedIds.has(query.id)) return; groupedByConnection.set(query.connectionId, [ ...(groupedByConnection.get(query.connectionId) || []), query, ]); }); const automaticChildren: TreeNode[] = []; connections.forEach((conn) => { const connectionQueries = groupedByConnection.get(conn.id); if (!connectionQueries || connectionQueries.length === 0) return; const iconType = resolveConnectionIconType(conn); const iconColor = resolveConnectionAccentColor(conn); automaticChildren.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, ]); }); automaticChildren.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 automaticChildren; }; const queryById = new Map(savedQueries.map((query) => [query.id, query])); const groupById = new Map(normalizedGroups.map((group) => [group.id, group])); const groupOwners = getSavedQueryGroupOwnerIds(normalizedGroups); const buildManualGroupNode = (group: SavedQueryGroup, ancestors = new Set()): TreeNode => { const nextAncestors = new Set(ancestors); nextAncestors.add(group.id); const children = resolveSavedQueryGroupChildOrder(group.id, normalizedGroups).flatMap((token): TreeNode[] => { if (isSavedQueryGroupQueryToken(token)) { const query = queryById.get(getSavedQueryIdFromGroupToken(token)); return query ? [createQueryNode(query)] : []; } if (isSavedQueryGroupToken(token)) { const childGroupId = getSavedQueryGroupIdFromToken(token); const childGroup = groupById.get(childGroupId); if (!childGroup || childGroup.parentGroupId !== group.id || nextAncestors.has(childGroup.id)) return []; return [buildManualGroupNode(childGroup, nextAncestors)]; } return []; }); return { title: group.name || t('sidebar.saved_query_group.untitled'), key: `saved-query-manual-group-${group.id}`, icon: , type: 'saved-query-manual-group', dataRef: group, selectable: false, isLeaf: false, children, }; }; const automaticChildren = buildAutomaticChildren( savedQueries.filter((query) => !groupOwners.has(query.id)), ); const children: TreeNode[] = normalizedGroups .filter((group) => !group.parentGroupId) .map((group) => buildManualGroupNode(group)); if (normalizedGroups.length === 0) { children.push(...automaticChildren); } else if (automaticChildren.length > 0) { children.push({ title: t('sidebar.tree.ungrouped_saved_queries'), key: 'all-saved-queries-ungrouped', icon: , type: 'saved-query-group', selectable: false, isLeaf: false, children: automaticChildren, }); } 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; onOpenSettings?: () => void; onToggleAI?: () => void; onToggleLogPanel?: () => void; uiVersion?: 'legacy' | 'v2'; onFocusCommandSearch?: () => void; onCollapseSidebar?: () => void; onExpandSidebar?: () => void; collapseSidebarLabel?: string; collapseSidebarButtonRef?: React.Ref; expandSidebarLabel?: string; expandSidebarButtonRef?: React.Ref; isTreePanelCollapsed?: boolean; }> = React.memo(({ onCreateConnection, onEditConnection, onOpenSettings, onToggleAI, onToggleLogPanel, uiVersion, onFocusCommandSearch, onCollapseSidebar, onExpandSidebar, collapseSidebarLabel, collapseSidebarButtonRef, expandSidebarLabel, expandSidebarButtonRef, isTreePanelCollapsed = false, }) => { const connections = useStore(state => state.connections); const savedQueries = useStore(state => state.savedQueries); const savedQueryGroups = useStore(state => state.savedQueryGroups); const externalSQLDirectories = useStore(state => state.externalSQLDirectories); const saveQuery = useStore(state => state.saveQuery); const deleteQuery = useStore(state => state.deleteQuery); const saveSavedQueryGroup = useStore(state => state.saveSavedQueryGroup); const deleteSavedQueryGroup = useStore(state => state.deleteSavedQueryGroup); const moveSavedQueryToGroup = useStore(state => state.moveSavedQueryToGroup); const reloadSavedQueryGroups = useStore(state => state.reloadSavedQueryGroups); const saveExternalSQLDirectory = useStore(state => state.saveExternalSQLDirectory); const deleteExternalSQLDirectory = useStore(state => state.deleteExternalSQLDirectory); const updateRecentSQLFilePath = useStore(state => state.updateRecentSQLFilePath); const removeRecentSQLFilesByPath = useStore(state => state.removeRecentSQLFilesByPath); const moveRecentSQLFilesByDirectory = useStore(state => state.moveRecentSQLFilesByDirectory); const removeRecentSQLFilesByDirectory = useStore(state => state.removeRecentSQLFilesByDirectory); const addConnection = useStore(state => state.addConnection); const updateConnection = useStore(state => state.updateConnection); const addTab = useStore(state => state.addTab); const updateQueryTabDraft = useStore(state => state.updateQueryTabDraft); const tabs = useWorkbenchTabs(); 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 moveConnectionTag = useStore(state => state.moveConnectionTag); 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 queryOptions = useStore(state => state.queryOptions); const setQueryOptions = useStore(state => state.setQueryOptions); const addSqlLog = useStore(state => state.addSqlLog); const hideSqlLogFromRecent = useStore(state => state.hideSqlLogFromRecent); const clearRecentSqlLogs = useStore(state => state.clearRecentSqlLogs); 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 sidebarTableMetadataFields = useMemo( () => resolveSidebarTableMetadataFields( queryOptions?.sidebarTableMetadataFields, queryOptions?.showSidebarTableComment === true, queryOptions?.sidebarTableMetadataFieldOrder, ), [queryOptions?.showSidebarTableComment, queryOptions?.sidebarTableMetadataFieldOrder, queryOptions?.sidebarTableMetadataFields], ); 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, uiVersion: isV2Ui ? 'v2' : 'legacy', }), [darkMode, disableLocalBackdropFilter, isV2Ui], ); 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 tableDoubleClickAction = appearance.tableDoubleClickAction === 'open-design' ? 'open-design' : 'open-data'; 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 commandSearchSqlLogs = useStore( state => selectSidebarCommandSearchSqlLogs(state, isV2Ui && isV2CommandSearchOpen), ); const recentSqlLogs = useMemo( () => selectRecentSidebarSqlLogs(commandSearchSqlLogs), [commandSearchSqlLogs], ); 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 databaseTreeTouchedAtRef = useRef>({}); const pruneLoadedDatabaseTreesRef = useRef<() => void>(() => {}); 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, savedQueryGroups); }, [connections, savedQueries, savedQueryGroups]); const sidebarHiddenObjectGroups = appearance.sidebarHiddenObjectGroups; const visibleSidebarTreeData = useMemo( () => filterSidebarTreeByHiddenObjectGroups(treeData, sidebarHiddenObjectGroups), [sidebarHiddenObjectGroups, treeData], ); const sidebarObjectVisibilitySignature = sidebarHiddenObjectGroups.join('|') || 'all'; 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(() => { pruneLoadedDatabaseTreesRef.current(); 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]); const persistV2SidebarFilter = useCallback((nextFilter: string) => { setAppearance({ v2SidebarPersistedFilter: nextFilter }); }, [setAppearance]); useSidebarFilterPersistence({ enabled: v2UseLegacySidebarFilter, searchValue, persistedFilter: v2PersistedSidebarFilter, onPersist: persistV2SidebarFilter, }); 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 -> 'loading' | '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 [schemaVisibilityForm] = Form.useForm<{ mode: SchemaVisibilityRule['mode']; schemas: string[]; }>(); const [schemaVisibilityTarget, setSchemaVisibilityTarget] = useState<{ connection: SavedConnection; dbName: string; databaseNodeKey: React.Key; availableSchemas: string[]; } | null>(null); const [isSavingSchemaVisibility, setIsSavingSchemaVisibility] = useState(false); 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); const [isSavedQueryGroupModalOpen, setIsSavedQueryGroupModalOpen] = useState(false); const [savedQueryGroupTargetId, setSavedQueryGroupTargetId] = useState(null); const [savedQueryGroupInitialParentId, setSavedQueryGroupInitialParentId] = useState(null); // Connection Tag Modals const [isCreateTagModalOpen, setIsCreateTagModalOpen] = useState(false); const [createTagForm] = Form.useForm(); const { handleExportDatabaseSQL, handleExportSchemaSQL, openBatchTableWorkbench, openBatchDatabaseWorkbench, } = useSidebarBatchExport({ connections, selectedNodesRef, addTab, }); // 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 buildTreeNode = (item: ReturnType[number]): TreeNode => { if (item.kind === 'connection') { return buildConnectionNode(item.connection); } return { title: item.tag.name, key: `tag-${item.tag.id}`, icon: ( ), type: 'tag', dataRef: item.tag, isLeaf: false, children: item.children.map(buildTreeNode), } as TreeNode; }; const orderedNodes = buildSidebarConnectionTagTree( connections, connectionTags, sidebarRootOrder, ).map(buildTreeNode); 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, dataRef?: unknown, ): TreeNode[] => { return list.map(node => { if (node.key === key) { return { ...node, children, ...(dataRef === undefined ? {} : { dataRef }), }; } if (node.children) { return { ...node, children: updateTreeData(node.children, key, children, dataRef), }; } 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, dataRef?: unknown, ): TreeNode[] => { const nextTreeData = updateTreeData(treeDataRef.current, key, children, dataRef); treeDataRef.current = nextTreeData; setTreeData(nextTreeData); return nextTreeData; }; const clearTreeNodeChildrenByKeys = useCallback((keysToClear: string[]) => { const keysToClearSet = new Set(keysToClear.map((key) => String(key || '').trim()).filter(Boolean)); if (keysToClearSet.size === 0) { return; } const clearChildren = (nodes: TreeNode[]): TreeNode[] => ( nodes.map((node) => { const nodeKey = String(node.key || '').trim(); if (keysToClearSet.has(nodeKey)) { return { ...node, children: undefined }; } if (node.children?.length) { return { ...node, children: clearChildren(node.children) }; } return node; }) ); setTreeData((prev) => { const nextTreeData = clearChildren(prev); treeDataRef.current = nextTreeData; return nextTreeData; }); setLoadedKeys((prev) => prev.filter((key) => !keysToClearSet.has(String(key)))); keysToClearSet.forEach((key) => { delete databaseTreeTouchedAtRef.current[key]; }); }, []); const pruneLoadedDatabaseTrees = useCallback(() => { const activeDatabaseKey = activeContext?.connectionId && activeContext?.dbName ? `${activeContext.connectionId}-${activeContext.dbName}` : ''; const keysToClear = resolveSidebarDatabaseTreePruneKeys({ treeData: treeDataRef.current, expandedKeys, selectedKeys, activeDatabaseKey, touchedAtByDatabaseKey: databaseTreeTouchedAtRef.current, maxLoadedDatabases: SIDEBAR_CACHED_DATABASE_TREE_LIMIT, }); if (keysToClear.length === 0) { return; } clearTreeNodeChildrenByKeys(keysToClear); }, [activeContext?.connectionId, activeContext?.dbName, clearTreeNodeChildrenByKeys, expandedKeys, selectedKeys]); pruneLoadedDatabaseTreesRef.current = pruneLoadedDatabaseTrees; 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, } = useSidebarExternalSqlWorkflow({ connections, externalSQLDirectories, activeTab, connectionIds, selectedNodesRef, addTab, saveExternalSQLDirectory, deleteExternalSQLDirectory, updateRecentSQLFilePath, removeRecentSQLFilesByPath, moveRecentSQLFilesByDirectory, removeRecentSQLFilesByDirectory, refreshGlobalExternalSQLRootNode, setExpandedKeys, setAutoExpandParent, getActiveContext: () => useStore.getState().activeContext, }); useEffect(() => { const handleWorkbenchAddExternalSQLDirectory = () => { void handleAddExternalSQLDirectory({ type: 'external-sql-root' }); }; window.addEventListener('gonavi:add-external-sql-directory', handleWorkbenchAddExternalSQLDirectory); return () => { window.removeEventListener('gonavi:add-external-sql-directory', handleWorkbenchAddExternalSQLDirectory); }; }, [handleAddExternalSQLDirectory]); 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 === 'saved-query-manual-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 conn = connections.find(c => c.id === id); const forceReadOnly = readOnly || isStructureOnlyDbType(id) || isConnectionStructureEditRestricted(conn?.config); 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; const conn = connections.find(c => c.id === id); if (isStructureOnlyDbType(id) || isConnectionStructureEditRestricted(conn?.config)) { 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 openSidebarObjectNode = (node: any): boolean => { 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 true; } 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 true; } if (node.type === 'db-event') { openEventDefinition(node); return true; } 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 true; } if (node.type === 'sequence') { openSequenceDefinition(node); return true; } if (node.type === 'package') { openPackageDefinition(node); return true; } return 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 === 'sequence' || type === 'package' || 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); } else if (openSidebarObjectNode(info.node)) { return; } }; const onExpand = (newExpandedKeys: React.Key[], info?: any) => { if (!info?.expanded && shouldClearSidebarNodeChildrenOnCollapse(info?.node)) { const collapsedKey = String(info.node?.key || '').trim(); const keysToClear = [ collapsedKey, ...collectSidebarSubtreeKeys(info.node), ].filter(Boolean); const keysToClearSet = new Set(keysToClear); setExpandedKeys(newExpandedKeys.filter((key) => !keysToClearSet.has(String(key)))); setAutoExpandParent(false); clearTreeNodeChildrenByKeys(keysToClear); return; } 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 === 'sequence' || type === 'package' || 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, initialViewMode: tableDoubleClickAction === 'open-design' ? 'fields' : undefined, initialViewModeRequestId: tableDoubleClickAction === 'open-design' ? String(Date.now()) : undefined, 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 === 'sequence') { openSequenceDefinition(node); return; } else if (node.type === 'package') { openPackageDefinition(node); 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 renderSidebarSwitcherIcon = useCallback((node: SidebarTreeSwitcherNodeLike) => { if (node.isLeaf) { return null; } const keepCollapsed = shouldKeepSidebarSwitcherCollapsedWhileLoading(node, loadingNodesRef.current); return ; }, []); 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, t); 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 { title: dbName, 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 openSavedQueryGroupModal = useCallback(async ( target?: SavedQueryGroup | null, initialParentGroupId?: string | null, ) => { try { const groups = await reloadSavedQueryGroups(); const targetId = String(target?.id || '').trim(); if (targetId && !groups.some((group) => group.id === targetId)) { message.warning(t('sidebar.message.saved_query_group_not_found')); return; } const parentId = String(initialParentGroupId || '').trim(); setSavedQueryGroupTargetId(targetId || null); setSavedQueryGroupInitialParentId( parentId && groups.some((group) => group.id === parentId) ? parentId : null, ); setIsSavedQueryGroupModalOpen(true); } catch (error) { message.error(t('sidebar.message.saved_query_group_load_failed', { error: error instanceof Error ? error.message : String(error), })); } }, [reloadSavedQueryGroups]); const closeSavedQueryGroupModal = useCallback(() => { setIsSavedQueryGroupModalOpen(false); setSavedQueryGroupTargetId(null); setSavedQueryGroupInitialParentId(null); }, []); const handleSaveSavedQueryGroup = useCallback(async (group: SavedQueryGroup) => { const isEditing = Boolean(group.id); await saveSavedQueryGroup(group); message.success(t( isEditing ? 'sidebar.message.saved_query_group_updated' : 'sidebar.message.saved_query_group_created', )); }, [saveSavedQueryGroup]); const savedQueryGroupTarget = useMemo( () => savedQueryGroups.find((group) => group.id === savedQueryGroupTargetId) || null, [savedQueryGroupTargetId, savedQueryGroups], ); const { loadDatabases, loadJVMResources, loadTables, } = useSidebarTreeLoaders({ savedQueries, tableSortPreference, tableAccessCount, pinnedSidebarTables, isV2Ui, loadingNodesRef, setConnectionStates, setLoadedKeys, replaceTreeNodeChildren, buildRuntimeConfig, buildJVMRuntimeConfig, buildJVMDiagnosticTreeNodes, resolveSavedQueryDisplayName, onDatabaseTreeLoaded: (databaseKey: string) => { databaseTreeTouchedAtRef.current[databaseKey] = Date.now(); pruneLoadedDatabaseTrees(); }, }); const openSchemaVisibilitySettings = useCallback((node: any) => { const dbName = String(node?.dataRef?.dbName || node?.title || '').trim(); const connectionId = String(node?.dataRef?.id || '').trim(); const connection = connections.find((item) => item.id === connectionId) || node?.dataRef; if (!connection || !dbName || !shouldHideSchemaPrefix(connection as SavedConnection)) { return; } const databaseNode = node?.type === 'database' ? node : getDatabaseNodeRef(connection, dbName); const currentRule = getSchemaVisibilityRule(connection as SavedConnection, dbName); const availableSchemas = Array.from(new Set([ ...(Array.isArray(databaseNode?.children) ? databaseNode.children .filter((item: any) => item?.dataRef?.groupKey === 'schema') .map((item: any) => String(item?.dataRef?.schemaName || item?.title || '').trim()) : []), ...(currentRule?.schemas || []), ].filter(Boolean))).sort((a, b) => a.localeCompare(b)); schemaVisibilityForm.setFieldsValue({ mode: currentRule?.mode || 'include', schemas: currentRule?.schemas || [], }); setSchemaVisibilityTarget({ connection: connection as SavedConnection, dbName, databaseNodeKey: databaseNode?.key || `${connectionId}-${dbName}`, availableSchemas, }); void loadSchemas(connection as SavedConnection, dbName).then((result) => { const loadedSchemas = Array.isArray(result?.schemas) ? result.schemas.map((schema) => String(schema || '').trim()).filter(Boolean) : []; if (loadedSchemas.length === 0) return; setSchemaVisibilityTarget((current) => { if (!current || current.connection.id !== connectionId || current.dbName !== dbName) { return current; } return { ...current, availableSchemas: Array.from(new Set([ ...current.availableSchemas, ...loadedSchemas, ])).sort((left, right) => left.localeCompare(right)), }; }); }).catch(() => undefined); }, [connections, getDatabaseNodeRef, schemaVisibilityForm]); const handleSaveSchemaVisibility = useCallback(async () => { if (!schemaVisibilityTarget) return; setIsSavingSchemaVisibility(true); try { const values = await schemaVisibilityForm.validateFields(); const mode = values.mode === 'exclude' ? 'exclude' : 'include'; const seenSchemas = new Set(); const schemas = (Array.isArray(values.schemas) ? values.schemas : []) .map((schema) => String(schema || '').trim()) .filter((schema) => { const normalized = schema.toLocaleLowerCase(); if (!normalized || seenSchemas.has(normalized)) return false; seenSchemas.add(normalized); return true; }); const nextRule: SchemaVisibilityRule | undefined = schemas.length > 0 ? { mode, schemas } : undefined; const nextConnection = updateSchemaVisibilityRule( schemaVisibilityTarget.connection, schemaVisibilityTarget.dbName, nextRule, ); const backendApp = (window as any).go?.app?.App; if (typeof backendApp?.SaveConnection !== 'function') { throw new Error(t('connection_modal.message.save_failed')); } const saved = await backendApp.SaveConnection({ id: nextConnection.id, name: nextConnection.name, config: nextConnection.config, includeDatabases: nextConnection.includeDatabases, includeRedisDatabases: nextConnection.includeRedisDatabases, schemaVisibilityByDatabase: nextConnection.schemaVisibilityByDatabase, iconType: nextConnection.iconType, iconColor: nextConnection.iconColor, }); const persistedConnection: SavedConnection = { ...nextConnection, ...(saved || {}), schemaVisibilityByDatabase: nextConnection.schemaVisibilityByDatabase, }; connectionReloadSignaturesRef.current[persistedConnection.id] = buildConnectionReloadSignature(persistedConnection); updateConnection(persistedConnection); await loadTables({ key: schemaVisibilityTarget.databaseNodeKey, type: 'database', dataRef: { ...persistedConnection, dbName: schemaVisibilityTarget.dbName, }, }); setExpandedKeys((previous) => previous.includes(schemaVisibilityTarget.databaseNodeKey) ? previous : [...previous, schemaVisibilityTarget.databaseNodeKey]); setSchemaVisibilityTarget(null); message.success(t('sidebar.schema_visibility.message.saved')); } catch (error: any) { message.error(t('sidebar.schema_visibility.message.save_failed', { error: error?.message || String(error), })); } finally { setIsSavingSchemaVisibility(false); } }, [loadTables, schemaVisibilityForm, schemaVisibilityTarget, updateConnection]); const migrateSchemaVisibilityForRenamedDatabase = useCallback(async ( connection: SavedConnection, oldDbName: string, newDbName: string, ): Promise => { const currentConnection = connections.find((item) => item.id === connection.id) || connection; const nextConnection = moveSchemaVisibilityRule(currentConnection, oldDbName, newDbName); if (nextConnection === currentConnection) { return currentConnection; } const backendApp = (window as any).go?.app?.App; if (typeof backendApp?.SaveConnection !== 'function') { message.warning(t('sidebar.schema_visibility.message.save_failed', { error: t('connection_modal.message.save_failed'), })); return currentConnection; } try { const saved = await backendApp.SaveConnection({ id: nextConnection.id, name: nextConnection.name, config: nextConnection.config, includeDatabases: nextConnection.includeDatabases, includeRedisDatabases: nextConnection.includeRedisDatabases, schemaVisibilityByDatabase: nextConnection.schemaVisibilityByDatabase, iconType: nextConnection.iconType, iconColor: nextConnection.iconColor, }); const persistedConnection: SavedConnection = { ...nextConnection, ...(saved || {}), schemaVisibilityByDatabase: nextConnection.schemaVisibilityByDatabase, }; connectionReloadSignaturesRef.current[persistedConnection.id] = buildConnectionReloadSignature(persistedConnection); updateConnection(persistedConnection); return persistedConnection; } catch (error: any) { message.warning(t('sidebar.schema_visibility.message.save_failed', { error: error?.message || String(error), })); return currentConnection; } }, [connections, updateConnection]); const { handleCopyStructure, handleCopyTable, handleCopyTableName, handleCopyDatabaseName, 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, handleRevealSavedQueryInFolder, isSavedQueryUnmatched, handleRebindSavedQuery, openRoutineDefinition, openEventDefinition, openEditEvent, openSequenceDefinition, openPackageDefinition, 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, migrateSchemaVisibilityForRenamedDatabase, }); 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, handleCopyTable, handleCopyDatabaseName, 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, sidebarTableMetadataFields, treeData: visibleSidebarTreeData, treeViewportWidth, treeHeight, isV2Ui, isV2CommandSearchOpen, connections, connectionIds, selectedKeys, selectedNodesRef, activeContext, activeTab, recentSqlLogs, 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(6, 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, openSchemaVisibilitySettings, resolveMessagePublishTarget, addSqlLog, handleV2TableContextMenuAction, handleV2TableGroupContextMenuAction, handleV2DatabaseContextMenuAction, handleV2ConnectionContextMenuAction, handleV2ConnectionGroupContextMenuAction, }); refreshV2TableContextMenuStatsRef.current = refreshV2TableContextMenuStats; const getV2TreeMetaTextRef = useRef(getV2TreeMetaText); getV2TreeMetaTextRef.current = getV2TreeMetaText; const toggleSidebarTablePinnedRef = useRef(toggleSidebarTablePinned); toggleSidebarTablePinnedRef.current = toggleSidebarTablePinned; const renderV2TreeTitle = useCallback((node: any, hoverTitle: string, statusBadge: React.ReactNode) => renderSidebarV2TreeTitle({ node, hoverTitle, statusBadge, getV2TreeMetaText: getV2TreeMetaTextRef.current, sidebarTableMetadataFields, toggleSidebarTablePinned: toggleSidebarTablePinnedRef.current, snapshotTreeSelectionBeforeDrag, restoreTreeSelectionAfterDrag, treeDragSelectSuppressUntilRef, setIsTreeDragging, }), [ restoreTreeSelectionAfterDrag, setIsTreeDragging, sidebarTableMetadataFields, snapshotTreeSelectionBeforeDrag, treeDragSelectSuppressUntilRef, ]); 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, shouldHideSchemaPrefix, openSchemaVisibilitySettings, 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, openEditEvent, openSequenceDefinition, openPackageDefinition, resolveMessagePublishTarget, openMessagePublishModal, openDesign, openCreateStarRocksRollup, handleCopyTableName, handleCopyTable, handleCopyStructure, handleExport, setRenameTableTarget, renameTableForm, setIsRenameTableModalOpen, handleTableDataDangerAction, handleDeleteTable, openExportDialog, isSavedQueryUnmatched, connections, handleRebindSavedQuery, openRenameSavedQueryModal, handleRevealSavedQueryInFolder, resolveSavedQueryDisplayName, deleteQuery, savedQueryGroups, openSavedQueryGroupModal, deleteSavedQueryGroup, moveSavedQueryToGroup, 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 getTagParentId = (tagId: unknown): string | null => { const tag = connectionTags.find((candidate) => candidate.id === String(tagId || '').trim()); const parentTagId = String(tag?.parentTagId || '').trim(); return parentTagId || null; }; const getConnectionParentTagId = (connectionId: unknown): string | null => ( connectionTags.find((tag) => tag.connectionIds.includes(String(connectionId || '').trim()))?.id || null ); const getNodeParentTagId = (node: any): string | null => { if (node?.type === 'tag') return getTagParentId(node?.dataRef?.id); if (node?.type === 'connection') return getConnectionParentTagId(node?.key); return null; }; const getNodeOrderToken = (node: any): string | null => { if (node?.type === 'tag') { const tagId = String(node?.dataRef?.id || '').trim(); return tagId ? buildSidebarRootTagToken(tagId) : null; } if (node?.type === 'connection') { const connectionId = String(node?.key || '').trim(); return connectionId ? buildSidebarRootConnectionToken(connectionId) : null; } return null; }; const allowSidebarTreeDrop = ({ dragNode, dropNode, dropPosition }: any): boolean => { if (!dragNode || !dropNode) return false; if ((dragNode.type !== 'tag' && dragNode.type !== 'connection') || (dropNode.type !== 'tag' && dropNode.type !== 'connection')) { return false; } // Connections cannot contain tree items. A group can contain a group only // when the pointer lands on its content, not on its before/after gap. const droppingIntoTag = dropNode.type === 'tag' && Number(dropPosition) === 0; if (dropNode.type === 'connection' && Number(dropPosition) === 0) return false; if (dragNode.type !== 'tag') return String(dragNode.key) !== String(dropNode.key); const dragTagId = String(dragNode?.dataRef?.id || '').trim(); const targetParentTagId = droppingIntoTag ? String(dropNode?.dataRef?.id || '').trim() || null : getNodeParentTagId(dropNode); return !!dragTagId && !isConnectionTagDescendant(dragTagId, targetParentTagId, connectionTags); }; 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); if (!dragNode || !dropNode) return; const droppingIntoTag = dropNode.type === 'tag' && ( info?.dropToGap === false || (info?.dropToGap === undefined && dropPosition === 0) ); const targetParentTagId = droppingIntoTag ? String(dropNode?.dataRef?.id || '').trim() || null : getNodeParentTagId(dropNode); const targetToken = droppingIntoTag ? null : getNodeOrderToken(dropNode); const targetInsertBefore = droppingIntoTag ? false : insertBefore; if (dragNode.type === 'tag') { const dragTagId = String(dragNode?.dataRef?.id || '').trim(); if (!dragTagId || isConnectionTagDescendant(dragTagId, targetParentTagId, connectionTags)) return; moveConnectionTag(dragTagId, targetParentTagId, targetToken, targetInsertBefore); return; } if (dragNode.type === 'connection') { const connectionId = String(dragNode.key || '').trim(); if (!connectionId || connectionId === String(dropNode.key || '')) return; moveConnectionToTag(connectionId, targetParentTagId, targetToken, targetInsertBefore); } }; 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 v2DataImportLabel = t('sidebar.action.data_import'); 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 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 handleOpenDataImportWorkbench = useCallback(() => { const node = selectedNodesRef.current[0]; const activeTab = tabs.find((tab) => tab.id === activeTabId); const nodeConnectionId = String( node?.dataRef?.id || (node?.type === 'connection' ? node?.key : '') || '', ).trim(); const connectionId = nodeConnectionId || String(activeContext?.connectionId || '').trim(); const dbName = String( node?.type === 'database' ? (node?.dataRef?.dbName || node?.title || '') : node?.dataRef?.dbName || activeContext?.dbName || '', ).trim(); const tableName = String( node?.type === 'table' ? (node?.dataRef?.tableName || node?.title || '') : !node && activeTab?.type === 'table' ? activeTab.tableName || '' : '', ).trim(); const mode = node?.type === 'database' ? 'database' : 'table'; const existingImportTab = tabs.find((tab) => tab.id === DATA_IMPORT_WORKBENCH_TAB_ID); addTab(resolveDataImportWorkbenchLaunchTab( existingImportTab, { connectionId, dbName, tableName, mode }, )); }, [activeContext?.connectionId, activeContext?.dbName, activeTabId, addTab, tabs]); 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)), onRemoveRecentItem: (item: V2CommandSearchItem) => { if (item.kind === 'recent') hideSqlLogFromRecent(item.logId); }, onClearRecentItems: clearRecentSqlLogs, onTogglePersistentFilter: toggleV2CommandSearchPersistentFilter, onResetFilter: resetV2SidebarFilter, }, }; // V2 Connection Rail 子组件 props(从原 renderV2ConnectionRail 抽出,保留所有原行为) const v2ConnectionRailProps = { labels: { railSystemActions: v2RailSystemActionsLabel, railObjectActions: v2RailObjectActionsLabel, newGroup: v2NewGroupLabel, batchTables: v2BatchTablesLabel, batchDatabases: v2BatchDatabasesLabel, dataImport: v2DataImportLabel, openExternalSqlFile: v2OpenExternalSqlFileLabel, locateCurrentTable: v2LocateCurrentTableLabel, locateCurrentTableUnavailable: v2LocateCurrentTableUnavailableLabel, aiAssistant: v2AiAssistantLabel, settings: v2SettingsLabel, }, handlers: { openCreateTagModal: () => { setRenameViewTarget(null); createTagForm.resetFields(); setIsCreateTagModalOpen(true); }, openBatchTableExport: openBatchTableWorkbench, openBatchDatabaseExport: openBatchDatabaseWorkbench, openDataImport: handleOpenDataImportWorkbench, openExternalSqlFile: handleOpenSQLFileFromToolbar, locateActiveTab: handleLocateActiveTabInSidebar, toggleAI: onToggleAI ?? (() => {}), openSettings: onOpenSettings ?? (() => {}), }, canLocateActiveTab, sidebarExpandAction: onExpandSidebar && expandSidebarLabel ? { label: expandSidebarLabel, onClick: onExpandSidebar, buttonRef: expandSidebarButtonRef, } : undefined, workbenchActions: ( <> ), }; 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' }} allowDrop={allowSidebarTreeDrop} 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} switcherIcon={renderSidebarSwitcherIcon} expandedKeys={expandedKeys} onExpand={onExpand} loadedKeys={loadedKeys} onLoad={setLoadedKeys} autoExpandParent={autoExpandParent} selectedKeys={selectedKeys} blockNode height={effectiveTreeHeight} scrollWidth={isV2Ui ? v2TreeHorizontalScrollWidth : undefined} onRightClick={onRightClick} />
{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} >
)} , t('sidebar.schema_visibility.title', { database: schemaVisibilityTarget?.dbName || '' }), t('sidebar.schema_visibility.description'), )} open={Boolean(schemaVisibilityTarget)} centered width={560} okText={t('common.save')} confirmLoading={isSavingSchemaVisibility} styles={{ content: modalPanelStyle, header: { background: 'transparent', borderBottom: 'none', paddingBottom: 10 }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none', paddingTop: 12 }, }} onOk={() => void handleSaveSchemaVisibility()} onCancel={() => { setSchemaVisibilityTarget(null); schemaVisibilityForm.resetFields(); }} >
{t('sidebar.schema_visibility.mode.include')} {t('sidebar.schema_visibility.mode.exclude')}