mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-22 05:11:44 +08:00
3204 lines
126 KiB
TypeScript
3204 lines
126 KiB
TypeScript
import SidebarConnectionRail from './sidebar/SidebarConnectionRail';
|
||
import SidebarSearchPanel, { type SidebarSearchPanelProps } from './sidebar/SidebarSearchPanel';
|
||
import SlowQueryRailButton from './sidebar/SlowQueryRailButton';
|
||
import { buildSidebarLegacyNodeMenuItems } from './sidebar/sidebarLegacyNodeMenu';
|
||
import {
|
||
getMetadataDialect,
|
||
shouldHideSchemaPrefix,
|
||
splitQualifiedName,
|
||
} from './sidebar/sidebarMetadataLoaders';
|
||
import {
|
||
useSidebarBatchExport,
|
||
} from './sidebar/useSidebarBatchExport';
|
||
import { SidebarBatchExportModals } from './sidebar/SidebarBatchExportModals';
|
||
import { SidebarEntityModals } from './sidebar/SidebarEntityModals';
|
||
import { renderSidebarV2TreeTitle } from './sidebar/SidebarTreeTitle';
|
||
import {
|
||
useSidebarV2ContextMenu,
|
||
} from './sidebar/useSidebarV2ContextMenu';
|
||
import {
|
||
useSidebarObjectActions,
|
||
type SidebarMessagePublishTarget,
|
||
} from './sidebar/useSidebarObjectActions';
|
||
import { useSidebarSearchModel } from './sidebar/useSidebarSearchModel';
|
||
import { useSidebarV2ActionHandlers } from './sidebar/useSidebarV2ActionHandlers';
|
||
import { useSidebarCommandSearchRunner } from './sidebar/useSidebarCommandSearchRunner';
|
||
import { useSidebarTitleRender } from './sidebar/useSidebarTitleRender';
|
||
import {
|
||
normalizeDriverType,
|
||
useSidebarTreeLoaders,
|
||
} from './sidebar/useSidebarTreeLoaders';
|
||
export { formatSidebarDriverAgentUpdateWarning } from './sidebar/useSidebarTreeLoaders';
|
||
import {
|
||
ExternalSQLFileModal,
|
||
useSidebarExternalSqlWorkflow,
|
||
} from './sidebar/SidebarExternalSqlWorkflow';
|
||
export {
|
||
buildSQLFileExecutionFooter,
|
||
SQLFileExecutionProgressContent,
|
||
} from './sidebar/SidebarExternalSqlWorkflow';
|
||
export type {
|
||
SQLFileExecutionProgressState,
|
||
SQLFileExecutionStatus,
|
||
} from './sidebar/SidebarExternalSqlWorkflow';
|
||
import {
|
||
V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID,
|
||
formatSidebarRowCount,
|
||
hasSidebarLazyChildren,
|
||
shouldClearSidebarActiveContextOnEmptySelect,
|
||
shouldLoadSidebarNodeOnExpand,
|
||
getV2RailConnectionGroupBadgeText,
|
||
type V2ExplorerFilter,
|
||
} from './sidebar/sidebarHelpers';
|
||
// 重新导出,保持外部测试文件的 `from './Sidebar'` 兼容
|
||
export {
|
||
V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID,
|
||
formatSidebarRowCount,
|
||
hasSidebarLazyChildren,
|
||
shouldClearSidebarActiveContextOnEmptySelect,
|
||
shouldLoadSidebarNodeOnExpand,
|
||
getV2RailConnectionGroupBadgeText,
|
||
isV2SidebarObjectNode,
|
||
resolveV2ObjectGroupTitle,
|
||
resolveSidebarTableNameForCopy,
|
||
parseV2CommandSearchQuery,
|
||
} from './sidebar/sidebarHelpers';
|
||
import React, { useEffect, useState, useMemo, useRef, useCallback, useDeferredValue } from 'react';
|
||
import { createPortal } from 'react-dom';
|
||
import { Tree, message, Dropdown, MenuProps, Input, Button, Form, Popover, Tooltip } from 'antd';
|
||
import {
|
||
CaretDownFilled,
|
||
DatabaseOutlined,
|
||
TableOutlined,
|
||
ConsoleSqlOutlined,
|
||
HddOutlined,
|
||
FolderOutlined,
|
||
FolderOpenOutlined,
|
||
FileTextOutlined,
|
||
CopyOutlined,
|
||
ExportOutlined,
|
||
FolderAddOutlined,
|
||
SaveOutlined,
|
||
EditOutlined,
|
||
SearchOutlined,
|
||
KeyOutlined,
|
||
ThunderboltOutlined,
|
||
UnorderedListOutlined,
|
||
FunctionOutlined,
|
||
LinkOutlined,
|
||
FileAddOutlined,
|
||
PlusOutlined,
|
||
ReloadOutlined,
|
||
SendOutlined,
|
||
DeleteOutlined,
|
||
DisconnectOutlined,
|
||
CheckSquareOutlined,
|
||
FilterOutlined,
|
||
DashboardOutlined,
|
||
WarningOutlined,
|
||
AimOutlined,
|
||
MoreOutlined,
|
||
ToolOutlined,
|
||
SettingOutlined
|
||
} from '@ant-design/icons';
|
||
import {
|
||
buildSidebarRootConnectionToken,
|
||
buildSidebarRootTagToken,
|
||
resolveSidebarRootOrderTokens,
|
||
useStore,
|
||
} from '../store';
|
||
import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
|
||
import { SavedConnection, SavedQuery, ExternalSQLDirectory, ExternalSQLTreeEntry } from '../types';
|
||
import { getDbIcon } from './DatabaseIcons';
|
||
import { ListSQLDirectory } from '../../wailsjs/go/app/App';
|
||
import { supportsTableTruncateAction } from './tableDataDangerActions';
|
||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||
import { useAutoFetchVisibility } from '../utils/autoFetchVisibility';
|
||
import FindInDatabaseModal from './FindInDatabaseModal';
|
||
import { buildRpcConnectionConfig } from '../utils/connectionRpcConfig';
|
||
import { resolveDataSourceType } from '../utils/dataSourceCapabilities';
|
||
import { 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 { buildJVMTabTitle } from '../utils/jvmRuntimePresentation';
|
||
import { buildJVMDiagnosticActionDescriptor, buildJVMMonitoringActionDescriptors } from '../utils/jvmSidebarActions';
|
||
import {
|
||
buildBatchDatabaseExportWorkbenchTab,
|
||
buildBatchTableExportWorkbenchTab,
|
||
} from '../utils/tableExportTab';
|
||
import { useExportProgressDialog } from './ExportProgressModal';
|
||
import { getShortcutPlatform, resolveShortcutDisplay } from '../utils/shortcuts';
|
||
import { buildExternalSQLRootNode, type ExternalSQLTreeNode } from '../utils/externalSqlTree';
|
||
import { resolveSidebarTableMetadataFields } from '../utils/sidebarTableMetadata';
|
||
import { t } from '../i18n';
|
||
import MessagePublishModal from './MessagePublishModal';
|
||
import {
|
||
SIDEBAR_CONTEXT_MENU_FALLBACK_HEIGHT,
|
||
SIDEBAR_CONTEXT_MENU_FALLBACK_WIDTH,
|
||
resolveSidebarContextMenuPosition,
|
||
type SearchScope,
|
||
} from './sidebarCoreUtils';
|
||
export { resolveSidebarContextMenuPosition } from './sidebarCoreUtils';
|
||
export type { ExternalSQLFileModalMode, SearchScope } from './sidebarCoreUtils';
|
||
import {
|
||
buildSidebarTableChildrenForUi,
|
||
buildV2RailConnectionGroups,
|
||
buildV2SidebarTableSectionedChildren,
|
||
collectSidebarSubtreeKeys,
|
||
estimateV2TreeHorizontalScrollWidth,
|
||
filterV2CommandSearchTreeItems,
|
||
filterV2ExplorerTreeByKind,
|
||
isSidebarTablePinned,
|
||
normalizeSidebarTreeRelativeDropPosition,
|
||
resolveSidebarConnectionIdFromKey,
|
||
resolveSidebarDropInsertBefore,
|
||
resolveSidebarDropNodeFromDomEvent,
|
||
resolveSidebarDropTargetMetricsFromDomEvent,
|
||
resolveSidebarDatabaseTreePruneKeys,
|
||
resolveSidebarNodeConnectionId,
|
||
resolveSidebarTagDropInsertBefore,
|
||
resolveV2ActiveConnectionId,
|
||
resolveV2CommandSearchPersistentFilter,
|
||
shouldClearSidebarNodeChildrenOnCollapse,
|
||
shouldSkipSidebarLoadOnExpandWhileDragging,
|
||
shouldSkipSidebarSelectWhileDragging,
|
||
shouldCloseV2CommandSearchOnGlobalKey,
|
||
shouldRunV2CommandSearchEnter,
|
||
sortSidebarTableEntries,
|
||
type SidebarConnectionState,
|
||
type SidebarTreeNode as TreeNode,
|
||
type V2CommandSearchItem,
|
||
} from './sidebarV2Utils';
|
||
|
||
export {
|
||
buildSidebarTableChildrenForUi,
|
||
buildV2RailConnectionGroups,
|
||
buildV2SidebarTableSectionedChildren,
|
||
collectSidebarSubtreeKeys,
|
||
estimateV2TreeHorizontalScrollWidth,
|
||
filterV2CommandSearchTreeItems,
|
||
filterV2ExplorerTreeByKind,
|
||
isSidebarTablePinned,
|
||
normalizeSidebarTreeRelativeDropPosition,
|
||
resolveSidebarConnectionIdFromKey,
|
||
resolveSidebarDropInsertBefore,
|
||
resolveSidebarDropNodeFromDomEvent,
|
||
resolveSidebarDropTargetMetricsFromDomEvent,
|
||
resolveSidebarDatabaseTreePruneKeys,
|
||
resolveSidebarNodeConnectionId,
|
||
resolveSidebarTagDropInsertBefore,
|
||
resolveV2ActiveConnectionId,
|
||
resolveV2CommandSearchPersistentFilter,
|
||
shouldClearSidebarNodeChildrenOnCollapse,
|
||
shouldSkipSidebarLoadOnExpandWhileDragging,
|
||
shouldSkipSidebarSelectWhileDragging,
|
||
shouldCloseV2CommandSearchOnGlobalKey,
|
||
shouldRunV2CommandSearchEnter,
|
||
sortSidebarTableEntries,
|
||
};
|
||
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<string>,
|
||
): 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 || [],
|
||
});
|
||
};
|
||
|
||
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<string>): boolean => (
|
||
query.bindingStatus === 'orphan' || !connectionIds.has(query.connectionId)
|
||
);
|
||
|
||
export const buildAllSavedQueriesTreeNode = (
|
||
savedQueries: SavedQuery[],
|
||
connections: SavedConnection[],
|
||
): TreeNode | null => {
|
||
if (savedQueries.length === 0) {
|
||
return null;
|
||
}
|
||
|
||
const connectionIds = new Set(connections.map((conn) => conn.id));
|
||
const unmatchedSavedQueries = savedQueries.filter((query) => isSavedQueryUnmatchedForConnectionIds(query, connectionIds));
|
||
const unmatchedIds = new Set(unmatchedSavedQueries.map((query) => query.id));
|
||
const createQueryNode = (query: SavedQuery): TreeNode => ({
|
||
title: query.name || t('sidebar.tree.untitled_query'),
|
||
key: `all-saved-query-${query.id}`,
|
||
icon: <FileTextOutlined />,
|
||
type: 'saved-query',
|
||
dataRef: query,
|
||
isLeaf: true,
|
||
});
|
||
const buildDatabaseGroups = (queries: SavedQuery[], keyPrefix: string): TreeNode[] => {
|
||
const groupedByDatabase = new Map<string, SavedQuery[]>();
|
||
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: <DatabaseOutlined />,
|
||
type: 'saved-query-group',
|
||
selectable: false,
|
||
isLeaf: false,
|
||
children: items.map(createQueryNode),
|
||
}));
|
||
};
|
||
|
||
const groupedByConnection = new Map<string, SavedQuery[]>();
|
||
savedQueries.forEach((query) => {
|
||
if (unmatchedIds.has(query.id)) {
|
||
return;
|
||
}
|
||
groupedByConnection.set(query.connectionId, [
|
||
...(groupedByConnection.get(query.connectionId) || []),
|
||
query,
|
||
]);
|
||
});
|
||
|
||
const children: TreeNode[] = [];
|
||
connections.forEach((conn) => {
|
||
const connectionQueries = groupedByConnection.get(conn.id);
|
||
if (!connectionQueries || connectionQueries.length === 0) {
|
||
return;
|
||
}
|
||
const iconType = resolveConnectionIconType(conn);
|
||
const iconColor = resolveConnectionAccentColor(conn);
|
||
children.push({
|
||
title: conn.name || conn.id,
|
||
key: `all-saved-queries-connection-${conn.id}`,
|
||
icon: getDbIcon(iconType, iconColor, 22),
|
||
type: 'saved-query-group',
|
||
selectable: false,
|
||
isLeaf: false,
|
||
children: buildDatabaseGroups(connectionQueries, `all-saved-queries-connection-${conn.id}`),
|
||
});
|
||
});
|
||
|
||
if (unmatchedSavedQueries.length > 0) {
|
||
const groupedByOriginalConnection = new Map<string, SavedQuery[]>();
|
||
unmatchedSavedQueries.forEach((query) => {
|
||
const originalConnectionId = String(query.originalConnectionId || query.connectionId || t('sidebar.tree.unknown_connection')).trim() || t('sidebar.tree.unknown_connection');
|
||
groupedByOriginalConnection.set(originalConnectionId, [
|
||
...(groupedByOriginalConnection.get(originalConnectionId) || []),
|
||
query,
|
||
]);
|
||
});
|
||
children.push({
|
||
title: t('sidebar.tree.unmatched_saved_queries'),
|
||
key: 'all-saved-queries-unmatched',
|
||
icon: <WarningOutlined />,
|
||
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: <FolderOpenOutlined />,
|
||
type: 'saved-query-group',
|
||
selectable: false,
|
||
isLeaf: false,
|
||
children: buildDatabaseGroups(items, `all-saved-queries-unmatched-${encodeURIComponent(connectionLabel)}`),
|
||
})),
|
||
});
|
||
}
|
||
|
||
return {
|
||
title: t('sidebar.tree.all_saved_queries'),
|
||
key: 'all-saved-queries',
|
||
icon: <FolderOpenOutlined />,
|
||
type: 'all-saved-queries',
|
||
isLeaf: false,
|
||
selectable: false,
|
||
children,
|
||
};
|
||
};
|
||
|
||
const Sidebar: React.FC<{
|
||
onCreateConnection?: () => void;
|
||
onEditConnection?: (conn: SavedConnection) => void;
|
||
onOpenTools?: () => void;
|
||
onOpenSettings?: () => void;
|
||
onToggleAI?: () => void;
|
||
onToggleLogPanel?: () => void;
|
||
uiVersion?: 'legacy' | 'v2';
|
||
onFocusCommandSearch?: () => void;
|
||
}> = React.memo(({
|
||
onCreateConnection,
|
||
onEditConnection,
|
||
onOpenTools,
|
||
onOpenSettings,
|
||
onToggleAI,
|
||
onToggleLogPanel,
|
||
uiVersion,
|
||
onFocusCommandSearch,
|
||
}) => {
|
||
const connections = useStore(state => state.connections);
|
||
const savedQueries = useStore(state => state.savedQueries);
|
||
const externalSQLDirectories = useStore(state => state.externalSQLDirectories);
|
||
const saveQuery = useStore(state => state.saveQuery);
|
||
const deleteQuery = useStore(state => state.deleteQuery);
|
||
const saveExternalSQLDirectory = useStore(state => state.saveExternalSQLDirectory);
|
||
const deleteExternalSQLDirectory = useStore(state => state.deleteExternalSQLDirectory);
|
||
const addConnection = useStore(state => state.addConnection);
|
||
const addTab = useStore(state => state.addTab);
|
||
const updateQueryTabDraft = useStore(state => state.updateQueryTabDraft);
|
||
const tabs = useStore(state => state.tabs);
|
||
const activeTabId = useStore(state => state.activeTabId);
|
||
const setActiveContext = useStore(state => state.setActiveContext);
|
||
const removeConnection = useStore(state => state.removeConnection);
|
||
const connectionTags = useStore(state => state.connectionTags);
|
||
const sidebarRootOrder = useStore(state => state.sidebarRootOrder);
|
||
const addConnectionTag = useStore(state => state.addConnectionTag);
|
||
const updateConnectionTag = useStore(state => state.updateConnectionTag);
|
||
const removeConnectionTag = useStore(state => state.removeConnectionTag);
|
||
const moveConnectionToTag = useStore(state => state.moveConnectionToTag);
|
||
const reorderConnections = useStore(state => state.reorderConnections);
|
||
const reorderTags = useStore(state => state.reorderTags);
|
||
const reorderSidebarRoot = useStore(state => state.reorderSidebarRoot);
|
||
const closeTabsByConnection = useStore(state => state.closeTabsByConnection);
|
||
const closeTabsByDatabase = useStore(state => state.closeTabsByDatabase);
|
||
const theme = useStore(state => state.theme);
|
||
const appearance = useStore(state => state.appearance);
|
||
const activeContext = useStore(state => state.activeContext);
|
||
const tableAccessCount = useStore(state => state.tableAccessCount);
|
||
const tableSortPreference = useStore(state => state.tableSortPreference);
|
||
const pinnedSidebarTables = useStore(state => state.pinnedSidebarTables);
|
||
const recordTableAccess = useStore(state => state.recordTableAccess);
|
||
const setTableSortPreference = useStore(state => state.setTableSortPreference);
|
||
const setSidebarTablePinned = useStore(state => state.setSidebarTablePinned);
|
||
const queryOptions = useStore(state => state.queryOptions);
|
||
const setQueryOptions = useStore(state => state.setQueryOptions);
|
||
const addSqlLog = useStore(state => state.addSqlLog);
|
||
const sqlLogs = useStore(state => state.sqlLogs) || [];
|
||
const shortcutOptions = useStore(state => state.shortcutOptions);
|
||
const languagePreference = useStore(state => state.languagePreference);
|
||
const setAppearance = useStore(state => state.setAppearance);
|
||
const setAIPanelVisible = useStore(state => state.setAIPanelVisible);
|
||
const addAIContext = useStore(state => state.addAIContext);
|
||
void languagePreference;
|
||
const darkMode = theme === 'dark';
|
||
const resolvedAppearance = resolveAppearanceValues(appearance);
|
||
const opacity = normalizeOpacityForPlatform(resolvedAppearance.opacity);
|
||
const 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<TreeNode[]>([]);
|
||
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) => (
|
||
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
|
||
<div style={{ width: 34, height: 34, borderRadius: 12, display: 'grid', placeItems: 'center', background: overlayTheme.iconBg, color: overlayTheme.iconColor, flexShrink: 0 }}>
|
||
{icon}
|
||
</div>
|
||
<div style={{ minWidth: 0 }}>
|
||
<div style={{ fontSize: 16, fontWeight: 700, color: overlayTheme.titleText }}>{title}</div>
|
||
<div style={{ marginTop: 4, color: overlayTheme.mutedText, fontSize: 12, lineHeight: 1.6 }}>{description}</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
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<SearchScope[]>(['smart']);
|
||
const [v2ExplorerFilter, setV2ExplorerFilter] = useState<V2ExplorerFilter>('all');
|
||
const [isSearchScopePopoverOpen, setIsSearchScopePopoverOpen] = useState(false);
|
||
const searchInputRef = useRef<any>(null);
|
||
const commandSearchInputRef = useRef<any>(null);
|
||
const [isV2CommandSearchOpen, setIsV2CommandSearchOpen] = useState(false);
|
||
const [v2CommandSearchValue, setV2CommandSearchValue] = useState('');
|
||
const deferredV2CommandSearchValue = useDeferredValue(v2CommandSearchValue);
|
||
const [v2CommandActiveIndex, setV2CommandActiveIndex] = useState(0);
|
||
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
|
||
const [autoExpandParent, setAutoExpandParent] = useState(true);
|
||
const [loadedKeys, setLoadedKeys] = useState<React.Key[]>([]);
|
||
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||
const selectedNodesRef = useRef<any[]>([]);
|
||
const loadingNodesRef = useRef<Set<string>>(new Set());
|
||
const databaseTreeTouchedAtRef = useRef<Record<string, number>>({});
|
||
const pruneLoadedDatabaseTreesRef = useRef<() => void>(() => {});
|
||
const clickTimerRef = useRef<ReturnType<typeof setTimeout> | 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<Record<string, string>>({});
|
||
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<TreeNode | null>(() => {
|
||
return buildAllSavedQueriesTreeNode(savedQueries, connections);
|
||
}, [connections, savedQueries]);
|
||
const snapshotTreeSelectionBeforeDrag = useCallback(() => {
|
||
treeDragSelectionSnapshotRef.current = {
|
||
selectedKeys: [...selectedKeys],
|
||
selectedNodes: [...selectedNodesRef.current],
|
||
activeContext: activeContext ? { ...activeContext } : null,
|
||
};
|
||
}, [activeContext, selectedKeys]);
|
||
|
||
const restoreTreeSelectionAfterDrag = useCallback(() => {
|
||
const snapshot = treeDragSelectionSnapshotRef.current;
|
||
treeDragSelectSuppressUntilRef.current = Date.now() + 1000;
|
||
setSelectedKeys(snapshot.selectedKeys);
|
||
selectedNodesRef.current = snapshot.selectedNodes;
|
||
setActiveContext(snapshot.activeContext);
|
||
}, [setActiveContext]);
|
||
|
||
const openV2CommandSearch = useCallback(() => {
|
||
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]);
|
||
|
||
useEffect(() => {
|
||
if (!v2UseLegacySidebarFilter) {
|
||
return;
|
||
}
|
||
const nextFilter = searchValue.trim();
|
||
if (nextFilter !== v2PersistedSidebarFilter) {
|
||
setAppearance({ v2SidebarPersistedFilter: nextFilter });
|
||
}
|
||
}, [searchValue, setAppearance, v2PersistedSidebarFilter, v2UseLegacySidebarFilter]);
|
||
|
||
const handleV2CommandSearchValueChange = useCallback((value: string) => {
|
||
setV2CommandSearchValue(value);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!v2CommandSearchPersistentFilterEnabled) {
|
||
return;
|
||
}
|
||
if (!isV2CommandSearchOpen) {
|
||
return;
|
||
}
|
||
const nextFilter = resolveV2CommandSearchPersistentFilter({
|
||
commandSearchValue: deferredV2CommandSearchValue,
|
||
persistedFilter: v2PersistedSidebarFilter,
|
||
enabled: v2CommandSearchPersistentFilterEnabled,
|
||
isOpen: isV2CommandSearchOpen,
|
||
});
|
||
setSearchValue(nextFilter);
|
||
const timer = window.setTimeout(() => {
|
||
setAppearance({ v2SidebarPersistedFilter: nextFilter });
|
||
}, 160);
|
||
return () => window.clearTimeout(timer);
|
||
}, [deferredV2CommandSearchValue, isV2CommandSearchOpen, setAppearance, v2CommandSearchPersistentFilterEnabled, v2PersistedSidebarFilter]);
|
||
|
||
const toggleV2CommandSearchPersistentFilter = useCallback((enabled: boolean) => {
|
||
const nextFilter = enabled ? v2CommandSearchValue.trim() : '';
|
||
setSearchValue(nextFilter);
|
||
setAppearance({
|
||
v2CommandSearchPersistentFilterEnabled: enabled,
|
||
v2SidebarPersistedFilter: nextFilter,
|
||
});
|
||
message.success(
|
||
enabled
|
||
? t('sidebar.message.sidebar_filter_sync_enabled')
|
||
: t('sidebar.message.sidebar_filter_sync_disabled'),
|
||
);
|
||
}, [setAppearance, v2CommandSearchValue]);
|
||
|
||
const resetV2SidebarFilter = useCallback(() => {
|
||
setSearchValue('');
|
||
setAppearance({
|
||
v2CommandSearchPersistentFilterEnabled: false,
|
||
v2SidebarPersistedFilter: '',
|
||
});
|
||
message.success(t('sidebar.message.sidebar_filter_reset'));
|
||
}, [setAppearance]);
|
||
|
||
// Virtual Scroll State
|
||
const [treeHeight, setTreeHeight] = useState(500);
|
||
const [treeViewportWidth, setTreeViewportWidth] = useState(0);
|
||
const treeContainerRef = useRef<HTMLDivElement>(null);
|
||
const treeRef = useRef<any>(null);
|
||
const treeDataRef = useRef<TreeNode[]>([]);
|
||
const externalSQLDirectoryTreesRef = useRef<Record<string, ExternalSQLTreeEntry[]>>({});
|
||
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<Record<string, SidebarConnectionState>>({});
|
||
const [isTreeDragging, setIsTreeDragging] = useState(false);
|
||
|
||
// Create Database Modal
|
||
const [isCreateDbModalOpen, setIsCreateDbModalOpen] = useState(false);
|
||
const [createDbForm] = Form.useForm();
|
||
const [targetConnection, setTargetConnection] = useState<any>(null);
|
||
const [isCreateSchemaModalOpen, setIsCreateSchemaModalOpen] = useState(false);
|
||
const [createSchemaForm] = Form.useForm();
|
||
const [createSchemaTarget, setCreateSchemaTarget] = useState<any>(null);
|
||
const [isRenameSchemaModalOpen, setIsRenameSchemaModalOpen] = useState(false);
|
||
const [renameSchemaForm] = Form.useForm();
|
||
const [renameSchemaTarget, setRenameSchemaTarget] = useState<any>(null);
|
||
const [isRenameDbModalOpen, setIsRenameDbModalOpen] = useState(false);
|
||
const [renameDbForm] = Form.useForm();
|
||
const [renameDbTarget, setRenameDbTarget] = useState<any>(null);
|
||
const [isRenameTableModalOpen, setIsRenameTableModalOpen] = useState(false);
|
||
const [renameTableForm] = Form.useForm();
|
||
const [renameTableTarget, setRenameTableTarget] = useState<any>(null);
|
||
const [messagePublishTarget, setMessagePublishTarget] = useState<SidebarMessagePublishTarget | null>(null);
|
||
const [isRenameViewModalOpen, setIsRenameViewModalOpen] = useState(false);
|
||
const [renameViewForm] = Form.useForm();
|
||
const [renameViewTarget, setRenameViewTarget] = useState<any>(null);
|
||
const [isRenameSavedQueryModalOpen, setIsRenameSavedQueryModalOpen] = useState(false);
|
||
const [renameSavedQueryForm] = Form.useForm();
|
||
const [renameSavedQueryTarget, setRenameSavedQueryTarget] = useState<SavedQuery | null>(null);
|
||
// Connection Tag Modals
|
||
const [isCreateTagModalOpen, setIsCreateTagModalOpen] = useState(false);
|
||
const [createTagForm] = Form.useForm();
|
||
|
||
const {
|
||
isBatchModalOpen,
|
||
setIsBatchModalOpen,
|
||
batchTables,
|
||
checkedTableKeys,
|
||
setCheckedTableKeys,
|
||
selectedConnection,
|
||
selectedDatabase,
|
||
availableDatabases,
|
||
batchFilterKeyword,
|
||
setBatchFilterKeyword,
|
||
batchFilterType,
|
||
setBatchFilterType,
|
||
batchSelectionScope,
|
||
setBatchSelectionScope,
|
||
filteredBatchObjects,
|
||
groupedBatchObjects,
|
||
selectionScopeTargetKeys,
|
||
isBatchDbModalOpen,
|
||
setIsBatchDbModalOpen,
|
||
batchDatabases,
|
||
checkedDbKeys,
|
||
setCheckedDbKeys,
|
||
selectedDbConnection,
|
||
handleExportDatabaseSQL,
|
||
handleExportSchemaSQL,
|
||
openBatchOperationModal,
|
||
openBatchTableExportWorkbench,
|
||
handleConnectionChange,
|
||
handleDatabaseChange,
|
||
handleBatchExport,
|
||
handleBatchClear,
|
||
handleBatchDeleteTables,
|
||
handleCheckAll,
|
||
handleInvertSelection,
|
||
openBatchDatabaseModal,
|
||
openBatchDatabaseExportWorkbench,
|
||
handleDbConnectionChange,
|
||
handleBatchDbExport,
|
||
handleBatchDbDelete,
|
||
handleCheckAllDb,
|
||
handleInvertSelectionDb,
|
||
} = useSidebarBatchExport({
|
||
connections,
|
||
selectedNodesRef,
|
||
addTab,
|
||
addSqlLog,
|
||
});
|
||
// Find in Database Modal
|
||
const [findInDbContext, setFindInDbContext] = useState<{ open: boolean; connectionId: string; dbName: string }>({ open: false, connectionId: '', dbName: '' });
|
||
|
||
useEffect(() => {
|
||
if (!autoFetchVisible) {
|
||
return;
|
||
}
|
||
|
||
expandedKeys.forEach(key => {
|
||
const node = findTreeNodeByKey(treeData, key);
|
||
if (node && node.type === 'database') {
|
||
loadTables(node);
|
||
}
|
||
});
|
||
}, [autoFetchVisible, savedQueries]);
|
||
|
||
useEffect(() => {
|
||
const previousSignatures = connectionReloadSignaturesRef.current;
|
||
const nextSignatures: Record<string, string> = {};
|
||
const staleConnectionIds = new Set<string>();
|
||
|
||
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<string, TreeNode>();
|
||
|
||
// We need to recursively extract connections from old tag structures
|
||
// so if a user expands a connection that was tagged, the state remains
|
||
const recurseCollect = (nodes: TreeNode[]) => {
|
||
nodes.forEach((node) => {
|
||
if (node.type === 'tag') {
|
||
if (node.children) recurseCollect(node.children);
|
||
} else if (node.type === 'connection') {
|
||
prevMap.set(String(node.key), node);
|
||
}
|
||
});
|
||
};
|
||
recurseCollect(prev);
|
||
|
||
const buildConnectionNode = (conn: SavedConnection): TreeNode => {
|
||
const existing = prevMap.get(conn.id);
|
||
const iconType = resolveConnectionIconType(conn);
|
||
const iconColor = resolveConnectionAccentColor(conn);
|
||
const preserveChildren = existing && !staleConnectionIds.has(conn.id);
|
||
return {
|
||
title: conn.name,
|
||
key: conn.id,
|
||
icon: getDbIcon(iconType, iconColor, 22),
|
||
type: 'connection',
|
||
dataRef: conn,
|
||
isLeaf: false,
|
||
children: preserveChildren ? existing.children : undefined,
|
||
} as TreeNode;
|
||
};
|
||
|
||
const taggedConnIds = new Set<string>();
|
||
const tagNodesById = new Map<string, TreeNode>();
|
||
connectionTags.forEach((tag) => {
|
||
tag.connectionIds.forEach(id => taggedConnIds.add(id));
|
||
tagNodesById.set(tag.id, {
|
||
title: tag.name,
|
||
key: `tag-${tag.id}`,
|
||
icon: (
|
||
<span
|
||
className="gn-v2-tree-folder-icon"
|
||
data-sidebar-tree-folder-icon="true"
|
||
>
|
||
<FolderOutlined />
|
||
</span>
|
||
),
|
||
type: 'tag',
|
||
dataRef: tag,
|
||
isLeaf: false,
|
||
children: tag.connectionIds
|
||
.map(cid => connections.find(c => c.id === cid))
|
||
.filter(Boolean)
|
||
.map(conn => buildConnectionNode(conn!)),
|
||
} as TreeNode);
|
||
});
|
||
|
||
const ungroupedNodesById = new Map<string, TreeNode>();
|
||
connections
|
||
.filter(c => !taggedConnIds.has(c.id))
|
||
.forEach((conn) => {
|
||
ungroupedNodesById.set(conn.id, buildConnectionNode(conn));
|
||
});
|
||
|
||
const orderedRootTokens = resolveSidebarRootOrderTokens(
|
||
sidebarRootOrder,
|
||
connectionTags,
|
||
connections,
|
||
);
|
||
const orderedNodes: TreeNode[] = [];
|
||
orderedRootTokens.forEach((token) => {
|
||
if (token.startsWith('tag:')) {
|
||
const tagNode = tagNodesById.get(token.slice('tag:'.length));
|
||
if (!tagNode) return;
|
||
orderedNodes.push(tagNode);
|
||
tagNodesById.delete(token.slice('tag:'.length));
|
||
return;
|
||
}
|
||
if (token.startsWith('connection:')) {
|
||
const connectionNode = ungroupedNodesById.get(token.slice('connection:'.length));
|
||
if (!connectionNode) return;
|
||
orderedNodes.push(connectionNode);
|
||
ungroupedNodesById.delete(token.slice('connection:'.length));
|
||
}
|
||
});
|
||
|
||
orderedNodes.push(...Array.from(tagNodesById.values()));
|
||
orderedNodes.push(...Array.from(ungroupedNodesById.values()));
|
||
if (allSavedQueriesNode) {
|
||
orderedNodes.push(allSavedQueriesNode);
|
||
}
|
||
const externalSQLRootNode = prev.find((node) => node.type === 'external-sql-root');
|
||
return externalSQLRootNode ? [...orderedNodes, externalSQLRootNode] : orderedNodes;
|
||
});
|
||
}, [connections, connectionTags, sidebarRootOrder, allSavedQueriesNode]);
|
||
|
||
const handleDuplicateConnection = async (conn: SavedConnection) => {
|
||
if (!conn?.id) return;
|
||
|
||
const backendApp = (window as any).go?.app?.App;
|
||
if (typeof backendApp?.DuplicateConnection !== 'function') {
|
||
message.error(t('connection.sidebar.duplicate.backendUnavailable'));
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const duplicatedConnection = await backendApp.DuplicateConnection(conn.id);
|
||
if (!duplicatedConnection) {
|
||
throw new Error(t('connection.sidebar.duplicate.noResult'));
|
||
}
|
||
addConnection(duplicatedConnection);
|
||
message.success(t('connection.sidebar.duplicate.success', {
|
||
name: duplicatedConnection.name,
|
||
}));
|
||
} catch (error: any) {
|
||
message.error(error?.message || t('connection.sidebar.duplicate.failureFallback'));
|
||
}
|
||
};
|
||
const updateTreeData = (list: TreeNode[], key: React.Key, children: TreeNode[] | undefined): TreeNode[] => {
|
||
return list.map(node => {
|
||
if (node.key === key) {
|
||
return { ...node, children };
|
||
}
|
||
if (node.children) {
|
||
return { ...node, children: updateTreeData(node.children, key, children) };
|
||
}
|
||
return node;
|
||
});
|
||
};
|
||
|
||
const findTreeNodeByKey = (nodes: TreeNode[], targetKey: React.Key): TreeNode | null => {
|
||
for (const node of nodes) {
|
||
if (node.key === targetKey) {
|
||
return node;
|
||
}
|
||
if (node.children) {
|
||
const child = findTreeNodeByKey(node.children, targetKey);
|
||
if (child) {
|
||
return child;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
};
|
||
|
||
findTreeNodeByKeyRef.current = findTreeNodeByKey;
|
||
|
||
const replaceTreeNodeChildren = (key: React.Key, children: TreeNode[] | undefined): TreeNode[] => {
|
||
const nextTreeData = updateTreeData(treeDataRef.current, key, children);
|
||
treeDataRef.current = nextTreeData;
|
||
setTreeData(nextTreeData);
|
||
return nextTreeData;
|
||
};
|
||
|
||
const 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 <FolderOpenOutlined />;
|
||
case 'external-sql-directory':
|
||
return <HddOutlined />;
|
||
case 'external-sql-folder':
|
||
return <FolderOutlined />;
|
||
default:
|
||
return <FileTextOutlined />;
|
||
}
|
||
})();
|
||
|
||
return {
|
||
...node,
|
||
icon,
|
||
children: node.children?.map((child) => decorateExternalSQLTreeNode(child)),
|
||
};
|
||
};
|
||
|
||
const buildExternalSQLRootTreeNode = useCallback((
|
||
directories: ExternalSQLDirectory[] = externalSQLDirectories,
|
||
directoryTrees: Record<string, ExternalSQLTreeEntry[]> = externalSQLDirectoryTreesRef.current,
|
||
): TreeNode => decorateExternalSQLTreeNode(buildExternalSQLRootNode({
|
||
directories,
|
||
directoryTrees,
|
||
})), [externalSQLDirectories]);
|
||
|
||
const refreshGlobalExternalSQLRootNode = useCallback(async (
|
||
showSuccess = false,
|
||
directoriesOverride?: ExternalSQLDirectory[],
|
||
) => {
|
||
const targetDirectories = directoriesOverride || externalSQLDirectories;
|
||
const directoryTrees: Record<string, ExternalSQLTreeEntry[]> = {};
|
||
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,
|
||
refreshGlobalExternalSQLRootNode,
|
||
setExpandedKeys,
|
||
setAutoExpandParent,
|
||
getActiveContext: () => useStore.getState().activeContext,
|
||
});
|
||
|
||
const getNodeDatabaseContext = (node: any): { connectionId: string; dbName: string; dbNodeKey: string } | null => {
|
||
if (!node) return null;
|
||
if (node.type === 'database') {
|
||
return {
|
||
connectionId: String(node?.dataRef?.id || '').trim(),
|
||
dbName: String(node?.dataRef?.dbName || '').trim(),
|
||
dbNodeKey: String(node.key || '').trim(),
|
||
};
|
||
}
|
||
|
||
if (
|
||
node.type === 'external-sql-root'
|
||
|| node.type === 'external-sql-directory'
|
||
|| node.type === 'external-sql-folder'
|
||
|| node.type === 'external-sql-file'
|
||
) {
|
||
return {
|
||
connectionId: String(node?.dataRef?.connectionId || '').trim(),
|
||
dbName: String(node?.dataRef?.dbName || '').trim(),
|
||
dbNodeKey: String(node?.dataRef?.dbNodeKey || '').trim(),
|
||
};
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
const locateObjectInSidebarRef = useRef<(detail: unknown) => Promise<void>>(async () => {});
|
||
|
||
const waitForSidebarLoadKey = async (loadKey: string): Promise<boolean> => {
|
||
for (let attempt = 0; attempt < SIDEBAR_LOCATE_LOAD_WAIT_ATTEMPTS && loadingNodesRef.current.has(loadKey); attempt += 1) {
|
||
await new Promise(resolve => window.setTimeout(resolve, SIDEBAR_LOCATE_LOAD_WAIT_INTERVAL_MS));
|
||
}
|
||
return !loadingNodesRef.current.has(loadKey);
|
||
};
|
||
|
||
const locateObjectInSidebar = async (detail: unknown) => {
|
||
const request = normalizeSidebarLocateObjectRequest(detail);
|
||
if (!request) {
|
||
message.warning(t('sidebar.message.locate_current_table_unavailable'));
|
||
return;
|
||
}
|
||
|
||
if (request.objectGroup === 'externalSqlFiles') {
|
||
await refreshGlobalExternalSQLRootNode(false);
|
||
const target = resolveSidebarLocateTarget(request, { groupBySchema: false });
|
||
const path = findSidebarNodePathForLocate(treeDataRef.current as SidebarLocateTreeNodeLike[], target);
|
||
if (!path) {
|
||
message.warning(t('sidebar.message.locate_external_sql_file_not_found', { path: request.filePath }));
|
||
return;
|
||
}
|
||
const targetKey = path[path.length - 1];
|
||
const targetNode = findTreeNodeByKey(treeDataRef.current, targetKey);
|
||
setSearchValue('');
|
||
mergeExpandedTreeKeys(path.slice(0, -1));
|
||
setSelectedKeys([targetKey]);
|
||
selectedNodesRef.current = targetNode ? [targetNode] : [];
|
||
const connectionId = String(request.connectionId || activeContext?.connectionId || activeTab?.connectionId || '').trim();
|
||
const dbName = String(request.dbName || activeContext?.dbName || activeTab?.dbName || '').trim();
|
||
if (connectionId) {
|
||
setActiveContext({ connectionId, dbName });
|
||
}
|
||
scrollSidebarTreeToKey(targetKey);
|
||
return;
|
||
}
|
||
|
||
const conn = connections.find(item => item.id === request.connectionId);
|
||
if (!conn) {
|
||
message.warning(t('sidebar.message.locate_connection_not_found_for_object'));
|
||
return;
|
||
}
|
||
|
||
const target = resolveSidebarLocateTarget(request, {
|
||
groupBySchema: shouldHideSchemaPrefix(conn),
|
||
});
|
||
const objectLabel = request.objectGroup === 'materializedViews'
|
||
? t('sidebar.locate.object.materialized_view')
|
||
: request.objectGroup === 'views'
|
||
? t('sidebar.locate.object.view')
|
||
: request.objectGroup === 'triggers'
|
||
? t('sidebar.locate.object.trigger')
|
||
: request.objectGroup === 'routines'
|
||
? t('sidebar.locate.object.routine')
|
||
: t('sidebar.locate.object.table');
|
||
|
||
let path = findSidebarNodePathForLocate(treeDataRef.current as SidebarLocateTreeNodeLike[], target);
|
||
const dbLoadKey = `dbs-${request.connectionId}`;
|
||
const tableLoadKey = `tables-${request.connectionId}-${request.dbName}`;
|
||
|
||
if (!path && !findSidebarNodePathByKey(treeDataRef.current as SidebarLocateTreeNodeLike[], target.databaseKey)) {
|
||
const connectionNode = findTreeNodeByKey(treeDataRef.current, target.connectionKey);
|
||
if (!connectionNode) {
|
||
message.warning(t('sidebar.message.locate_connection_not_in_tree'));
|
||
return;
|
||
}
|
||
if (loadingNodesRef.current.has(dbLoadKey)) {
|
||
const loaded = await waitForSidebarLoadKey(dbLoadKey);
|
||
if (!loaded) {
|
||
message.info(t('sidebar.message.locate_database_loading', { database: request.dbName }));
|
||
return;
|
||
}
|
||
} else {
|
||
await loadDatabases(connectionNode);
|
||
}
|
||
}
|
||
|
||
const dbNode = findTreeNodeByKey(treeDataRef.current, target.databaseKey);
|
||
if (!dbNode) {
|
||
message.warning(t('sidebar.message.locate_database_not_found', { database: request.dbName }));
|
||
return;
|
||
}
|
||
|
||
path = findSidebarNodePathForLocate(treeDataRef.current as SidebarLocateTreeNodeLike[], target);
|
||
if (!path) {
|
||
if (loadingNodesRef.current.has(tableLoadKey)) {
|
||
const loaded = await waitForSidebarLoadKey(tableLoadKey);
|
||
if (!loaded) {
|
||
message.info(t('sidebar.message.locate_object_loading', {
|
||
object: objectLabel,
|
||
database: request.dbName,
|
||
}));
|
||
return;
|
||
}
|
||
} else {
|
||
await loadTables(dbNode);
|
||
}
|
||
path = findSidebarNodePathForLocate(treeDataRef.current as SidebarLocateTreeNodeLike[], target);
|
||
}
|
||
|
||
if (!path) {
|
||
message.warning(t('sidebar.message.locate_object_not_found', {
|
||
object: objectLabel,
|
||
name: request.tableName,
|
||
}));
|
||
return;
|
||
}
|
||
|
||
const targetKey = path[path.length - 1];
|
||
const targetNode = findTreeNodeByKey(treeDataRef.current, targetKey);
|
||
setSearchValue('');
|
||
mergeExpandedTreeKeys(path.slice(0, -1));
|
||
setSelectedKeys([targetKey]);
|
||
selectedNodesRef.current = targetNode ? [targetNode] : [];
|
||
setActiveContext({ connectionId: request.connectionId, dbName: request.dbName });
|
||
scrollSidebarTreeToKey(targetKey);
|
||
};
|
||
|
||
const handleLocateActiveTabInSidebar = () => {
|
||
if (!activeTabLocateRequest) {
|
||
message.warning(t('sidebar.message.locate_current_table_unavailable'));
|
||
return;
|
||
}
|
||
void locateObjectInSidebar(activeTabLocateRequest);
|
||
};
|
||
|
||
useEffect(() => {
|
||
locateObjectInSidebarRef.current = locateObjectInSidebar;
|
||
});
|
||
|
||
useEffect(() => {
|
||
const handleLocateSidebarObject = (event: Event) => {
|
||
void locateObjectInSidebarRef.current((event as CustomEvent).detail);
|
||
};
|
||
window.addEventListener('gonavi:locate-sidebar-object', handleLocateSidebarObject as EventListener);
|
||
return () => {
|
||
window.removeEventListener('gonavi:locate-sidebar-object', handleLocateSidebarObject as EventListener);
|
||
};
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
const handleSidebarTablePinChanged = (event: Event) => {
|
||
const detail = (event as CustomEvent).detail || {};
|
||
const connectionId = String(detail.connectionId || '').trim();
|
||
const dbName = String(detail.dbName || '').trim();
|
||
if (!connectionId || !dbName) return;
|
||
const dbNode = findTreeNodeByKeyRef.current(treeDataRef.current, `${connectionId}-${dbName}`);
|
||
if (dbNode) {
|
||
void loadTables(dbNode);
|
||
}
|
||
};
|
||
window.addEventListener('gonavi:sidebar-table-pin-changed', handleSidebarTablePinChanged as EventListener);
|
||
return () => {
|
||
window.removeEventListener('gonavi:sidebar-table-pin-changed', handleSidebarTablePinChanged as EventListener);
|
||
};
|
||
}, []);
|
||
|
||
const onLoadData = async ({ key, children, dataRef, type }: any) => {
|
||
if (type === 'tag' || type === 'all-saved-queries' || type === 'saved-query-group' || type === 'unmatched-saved-queries') return;
|
||
if (hasSidebarLazyChildren(children)) return;
|
||
|
||
if (type === 'connection') {
|
||
await loadDatabases({ key, dataRef });
|
||
} else if (type === 'jvm-mode' || type === 'jvm-resource') {
|
||
await loadJVMResources({ key, dataRef });
|
||
} else if (type === 'database') {
|
||
await loadTables({ key, dataRef });
|
||
} else if (type === 'external-sql-root') {
|
||
await refreshGlobalExternalSQLRootNode(false);
|
||
} else if (type === 'table') {
|
||
// Expand table to show object categories
|
||
const conn = dataRef;
|
||
|
||
const folders: TreeNode[] = [
|
||
{
|
||
title: t('sidebar.table_folder.columns'),
|
||
key: `${key}-columns`,
|
||
icon: <UnorderedListOutlined />,
|
||
type: 'folder-columns',
|
||
isLeaf: true,
|
||
dataRef: conn
|
||
},
|
||
{
|
||
title: t('sidebar.table_folder.indexes'),
|
||
key: `${key}-indexes`,
|
||
icon: <KeyOutlined style={{ transform: 'rotate(45deg)' }} />,
|
||
type: 'folder-indexes',
|
||
isLeaf: true,
|
||
dataRef: conn
|
||
},
|
||
{
|
||
title: t('sidebar.table_folder.foreign_keys'),
|
||
key: `${key}-fks`,
|
||
icon: <LinkOutlined />,
|
||
type: 'folder-fks',
|
||
isLeaf: true,
|
||
dataRef: conn
|
||
},
|
||
{
|
||
title: t('sidebar.table_folder.triggers'),
|
||
key: `${key}-triggers`,
|
||
icon: <ThunderboltOutlined />,
|
||
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 <CaretDownFilled rotate={keepCollapsed ? -90 : undefined} />;
|
||
}, []);
|
||
|
||
|
||
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: <DashboardOutlined />,
|
||
type: 'jvm-diagnostic',
|
||
dataRef: {
|
||
...conn,
|
||
diagnosticTransport: descriptor.transport,
|
||
},
|
||
isLeaf: true,
|
||
}];
|
||
};
|
||
|
||
const openJVMResourceTab = (conn: SavedConnection, providerMode: string, resourcePath: string, resourceKind?: string) => {
|
||
const trimmedResourcePath = String(resourcePath || '').trim();
|
||
addTab({
|
||
id: `jvm-resource-${conn.id}-${providerMode}-${encodeURIComponent(trimmedResourcePath)}`,
|
||
title: trimmedResourcePath
|
||
? `${buildJVMTabTitle(conn.name, 'resource', providerMode)} · ${trimmedResourcePath}`
|
||
: buildJVMTabTitle(conn.name, 'resource', providerMode),
|
||
type: 'jvm-resource',
|
||
connectionId: conn.id,
|
||
providerMode: providerMode as 'jmx' | 'endpoint' | 'agent',
|
||
resourcePath: trimmedResourcePath,
|
||
resourceKind,
|
||
});
|
||
};
|
||
|
||
const openJVMDiagnosticTab = (conn: SavedConnection) => {
|
||
const transport = conn.config.jvm?.diagnostic?.transport || 'agent-bridge';
|
||
addTab({
|
||
id: `jvm-diagnostic-${conn.id}`,
|
||
title: buildJVMTabTitle(conn.name, 'diagnostic', transport),
|
||
type: 'jvm-diagnostic',
|
||
connectionId: conn.id,
|
||
});
|
||
};
|
||
|
||
const getConnectionNodeRef = (connRef: any) => {
|
||
const latestConn = connections.find(c => c.id === connRef.id);
|
||
return { key: connRef.id, dataRef: latestConn || connRef };
|
||
};
|
||
|
||
const getDatabaseNodeRef = (connRef: any, dbName: string) => {
|
||
const latestConn = connections.find(c => c.id === connRef.id);
|
||
return {
|
||
key: `${connRef.id}-${dbName}`,
|
||
dataRef: { ...(latestConn || connRef), dbName }
|
||
};
|
||
};
|
||
|
||
const extractObjectName = (fullName: string) => {
|
||
return splitQualifiedName(String(fullName || '').trim()).objectName || String(fullName || '').trim();
|
||
};
|
||
|
||
|
||
const resolveSavedQueryDisplayName = (name: string | null | undefined) => {
|
||
const rawName = String(name || '').trim();
|
||
return rawName || t('query_editor.save_modal.unnamed');
|
||
};
|
||
|
||
const {
|
||
loadDatabases,
|
||
loadJVMResources,
|
||
loadTables,
|
||
} = useSidebarTreeLoaders({
|
||
savedQueries,
|
||
tableSortPreference,
|
||
tableAccessCount,
|
||
pinnedSidebarTables,
|
||
isV2Ui,
|
||
loadingNodesRef,
|
||
setConnectionStates,
|
||
setLoadedKeys,
|
||
replaceTreeNodeChildren,
|
||
buildRuntimeConfig,
|
||
buildJVMRuntimeConfig,
|
||
buildJVMDiagnosticTreeNodes,
|
||
resolveSavedQueryDisplayName,
|
||
onDatabaseTreeLoaded: (databaseKey: string) => {
|
||
databaseTreeTouchedAtRef.current[databaseKey] = Date.now();
|
||
pruneLoadedDatabaseTrees();
|
||
},
|
||
});
|
||
|
||
const {
|
||
handleCopyStructure,
|
||
handleCopyTableName,
|
||
handleExport,
|
||
openExportDialog,
|
||
handleCopyTableAsInsert,
|
||
openTableDdlInDesigner,
|
||
openTableInERView,
|
||
injectTablePromptToAI,
|
||
handleCreateDatabase,
|
||
openCreateSchemaModal,
|
||
handleCreateSchema,
|
||
openRenameSchemaModal,
|
||
handleRenameSchema,
|
||
handleDeleteSchema,
|
||
handleRenameDatabase,
|
||
handleDeleteDatabase,
|
||
handleRenameTable,
|
||
handleDeleteTable,
|
||
handleTableDataDangerAction,
|
||
openViewDefinition,
|
||
openEditView,
|
||
openCreateView,
|
||
openCreateStarRocksMaterializedView,
|
||
openCreateStarRocksExternalCatalog,
|
||
openCreateStarRocksRollup,
|
||
handleDropView,
|
||
handleRenameView,
|
||
openRenameSavedQueryModal,
|
||
handleRenameSavedQuery,
|
||
isSavedQueryUnmatched,
|
||
handleRebindSavedQuery,
|
||
openRoutineDefinition,
|
||
openEventDefinition,
|
||
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,
|
||
});
|
||
|
||
|
||
|
||
const refreshV2TableContextMenuStatsRef = useRef<(node: any) => void>(() => {});
|
||
|
||
const {
|
||
getConnectionNodeForAction,
|
||
toggleSidebarTablePinned,
|
||
handleV2TableContextMenuAction,
|
||
handleTableGroupSortAction,
|
||
handleV2TableGroupContextMenuAction,
|
||
handleV2DatabaseContextMenuAction,
|
||
disconnectConnectionNode,
|
||
deleteConnectionNode,
|
||
handleV2ConnectionContextMenuAction,
|
||
handleV2ConnectionGroupContextMenuAction,
|
||
} = useSidebarV2ActionHandlers({
|
||
connections,
|
||
connectionTags,
|
||
pinnedSidebarTables,
|
||
loadingNodesRef,
|
||
treeDataRef,
|
||
findTreeNodeByKeyRef,
|
||
refreshV2TableContextMenuStatsRef,
|
||
setConnectionStates,
|
||
setExpandedKeys,
|
||
setLoadedKeys,
|
||
setTargetConnection,
|
||
setIsCreateDbModalOpen,
|
||
setRenameDbTarget,
|
||
setIsRenameDbModalOpen,
|
||
setRenameTableTarget,
|
||
setIsRenameTableModalOpen,
|
||
setRenameViewTarget,
|
||
setIsCreateTagModalOpen,
|
||
renameDbForm,
|
||
renameTableForm,
|
||
createTagForm,
|
||
addTab,
|
||
closeTabsByDatabase,
|
||
closeTabsByConnection,
|
||
removeConnection,
|
||
removeConnectionTag,
|
||
moveConnectionToTag,
|
||
setSidebarTablePinned,
|
||
setTableSortPreference,
|
||
replaceTreeNodeChildren,
|
||
loadDatabases,
|
||
loadTables,
|
||
getDatabaseNodeRef,
|
||
extractObjectName,
|
||
openDesign,
|
||
openNewTableDesign,
|
||
onDoubleClick,
|
||
openMessagePublishModal,
|
||
openTableDdlInDesigner,
|
||
openTableInERView,
|
||
handleCopyTableName,
|
||
handleCopyStructure,
|
||
handleCopyTableAsInsert,
|
||
openCreateStarRocksRollup,
|
||
handleExport,
|
||
openExportDialog,
|
||
injectTablePromptToAI,
|
||
handleTableDataDangerAction,
|
||
handleDeleteTable,
|
||
openCreateSchemaModal,
|
||
openCreateStarRocksMaterializedView,
|
||
openCreateStarRocksExternalCatalog,
|
||
handleExportDatabaseSQL,
|
||
handleRunSQLFile,
|
||
handleDeleteDatabase,
|
||
onEditConnection,
|
||
handleDuplicateConnection,
|
||
buildConnectionRootQueryTabTitle,
|
||
buildConnectionRootRedisCommandTabTitle,
|
||
buildConnectionRootRedisMonitorTabTitle,
|
||
});
|
||
const {
|
||
onSearch,
|
||
searchScopeSummary,
|
||
searchScopePopoverContent,
|
||
displayTreeData,
|
||
v2CommandSearchObjectMode,
|
||
v2CommandSearchAiMode,
|
||
filteredCommandSearchTreeItems,
|
||
filteredCommandSearchActionItems,
|
||
filteredCommandSearchRecentItems,
|
||
commandSearchAiItem,
|
||
commandSearchFlatItems,
|
||
flattenConnectionNodes,
|
||
activeConnection,
|
||
activeConnectionDisplayName,
|
||
activeDatabaseDisplayName,
|
||
v2VisibleTreeData,
|
||
v2TreeHorizontalScrollWidth,
|
||
effectiveTreeHeight,
|
||
v2TreeMetrics,
|
||
activeConnectionObjectCount,
|
||
} = useSidebarSearchModel({
|
||
searchScopes,
|
||
setSearchScopes,
|
||
setSearchValue,
|
||
deferredSearchValue,
|
||
deferredV2CommandSearchValue,
|
||
v2CommandSearchValue,
|
||
setV2CommandActiveIndex,
|
||
v2ExplorerFilter,
|
||
sidebarTableMetadataFields,
|
||
treeData,
|
||
treeViewportWidth,
|
||
treeHeight,
|
||
isV2Ui,
|
||
isV2CommandSearchOpen,
|
||
connections,
|
||
connectionIds,
|
||
selectedKeys,
|
||
selectedNodesRef,
|
||
activeContext,
|
||
activeTab,
|
||
sqlLogs,
|
||
shortcutOptions,
|
||
activeShortcutPlatform,
|
||
overlayTheme,
|
||
darkMode,
|
||
onCreateConnection,
|
||
onToggleAI,
|
||
onToggleLogPanel,
|
||
setAIPanelVisible,
|
||
extractObjectName,
|
||
});
|
||
const legacyToolbarButtonColor = darkMode ? 'rgba(255,255,255,0.65)' : 'rgba(0,0,0,0.65)';
|
||
const legacyToolbarStyle: React.CSSProperties = {
|
||
padding: '6px 16px',
|
||
display: 'grid',
|
||
gridTemplateColumns: 'repeat(5, minmax(0, 1fr))',
|
||
gap: 8,
|
||
alignItems: 'center',
|
||
justifyItems: 'center',
|
||
borderTop: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}`,
|
||
borderBottom: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}`,
|
||
background: darkMode ? 'rgba(0,0,0,0.2)' : 'rgba(0,0,0,0.015)',
|
||
};
|
||
const legacyToolbarItemStyle: React.CSSProperties = {
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
minWidth: 0,
|
||
};
|
||
const legacyToolbarDisabledWrapStyle: React.CSSProperties = {
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
};
|
||
|
||
const {
|
||
contextMenu,
|
||
setContextMenu,
|
||
contextMenuPortalRef,
|
||
buildRailConnectionStatus,
|
||
openV2ConnectionContextMenu,
|
||
getV2TreeMetaText,
|
||
renderV2SidebarContextMenuContent,
|
||
fetchV2TableContextMenuStats,
|
||
refreshV2TableContextMenuStats,
|
||
} = useSidebarV2ContextMenu({
|
||
connections,
|
||
connectionStates,
|
||
connectionTags,
|
||
activeShortcutPlatform,
|
||
flattenConnectionNodes,
|
||
v2TreeMetrics,
|
||
tableSortPreference,
|
||
pinnedSidebarTables,
|
||
getConnectionNodeForAction,
|
||
buildRuntimeConfig,
|
||
extractObjectName,
|
||
isPostgresSchemaDialect,
|
||
loadTables,
|
||
getDatabaseNodeRef,
|
||
handleExportSchemaSQL,
|
||
handleDeleteSchema,
|
||
openRenameSchemaModal,
|
||
resolveMessagePublishTarget,
|
||
addSqlLog,
|
||
handleV2TableContextMenuAction,
|
||
handleV2TableGroupContextMenuAction,
|
||
handleV2DatabaseContextMenuAction,
|
||
handleV2ConnectionContextMenuAction,
|
||
handleV2ConnectionGroupContextMenuAction,
|
||
});
|
||
refreshV2TableContextMenuStatsRef.current = refreshV2TableContextMenuStats;
|
||
|
||
const renderV2TreeTitle = (node: any, hoverTitle: string, statusBadge: React.ReactNode) => renderSidebarV2TreeTitle({
|
||
node,
|
||
hoverTitle,
|
||
statusBadge,
|
||
getV2TreeMetaText,
|
||
sidebarTableMetadataFields,
|
||
toggleSidebarTablePinned,
|
||
snapshotTreeSelectionBeforeDrag,
|
||
restoreTreeSelectionAfterDrag,
|
||
treeDragSelectSuppressUntilRef,
|
||
setIsTreeDragging,
|
||
});
|
||
|
||
const {
|
||
selectConnectionFromRail,
|
||
runCommandSearchItem,
|
||
handleV2CommandSearchKeyDown,
|
||
} = useSidebarCommandSearchRunner({
|
||
activeContext,
|
||
activeTab,
|
||
addTab,
|
||
closeV2CommandSearch,
|
||
commandSearchFlatItems,
|
||
connectionIds,
|
||
findTreeNodeByKeyRef,
|
||
locateObjectInSidebar,
|
||
loadDatabases,
|
||
mergeExpandedTreeKeys,
|
||
onDoubleClick,
|
||
scrollSidebarTreeToKey,
|
||
selectedNodesRef,
|
||
setActiveContext,
|
||
setSelectedKeys,
|
||
setV2CommandActiveIndex,
|
||
treeDataRef,
|
||
v2CommandActiveIndex,
|
||
});
|
||
expandConnectionFromRailRef.current = (connectionId: string) => {
|
||
const conn = connections.find((item) => item.id === connectionId);
|
||
if (conn) {
|
||
selectConnectionFromRail(conn);
|
||
}
|
||
};
|
||
|
||
const getNodeMenuItems = (node: any): MenuProps['items'] => buildSidebarLegacyNodeMenuItems(node, {
|
||
addTab,
|
||
getMetadataDialect,
|
||
handleV2DatabaseContextMenuAction,
|
||
isPostgresSchemaDialect,
|
||
handleExportSchemaSQL,
|
||
openRenameSchemaModal,
|
||
loadTables,
|
||
getDatabaseNodeRef,
|
||
handleDeleteSchema,
|
||
tableSortPreference,
|
||
isStructureOnlyDbType,
|
||
openNewTableDesign,
|
||
handleTableGroupSortAction,
|
||
openCreateView,
|
||
openCreateStarRocksMaterializedView,
|
||
openCreateRoutine,
|
||
createTagForm,
|
||
setRenameViewTarget,
|
||
setIsCreateTagModalOpen,
|
||
removeConnectionTag,
|
||
setExpandedKeys,
|
||
setLoadedKeys,
|
||
loadingNodesRef,
|
||
loadDatabases,
|
||
buildConnectionRootRedisCommandTabTitle,
|
||
buildConnectionRootRedisMonitorTabTitle,
|
||
onEditConnection,
|
||
handleDuplicateConnection,
|
||
disconnectConnectionNode,
|
||
deleteConnectionNode,
|
||
connectionTags,
|
||
moveConnectionToTag,
|
||
setTargetConnection,
|
||
setIsCreateDbModalOpen,
|
||
buildConnectionRootQueryTabTitle,
|
||
handleRunSQLFile,
|
||
openCreateStarRocksExternalCatalog,
|
||
openEditView,
|
||
renameViewForm,
|
||
setIsRenameViewModalOpen,
|
||
handleDropView,
|
||
onDoubleClick,
|
||
openViewDefinition,
|
||
openRoutineDefinition,
|
||
openEditRoutine,
|
||
handleDropRoutine,
|
||
openEventDefinition,
|
||
openEditEvent,
|
||
openSequenceDefinition,
|
||
openPackageDefinition,
|
||
resolveMessagePublishTarget,
|
||
openMessagePublishModal,
|
||
openDesign,
|
||
openCreateStarRocksRollup,
|
||
handleCopyTableName,
|
||
handleCopyStructure,
|
||
handleExport,
|
||
setRenameTableTarget,
|
||
renameTableForm,
|
||
setIsRenameTableModalOpen,
|
||
handleTableDataDangerAction,
|
||
handleDeleteTable,
|
||
openExportDialog,
|
||
isSavedQueryUnmatched,
|
||
connections,
|
||
handleRebindSavedQuery,
|
||
openRenameSavedQueryModal,
|
||
resolveSavedQueryDisplayName,
|
||
deleteQuery,
|
||
treeDataRef,
|
||
setTreeData,
|
||
handleAddExternalSQLDirectory,
|
||
openCreateExternalSQLFileModal,
|
||
openCreateExternalSQLDirectoryModal,
|
||
openRenameExternalSQLDirectoryModal,
|
||
handleRefreshExternalSQLDirectory,
|
||
handleDeleteExternalSQLDirectory,
|
||
handleRemoveExternalSQLDirectory,
|
||
openExternalSQLFile,
|
||
openRenameExternalSQLFileModal,
|
||
handleDeleteExternalSQLFile,
|
||
extractObjectName,
|
||
});
|
||
|
||
const titleRender = useSidebarTitleRender({
|
||
connectionStates,
|
||
isV2Ui,
|
||
renderV2TreeTitle,
|
||
handleAddExternalSQLDirectory,
|
||
snapshotTreeSelectionBeforeDrag,
|
||
restoreTreeSelectionAfterDrag,
|
||
treeDragSelectSuppressUntilRef,
|
||
setIsTreeDragging,
|
||
});
|
||
const handleDrop = (info: any) => {
|
||
setIsTreeDragging(false);
|
||
const dropPosition = normalizeSidebarTreeRelativeDropPosition(
|
||
Number(info.dropPosition || 0),
|
||
info?.node?.pos,
|
||
);
|
||
const domDropNode = resolveSidebarDropNodeFromDomEvent(info?.event);
|
||
const dropTargetMetrics = resolveSidebarDropTargetMetricsFromDomEvent(info?.event);
|
||
const insertBefore = resolveSidebarDropInsertBefore(dropPosition, dropTargetMetrics ? {
|
||
clientY: info?.event?.clientY,
|
||
top: dropTargetMetrics.top,
|
||
height: dropTargetMetrics.height,
|
||
} : null);
|
||
|
||
const dragNode = info.dragNode;
|
||
const dropNode = domDropNode && domDropNode.key === String(info?.node?.key || '')
|
||
? info.node
|
||
: (domDropNode
|
||
? findTreeNodeByKeyRef.current(treeDataRef.current, domDropNode.key) || info.node
|
||
: info.node);
|
||
|
||
const getDropRootToken = (node: any): string => {
|
||
if (!node) return '';
|
||
if (node.type === 'tag') {
|
||
return buildSidebarRootTagToken(String(node?.dataRef?.id || ''));
|
||
}
|
||
if (node.type === 'connection') {
|
||
const groupedTagId = connectionTags.find((tag) =>
|
||
tag.connectionIds.includes(String(node.key)),
|
||
)?.id || '';
|
||
return groupedTagId
|
||
? buildSidebarRootTagToken(groupedTagId)
|
||
: buildSidebarRootConnectionToken(String(node.key));
|
||
}
|
||
return '';
|
||
};
|
||
|
||
// Root tag or ungrouped connection reordering
|
||
if (dragNode.type === 'tag') {
|
||
if (dropNode.type === 'tag' || dropNode.type === 'connection') {
|
||
const currentTagOrder = connectionTags.map(t => t.id);
|
||
const dragTagId = dragNode.dataRef.id;
|
||
const dropTagId = dropNode.type === 'tag'
|
||
? dropNode.dataRef.id
|
||
: (connectionTags.find(t => t.connectionIds.includes(String(dropNode.key)))?.id || '');
|
||
const dragRootToken = buildSidebarRootTagToken(String(dragTagId));
|
||
const dropRootToken = getDropRootToken(dropNode);
|
||
|
||
if (dropRootToken && dropRootToken !== dragRootToken) {
|
||
if (dropTagId) {
|
||
const resolvedInsertBefore = resolveSidebarTagDropInsertBefore({
|
||
currentTagOrder,
|
||
dragTagId,
|
||
dropTagId,
|
||
relativeDropPosition: dropPosition,
|
||
fallbackInsertBefore: insertBefore,
|
||
metrics: dropTargetMetrics ? {
|
||
clientY: info?.event?.clientY,
|
||
top: dropTargetMetrics.top,
|
||
height: dropTargetMetrics.height,
|
||
} : null,
|
||
});
|
||
reorderSidebarRoot(dragRootToken, dropRootToken, resolvedInsertBefore);
|
||
} else {
|
||
reorderSidebarRoot(dragRootToken, dropRootToken, insertBefore);
|
||
}
|
||
return;
|
||
}
|
||
|
||
const newOrder = currentTagOrder.filter(id => id !== dragTagId);
|
||
let insertIndex = newOrder.length;
|
||
if (dropTagId) {
|
||
const dropIndex = newOrder.indexOf(dropTagId);
|
||
const resolvedInsertBefore = resolveSidebarTagDropInsertBefore({
|
||
currentTagOrder,
|
||
dragTagId,
|
||
dropTagId,
|
||
relativeDropPosition: dropPosition,
|
||
fallbackInsertBefore: insertBefore,
|
||
metrics: dropTargetMetrics ? {
|
||
clientY: info?.event?.clientY,
|
||
top: dropTargetMetrics.top,
|
||
height: dropTargetMetrics.height,
|
||
} : null,
|
||
});
|
||
|
||
if (resolvedInsertBefore) {
|
||
insertIndex = dropIndex;
|
||
} else {
|
||
insertIndex = dropIndex + 1;
|
||
}
|
||
} else {
|
||
// Dropped onto an ungrouped root connection, usually meaning moving to the end of tags
|
||
// Since tags are always displayed before ungrouped connections, just put it at the end
|
||
insertIndex = newOrder.length;
|
||
}
|
||
|
||
newOrder.splice(insertIndex, 0, dragTagId);
|
||
reorderTags(newOrder);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (dragNode.type === 'connection') {
|
||
const dragTagId = connectionTags.find((tag) =>
|
||
tag.connectionIds.includes(String(dragNode.key)),
|
||
)?.id || '';
|
||
const dragIsUngroupedRoot = !dragTagId;
|
||
const dropRootToken = getDropRootToken(dropNode);
|
||
if (dragIsUngroupedRoot && dropNode.type === 'connection' && dropRootToken) {
|
||
reorderSidebarRoot(
|
||
buildSidebarRootConnectionToken(String(dragNode.key)),
|
||
dropRootToken,
|
||
insertBefore,
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Connection moving to tag (any drop position on a tag node counts as "into")
|
||
if (dragNode.type === 'connection' && dropNode.type === 'tag') {
|
||
moveConnectionToTag(dragNode.key, dropNode.dataRef.id);
|
||
return;
|
||
}
|
||
|
||
// Connection reordering against another connection
|
||
if (dragNode.type === 'connection' && dropNode.type === 'connection') {
|
||
const targetTag = connectionTags.find(t => t.connectionIds.includes(dropNode.key));
|
||
reorderConnections(
|
||
String(dragNode.key),
|
||
String(dropNode.key),
|
||
targetTag?.id || null,
|
||
insertBefore,
|
||
);
|
||
return;
|
||
}
|
||
};
|
||
|
||
const onRightClick = ({ event, node }: any) => {
|
||
if (isV2Ui && node?.type === 'v2-table-section') {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
return;
|
||
}
|
||
if (isV2Ui && node?.type === 'connection') {
|
||
openV2ConnectionContextMenu(event, node);
|
||
return;
|
||
}
|
||
if (isV2Ui && node?.type === 'database') {
|
||
const position = resolveSidebarContextMenuPosition(event.clientX, event.clientY);
|
||
setContextMenu({
|
||
x: position.x,
|
||
y: position.y,
|
||
sourceX: event.clientX,
|
||
sourceY: event.clientY,
|
||
items: [],
|
||
kind: 'v2-database',
|
||
node,
|
||
rootClassName: 'gn-v2-table-context-menu-popup',
|
||
overlayStyle: { width: 264, maxWidth: 'calc(100vw - 24px)' },
|
||
maxHeight: position.maxHeight,
|
||
});
|
||
return;
|
||
}
|
||
if (
|
||
isV2Ui
|
||
&& node?.type === 'object-group'
|
||
&& node?.dataRef?.groupKey === 'schema'
|
||
&& isPostgresSchemaDialect(getMetadataDialect(node.dataRef as SavedConnection))
|
||
&& String(node?.dataRef?.schemaName || '').trim()
|
||
) {
|
||
const position = resolveSidebarContextMenuPosition(event.clientX, event.clientY);
|
||
setContextMenu({
|
||
x: position.x,
|
||
y: position.y,
|
||
sourceX: event.clientX,
|
||
sourceY: event.clientY,
|
||
items: [],
|
||
kind: 'v2-schema',
|
||
node,
|
||
rootClassName: 'gn-v2-table-context-menu-popup',
|
||
overlayStyle: { width: 264, maxWidth: 'calc(100vw - 24px)' },
|
||
maxHeight: position.maxHeight,
|
||
});
|
||
return;
|
||
}
|
||
if (isV2Ui && node?.type === 'object-group' && node?.dataRef?.groupKey === 'tables') {
|
||
const position = resolveSidebarContextMenuPosition(event.clientX, event.clientY);
|
||
setContextMenu({
|
||
x: position.x,
|
||
y: position.y,
|
||
sourceX: event.clientX,
|
||
sourceY: event.clientY,
|
||
items: [],
|
||
kind: 'v2-table-group',
|
||
node,
|
||
rootClassName: 'gn-v2-table-context-menu-popup',
|
||
overlayStyle: { width: 264, maxWidth: 'calc(100vw - 24px)' },
|
||
maxHeight: position.maxHeight,
|
||
});
|
||
return;
|
||
}
|
||
if (isV2Ui && node?.type === 'table') {
|
||
const position = resolveSidebarContextMenuPosition(event.clientX, event.clientY);
|
||
setContextMenu({
|
||
x: position.x,
|
||
y: position.y,
|
||
sourceX: event.clientX,
|
||
sourceY: event.clientY,
|
||
items: [],
|
||
kind: 'v2-table',
|
||
node,
|
||
rootClassName: 'gn-v2-table-context-menu-popup',
|
||
overlayStyle: { width: 264, maxWidth: 'calc(100vw - 24px)' },
|
||
maxHeight: position.maxHeight,
|
||
});
|
||
return;
|
||
}
|
||
const items = getNodeMenuItems(node);
|
||
if (items && items.length > 0) {
|
||
setContextMenu({
|
||
x: event.clientX,
|
||
y: event.clientY,
|
||
items
|
||
});
|
||
}
|
||
};
|
||
|
||
const v2RailObjectActionsLabel = t('sidebar.rail.object_actions');
|
||
const v2RailSystemActionsLabel = t('sidebar.rail.system_actions');
|
||
const v2NewGroupLabel = t('sidebar.action.new_group');
|
||
const v2BatchTablesLabel = t('sidebar.action.batch_tables');
|
||
const v2BatchDatabasesLabel = t('sidebar.action.batch_databases');
|
||
const v2OpenExternalSqlFileLabel = t('sidebar.sql_file_exec.title');
|
||
const v2LocateCurrentTableLabel = t('sidebar.action.locate_current_table');
|
||
const v2LocateCurrentTableUnavailableLabel = t('sidebar.message.locate_current_table_unavailable');
|
||
const v2AiAssistantLabel = t('app.sidebar.ai_assistant');
|
||
const v2ToolsLabel = t('app.sidebar.tools');
|
||
const v2SettingsLabel = t('app.sidebar.settings');
|
||
const v2ActiveConnectionHeaderLabel = t('sidebar.active_connection.current_host_database');
|
||
const v2NoDatabaseSelectedLabel = t('sidebar.active_connection.no_database_selected');
|
||
const v2ConnectionActionsLabel = t('sidebar.active_connection.actions');
|
||
const v2CommandSearchLabel = t('sidebar.command_search.label');
|
||
const v2CommandSearchPlaceholder = t('sidebar.command_search.placeholder');
|
||
|
||
const v2CommandSearchPanelProps: SidebarSearchPanelProps<V2CommandSearchItem> = {
|
||
isOpen: isV2CommandSearchOpen,
|
||
searchValue: v2CommandSearchValue,
|
||
activeIndex: v2CommandActiveIndex,
|
||
label: v2CommandSearchLabel,
|
||
placeholder: v2CommandSearchPlaceholder,
|
||
persistedFilter: v2PersistedSidebarFilter,
|
||
persistentFilterEnabled: v2CommandSearchPersistentFilterEnabled,
|
||
aiMode: v2CommandSearchAiMode,
|
||
objectMode: v2CommandSearchObjectMode,
|
||
flatItems: commandSearchFlatItems,
|
||
sections: {
|
||
goTo: filteredCommandSearchTreeItems,
|
||
ai: commandSearchAiItem,
|
||
actions: filteredCommandSearchActionItems,
|
||
recent: filteredCommandSearchRecentItems,
|
||
},
|
||
inputRef: commandSearchInputRef,
|
||
handlers: {
|
||
onSearchValueChange: handleV2CommandSearchValueChange,
|
||
onKeyDown: handleV2CommandSearchKeyDown,
|
||
onClose: closeV2CommandSearch,
|
||
onItemSelect: (item: V2CommandSearchItem) => runCommandSearchItem(item),
|
||
onItemHover: (key: string) => setV2CommandActiveIndex(commandSearchFlatItems.findIndex((entry) => entry.key === key)),
|
||
onTogglePersistentFilter: toggleV2CommandSearchPersistentFilter,
|
||
onResetFilter: resetV2SidebarFilter,
|
||
},
|
||
};
|
||
|
||
// V2 Connection Rail 子组件 props(从原 renderV2ConnectionRail 抽出,保留所有原行为)
|
||
const v2ConnectionRailProps = {
|
||
labels: {
|
||
railSystemActions: v2RailSystemActionsLabel,
|
||
railObjectActions: v2RailObjectActionsLabel,
|
||
newGroup: v2NewGroupLabel,
|
||
batchTables: v2BatchTablesLabel,
|
||
batchDatabases: v2BatchDatabasesLabel,
|
||
openExternalSqlFile: v2OpenExternalSqlFileLabel,
|
||
locateCurrentTable: v2LocateCurrentTableLabel,
|
||
locateCurrentTableUnavailable: v2LocateCurrentTableUnavailableLabel,
|
||
aiAssistant: v2AiAssistantLabel,
|
||
tools: v2ToolsLabel,
|
||
settings: v2SettingsLabel,
|
||
},
|
||
handlers: {
|
||
openCreateTagModal: () => { setRenameViewTarget(null); createTagForm.resetFields(); setIsCreateTagModalOpen(true); },
|
||
openBatchTableExport: () => openBatchOperationModal(),
|
||
openBatchDatabaseExport: () => openBatchDatabaseModal(),
|
||
openExternalSqlFile: handleOpenSQLFileFromToolbar,
|
||
locateActiveTab: handleLocateActiveTabInSidebar,
|
||
toggleAI: onToggleAI ?? (() => {}),
|
||
openTools: onOpenTools ?? (() => {}),
|
||
openSettings: onOpenSettings ?? (() => {}),
|
||
},
|
||
canLocateActiveTab,
|
||
};
|
||
|
||
return (
|
||
<div className={isV2Ui ? 'gn-v2-sidebar-redesign' : undefined} style={{ display: 'flex', height: '100%', minHeight: 0 }}>
|
||
{exportProgressModal}
|
||
{isV2Ui && <SidebarConnectionRail {...v2ConnectionRailProps} />}
|
||
<div className={isV2Ui ? 'gn-v2-object-explorer' : undefined} style={{ display: 'flex', flexDirection: 'column', height: '100%', minWidth: 0, flex: 1 }}>
|
||
{isV2Ui && (
|
||
<div className="gn-v2-active-connection-header" data-object-count={activeConnectionObjectCount}>
|
||
<div className="gn-v2-active-connection-trigger" aria-label={v2ActiveConnectionHeaderLabel}>
|
||
<span className={`gn-v2-live-dot is-${activeConnection ? buildRailConnectionStatus(activeConnection.id) : 'idle'}`} />
|
||
<div className="gn-v2-active-connection-copy">
|
||
<strong>{activeConnectionDisplayName}</strong>
|
||
<span>{activeDatabaseDisplayName || v2NoDatabaseSelectedLabel}</span>
|
||
</div>
|
||
</div>
|
||
<div className="gn-v2-active-connection-actions">
|
||
<Tooltip title={t('sidebar.menu.new_query')}>
|
||
<Button
|
||
size="small"
|
||
type="text"
|
||
className="gn-v2-active-connection-query-action"
|
||
icon={<FileTextOutlined />}
|
||
aria-label={t('sidebar.menu.new_query')}
|
||
data-gonavi-new-query-action="true"
|
||
disabled={!activeConnection}
|
||
onClick={() => {
|
||
if (activeConnection) {
|
||
handleV2ConnectionContextMenuAction(getConnectionNodeForAction(activeConnection), 'new-query');
|
||
}
|
||
}}
|
||
>
|
||
{t('sidebar.menu.new_query')}
|
||
</Button>
|
||
</Tooltip>
|
||
{onCreateConnection && (
|
||
<Tooltip title={t('connection.new')}>
|
||
<Button
|
||
size="small"
|
||
type="text"
|
||
icon={<PlusOutlined />}
|
||
aria-label={t('connection.new')}
|
||
data-gonavi-create-connection-action="true"
|
||
onClick={onCreateConnection}
|
||
/>
|
||
</Tooltip>
|
||
)}
|
||
<Tooltip title={v2ConnectionActionsLabel}>
|
||
<Button
|
||
size="small"
|
||
type="text"
|
||
icon={<MoreOutlined />}
|
||
aria-label={v2ConnectionActionsLabel}
|
||
disabled={!activeConnection}
|
||
onClick={(event) => {
|
||
if (activeConnection) {
|
||
openV2ConnectionContextMenu(event, activeConnection);
|
||
}
|
||
}}
|
||
/>
|
||
</Tooltip>
|
||
</div>
|
||
</div>
|
||
)}
|
||
<div className={isV2Ui ? 'gn-v2-explorer-search' : undefined} style={{ padding: '8px 14px', borderBottom: `1px solid ${darkMode ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)'}` }}>
|
||
{isV2Ui && !v2UseLegacySidebarFilter ? (
|
||
<div className="gn-v2-explorer-command-row" data-v2-sidebar-search-mode="command">
|
||
<button
|
||
type="button"
|
||
className="gn-v2-explorer-command-trigger"
|
||
onClick={() => {
|
||
openV2CommandSearch();
|
||
onFocusCommandSearch?.();
|
||
}}
|
||
aria-label={v2CommandSearchLabel}
|
||
>
|
||
<SearchOutlined />
|
||
<span>{v2PersistedSidebarFilter || v2CommandSearchPlaceholder}</span>
|
||
{focusSidebarSearchShortcutTokens.length > 0 ? (
|
||
<span className="gn-v2-search-shortcut" aria-hidden="true">
|
||
{focusSidebarSearchShortcutTokens.map((token, index) => (
|
||
<kbd key={`${token}-${index}`}>{token}</kbd>
|
||
))}
|
||
</span>
|
||
) : null}
|
||
</button>
|
||
<Tooltip title={v2PersistedSidebarFilter ? t('sidebar.command_search.reset_filter') : t('sidebar.command_search.no_synced_filter')}>
|
||
<button
|
||
type="button"
|
||
className="gn-v2-explorer-filter-action"
|
||
aria-label={t('sidebar.command_search.reset_filter')}
|
||
disabled={!v2PersistedSidebarFilter}
|
||
onClick={resetV2SidebarFilter}
|
||
>
|
||
<ReloadOutlined />
|
||
</button>
|
||
</Tooltip>
|
||
</div>
|
||
) : isV2Ui ? (
|
||
<div className="gn-v2-explorer-legacy-filter-row" data-v2-sidebar-search-mode="filter">
|
||
<Input
|
||
{...noAutoCapInputProps}
|
||
ref={searchInputRef}
|
||
value={searchValue}
|
||
placeholder={t('sidebar.search.placeholder')}
|
||
onChange={onSearch}
|
||
size="small"
|
||
prefix={<SearchOutlined />}
|
||
/>
|
||
<Tooltip title={searchValue ? t('sidebar.command_search.reset_filter') : t('sidebar.command_search.no_filter_content')}>
|
||
<button
|
||
type="button"
|
||
className="gn-v2-explorer-filter-action"
|
||
aria-label={t('sidebar.command_search.reset_filter')}
|
||
disabled={!searchValue}
|
||
onClick={resetV2SidebarFilter}
|
||
>
|
||
<ReloadOutlined />
|
||
</button>
|
||
</Tooltip>
|
||
</div>
|
||
) : (
|
||
<Input
|
||
{...noAutoCapInputProps}
|
||
ref={searchInputRef}
|
||
placeholder={t('sidebar.search.placeholder')}
|
||
onChange={onSearch}
|
||
size="small"
|
||
prefix={<SearchOutlined style={{ color: darkMode ? 'rgba(255,255,255,0.35)' : 'rgba(0,0,0,0.35)' }} />}
|
||
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={(
|
||
<Popover
|
||
content={searchScopePopoverContent}
|
||
trigger="click"
|
||
placement="bottomRight"
|
||
open={isSearchScopePopoverOpen}
|
||
onOpenChange={setIsSearchScopePopoverOpen}
|
||
styles={{ body: { padding: 0, borderRadius: 16, overflow: 'hidden' } }}
|
||
>
|
||
<Tooltip title={t('sidebar.command_search.scope.tooltip', { scope: searchScopeSummary })}>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 4,
|
||
cursor: 'pointer',
|
||
padding: '2px 6px',
|
||
borderRadius: 4,
|
||
background: isSearchScopePopoverOpen
|
||
? (darkMode ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.06)')
|
||
: 'transparent',
|
||
transition: 'background 0.2s',
|
||
color: searchScopes.includes('smart')
|
||
? (darkMode ? '#ffd666' : '#1677ff')
|
||
: (darkMode ? 'rgba(255,255,255,0.45)' : 'rgba(0,0,0,0.45)'),
|
||
}}
|
||
onMouseEnter={(e) => {
|
||
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)');
|
||
}
|
||
}}
|
||
>
|
||
<FilterOutlined style={{ fontSize: 13 }} />
|
||
<span style={{ fontSize: 12, fontWeight: 500 }}>
|
||
{searchScopes.includes('smart') ? t('sidebar.command_search.scope.compact_smart') : searchScopes.length}
|
||
</span>
|
||
</div>
|
||
</Tooltip>
|
||
</Popover>
|
||
)}
|
||
/>
|
||
)}
|
||
</div>
|
||
|
||
{isV2Ui && (
|
||
<div className="gn-v2-explorer-filter-tabs" aria-label={t('sidebar.command_search.object_kind.filter_aria')}>
|
||
{V2_EXPLORER_FILTER_OPTIONS.map((item) => (
|
||
<button
|
||
key={item.key}
|
||
type="button"
|
||
className={v2ExplorerFilter === item.key ? 'is-active' : undefined}
|
||
aria-pressed={v2ExplorerFilter === item.key}
|
||
onClick={() => setV2ExplorerFilter(item.key)}
|
||
>
|
||
{t(item.labelKey)}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Toolbar */}
|
||
{!isV2Ui && (
|
||
<div data-sidebar-legacy-toolbar="true" style={legacyToolbarStyle}>
|
||
<div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
|
||
<Tooltip title={t('sidebar.action.new_group')}>
|
||
<Button
|
||
size="small"
|
||
type="text"
|
||
icon={<FolderOpenOutlined />}
|
||
aria-label={t('sidebar.action.new_group')}
|
||
data-sidebar-create-group-action="true"
|
||
onClick={() => { setRenameViewTarget(null); createTagForm.resetFields(); setIsCreateTagModalOpen(true); }}
|
||
style={{ color: legacyToolbarButtonColor }}
|
||
/>
|
||
</Tooltip>
|
||
</div>
|
||
<div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
|
||
<Tooltip title={t('sidebar.action.batch_tables')}>
|
||
<Button
|
||
size="small"
|
||
type="text"
|
||
icon={<TableOutlined />}
|
||
aria-label={t('sidebar.action.batch_tables')}
|
||
data-sidebar-batch-table-action="true"
|
||
onClick={() => openBatchOperationModal()}
|
||
style={{ color: legacyToolbarButtonColor }}
|
||
/>
|
||
</Tooltip>
|
||
</div>
|
||
<div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
|
||
<Tooltip title={t('sidebar.action.batch_databases')}>
|
||
<Button
|
||
size="small"
|
||
type="text"
|
||
icon={<DatabaseOutlined />}
|
||
aria-label={t('sidebar.action.batch_databases')}
|
||
data-sidebar-batch-database-action="true"
|
||
onClick={() => openBatchDatabaseModal()}
|
||
style={{ color: legacyToolbarButtonColor }}
|
||
/>
|
||
</Tooltip>
|
||
</div>
|
||
<div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
|
||
<Tooltip title={v2OpenExternalSqlFileLabel}>
|
||
<Button
|
||
size="small"
|
||
type="text"
|
||
icon={<FileAddOutlined />}
|
||
aria-label={v2OpenExternalSqlFileLabel}
|
||
data-sidebar-open-external-sql-file-action="true"
|
||
onClick={handleOpenSQLFileFromToolbar}
|
||
style={{ color: legacyToolbarButtonColor }}
|
||
/>
|
||
</Tooltip>
|
||
</div>
|
||
<div data-sidebar-legacy-toolbar-item="true" style={legacyToolbarItemStyle}>
|
||
<Tooltip title={canLocateActiveTab ? t('sidebar.action.locate_current_tab') : t('sidebar.message.locate_current_tab_unavailable')}>
|
||
<span style={legacyToolbarDisabledWrapStyle}>
|
||
<Button
|
||
size="small"
|
||
type="text"
|
||
icon={<AimOutlined />}
|
||
aria-label={t('sidebar.action.locate_current_tab')}
|
||
data-sidebar-locate-current-tab-action="true"
|
||
disabled={!canLocateActiveTab}
|
||
onClick={handleLocateActiveTabInSidebar}
|
||
style={{ color: legacyToolbarButtonColor }}
|
||
/>
|
||
</span>
|
||
</Tooltip>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div
|
||
ref={treeContainerRef}
|
||
className={`sidebar-tree-scroll-shell${isV2Ui ? ' gn-v2-explorer-tree-shell' : ''}`}
|
||
style={{
|
||
flex: 1,
|
||
overflow: 'hidden',
|
||
minHeight: 0,
|
||
}}
|
||
>
|
||
<div className="sidebar-tree-scroll-content">
|
||
<Tree
|
||
key={isV2Ui ? `v2-tree-${v2ExplorerFilter}` : 'legacy-tree'}
|
||
ref={treeRef}
|
||
showIcon
|
||
draggable={{
|
||
icon: false,
|
||
nodeDraggable: (node: any) => node.type === 'connection' || node.type === 'tag'
|
||
}}
|
||
onDragStart={() => {
|
||
snapshotTreeSelectionBeforeDrag();
|
||
treeDragSelectSuppressUntilRef.current = Date.now() + 600;
|
||
setIsTreeDragging(true);
|
||
}}
|
||
onDragEnter={() => {
|
||
treeDragSelectSuppressUntilRef.current = Date.now() + 600;
|
||
setIsTreeDragging(true);
|
||
}}
|
||
onDragEnd={() => {
|
||
restoreTreeSelectionAfterDrag();
|
||
setIsTreeDragging(false);
|
||
}}
|
||
onDrop={handleDrop}
|
||
loadData={onLoadData}
|
||
treeData={isV2Ui ? v2VisibleTreeData : displayTreeData}
|
||
onDoubleClick={onDoubleClick}
|
||
onSelect={onSelect}
|
||
titleRender={titleRender}
|
||
switcherIcon={renderSidebarSwitcherIcon}
|
||
expandedKeys={expandedKeys}
|
||
onExpand={onExpand}
|
||
loadedKeys={loadedKeys}
|
||
onLoad={setLoadedKeys}
|
||
autoExpandParent={autoExpandParent}
|
||
selectedKeys={selectedKeys}
|
||
blockNode
|
||
height={effectiveTreeHeight}
|
||
scrollWidth={isV2Ui ? v2TreeHorizontalScrollWidth : undefined}
|
||
onRightClick={onRightClick}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{isV2Ui && (
|
||
<div className="gn-v2-sidebar-log-footer">
|
||
<SlowQueryRailButton
|
||
className="gn-v2-sidebar-slow-query-button"
|
||
tooltipPlacement="top"
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<SidebarSearchPanel {...v2CommandSearchPanelProps} />
|
||
|
||
{contextMenu?.kind && typeof document !== 'undefined' && createPortal(
|
||
<div
|
||
ref={contextMenuPortalRef}
|
||
className={`gn-v2-sidebar-context-menu-portal ${contextMenu.rootClassName || ''}`}
|
||
style={{
|
||
position: 'fixed',
|
||
left: contextMenu.x,
|
||
top: contextMenu.y,
|
||
zIndex: 10000,
|
||
width: contextMenu.overlayStyle?.width ?? SIDEBAR_CONTEXT_MENU_FALLBACK_WIDTH,
|
||
maxWidth: contextMenu.overlayStyle?.maxWidth ?? 'calc(100vw - 24px)',
|
||
['--gn-v2-context-menu-max-height' as any]: `${contextMenu.maxHeight ?? SIDEBAR_CONTEXT_MENU_FALLBACK_HEIGHT}px`,
|
||
}}
|
||
onMouseDown={(event) => event.stopPropagation()}
|
||
onClick={(event) => event.stopPropagation()}
|
||
onContextMenu={(event) => event.preventDefault()}
|
||
>
|
||
{renderV2SidebarContextMenuContent(contextMenu)}
|
||
</div>,
|
||
document.body,
|
||
)}
|
||
|
||
{contextMenu && !contextMenu.kind && (
|
||
<Dropdown
|
||
menu={{ items: contextMenu.items }}
|
||
open={true}
|
||
onOpenChange={(open) => { if (!open) setContextMenu(null); }}
|
||
trigger={['contextMenu']}
|
||
rootClassName={contextMenu.rootClassName}
|
||
overlayStyle={contextMenu.overlayStyle}
|
||
>
|
||
<div style={{ position: 'fixed', left: contextMenu.x, top: contextMenu.y, width: 1, height: 1 }} />
|
||
</Dropdown>
|
||
)}
|
||
|
||
<SidebarEntityModals
|
||
connections={connections}
|
||
connectionTags={connectionTags}
|
||
modalPanelStyle={modalPanelStyle}
|
||
modalSectionStyle={modalSectionStyle}
|
||
modalScrollSectionStyle={modalScrollSectionStyle}
|
||
renderSidebarModalTitle={renderSidebarModalTitle}
|
||
isCreateTagModalOpen={isCreateTagModalOpen}
|
||
setIsCreateTagModalOpen={setIsCreateTagModalOpen}
|
||
createTagForm={createTagForm}
|
||
renameViewTarget={renameViewTarget}
|
||
updateConnectionTag={updateConnectionTag}
|
||
addConnectionTag={addConnectionTag}
|
||
moveConnectionToTag={moveConnectionToTag}
|
||
isCreateDbModalOpen={isCreateDbModalOpen}
|
||
setIsCreateDbModalOpen={setIsCreateDbModalOpen}
|
||
createDbForm={createDbForm}
|
||
handleCreateDatabase={handleCreateDatabase}
|
||
isCreateSchemaModalOpen={isCreateSchemaModalOpen}
|
||
setIsCreateSchemaModalOpen={setIsCreateSchemaModalOpen}
|
||
createSchemaForm={createSchemaForm}
|
||
createSchemaTarget={createSchemaTarget}
|
||
setCreateSchemaTarget={setCreateSchemaTarget}
|
||
handleCreateSchema={handleCreateSchema}
|
||
isRenameSchemaModalOpen={isRenameSchemaModalOpen}
|
||
setIsRenameSchemaModalOpen={setIsRenameSchemaModalOpen}
|
||
renameSchemaForm={renameSchemaForm}
|
||
renameSchemaTarget={renameSchemaTarget}
|
||
setRenameSchemaTarget={setRenameSchemaTarget}
|
||
handleRenameSchema={handleRenameSchema}
|
||
isRenameDbModalOpen={isRenameDbModalOpen}
|
||
setIsRenameDbModalOpen={setIsRenameDbModalOpen}
|
||
renameDbForm={renameDbForm}
|
||
renameDbTarget={renameDbTarget}
|
||
setRenameDbTarget={setRenameDbTarget}
|
||
handleRenameDatabase={handleRenameDatabase}
|
||
isRenameTableModalOpen={isRenameTableModalOpen}
|
||
setIsRenameTableModalOpen={setIsRenameTableModalOpen}
|
||
renameTableForm={renameTableForm}
|
||
renameTableTarget={renameTableTarget}
|
||
setRenameTableTarget={setRenameTableTarget}
|
||
handleRenameTable={handleRenameTable}
|
||
isRenameViewModalOpen={isRenameViewModalOpen}
|
||
setIsRenameViewModalOpen={setIsRenameViewModalOpen}
|
||
renameViewForm={renameViewForm}
|
||
setRenameViewTarget={setRenameViewTarget}
|
||
handleRenameView={handleRenameView}
|
||
isRenameSavedQueryModalOpen={isRenameSavedQueryModalOpen}
|
||
setIsRenameSavedQueryModalOpen={setIsRenameSavedQueryModalOpen}
|
||
renameSavedQueryForm={renameSavedQueryForm}
|
||
renameSavedQueryTarget={renameSavedQueryTarget}
|
||
setRenameSavedQueryTarget={setRenameSavedQueryTarget}
|
||
handleRenameSavedQuery={handleRenameSavedQuery}
|
||
/>
|
||
|
||
<ExternalSQLFileModal {...externalSQLFileModalProps} />
|
||
|
||
<SidebarBatchExportModals
|
||
connections={connections}
|
||
modalPanelStyle={modalPanelStyle}
|
||
modalSectionStyle={modalSectionStyle}
|
||
modalScrollSectionStyle={modalScrollSectionStyle}
|
||
modalHintTextStyle={modalHintTextStyle}
|
||
darkMode={darkMode}
|
||
tableModalTitle={renderSidebarModalTitle(
|
||
<TableOutlined />,
|
||
t('sidebar.modal.batch_tables.title'),
|
||
t('sidebar.modal.batch_tables.description'),
|
||
)}
|
||
databaseModalTitle={renderSidebarModalTitle(
|
||
<DatabaseOutlined />,
|
||
t('sidebar.modal.batch_databases.title'),
|
||
t('sidebar.modal.batch_databases.description'),
|
||
)}
|
||
isBatchModalOpen={isBatchModalOpen}
|
||
setIsBatchModalOpen={setIsBatchModalOpen}
|
||
selectedConnection={selectedConnection}
|
||
selectedDatabase={selectedDatabase}
|
||
availableDatabases={availableDatabases}
|
||
batchTables={batchTables}
|
||
checkedTableKeys={checkedTableKeys}
|
||
setCheckedTableKeys={setCheckedTableKeys}
|
||
batchFilterKeyword={batchFilterKeyword}
|
||
setBatchFilterKeyword={setBatchFilterKeyword}
|
||
batchFilterType={batchFilterType}
|
||
setBatchFilterType={setBatchFilterType}
|
||
batchSelectionScope={batchSelectionScope}
|
||
setBatchSelectionScope={setBatchSelectionScope}
|
||
filteredBatchObjects={filteredBatchObjects}
|
||
groupedBatchObjects={groupedBatchObjects}
|
||
selectionScopeTargetKeys={selectionScopeTargetKeys}
|
||
handleConnectionChange={handleConnectionChange}
|
||
handleDatabaseChange={handleDatabaseChange}
|
||
handleBatchClear={handleBatchClear}
|
||
handleBatchDeleteTables={handleBatchDeleteTables}
|
||
handleBatchExport={handleBatchExport}
|
||
handleCheckAll={handleCheckAll}
|
||
handleInvertSelection={handleInvertSelection}
|
||
isBatchDbModalOpen={isBatchDbModalOpen}
|
||
setIsBatchDbModalOpen={setIsBatchDbModalOpen}
|
||
selectedDbConnection={selectedDbConnection}
|
||
batchDatabases={batchDatabases}
|
||
checkedDbKeys={checkedDbKeys}
|
||
setCheckedDbKeys={setCheckedDbKeys}
|
||
handleDbConnectionChange={handleDbConnectionChange}
|
||
handleBatchDbExport={handleBatchDbExport}
|
||
handleBatchDbDelete={handleBatchDbDelete}
|
||
handleCheckAllDb={handleCheckAllDb}
|
||
handleInvertSelectionDb={handleInvertSelectionDb}
|
||
/>
|
||
<FindInDatabaseModal
|
||
open={findInDbContext.open}
|
||
onClose={() => setFindInDbContext({ open: false, connectionId: '', dbName: '' })}
|
||
connectionId={findInDbContext.connectionId}
|
||
dbName={findInDbContext.dbName}
|
||
/>
|
||
<MessagePublishModal
|
||
open={Boolean(messagePublishTarget)}
|
||
connection={messagePublishTarget?.connection || null}
|
||
executionDbName={messagePublishTarget?.executionDbName || ''}
|
||
defaultDestination={messagePublishTarget?.destination || ''}
|
||
onCancel={() => setMessagePublishTarget(null)}
|
||
onSuccess={handleMessagePublishSuccess}
|
||
/>
|
||
</div>
|
||
);
|
||
});
|
||
|
||
export default Sidebar;
|