import Modal from './common/ResizableDraggableModal'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import { Button, Dropdown, message, Tabs, Tooltip } from 'antd'; import { AppstoreOutlined, CloseOutlined, ConsoleSqlOutlined, DatabaseOutlined, PlusOutlined, RobotOutlined, SettingOutlined } from '@ant-design/icons'; import type { MenuProps, TabsProps } from 'antd'; import { DndContext, PointerSensor, closestCenter, useSensor, useSensors } from '@dnd-kit/core'; import type { DragStartEvent, DragEndEvent } from '@dnd-kit/core'; import { SortableContext, useSortable, horizontalListSortingStrategy } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import { restrictToHorizontalAxis } from '@dnd-kit/modifiers'; import { useStore } from '../store'; import DataViewer from './DataViewer'; import QueryEditor from './QueryEditor'; import TableDesigner from './TableDesigner'; import RedisViewer from './RedisViewer'; import RedisCommandEditor from './RedisCommandEditor'; import RedisMonitor from './RedisMonitor'; import TriggerViewer from './TriggerViewer'; import DefinitionViewer from './DefinitionViewer'; import TableOverview from './TableOverview'; import TableExportWorkbench from './TableExportWorkbench'; import JVMOverview from './JVMOverview'; import JVMResourceBrowser from './JVMResourceBrowser'; import JVMAuditViewer from './JVMAuditViewer'; import JVMDiagnosticConsole from './JVMDiagnosticConsole'; import JVMMonitoringDashboard from './JVMMonitoringDashboard'; import SqlAnalysisWorkbench from './explain/SqlAnalysisWorkbench'; import type { TabData } from '../types'; import { t } from '../i18n'; import { buildTabDisplayModel, resolveConnectionHostSummary, type TabDisplayPart, type TabDisplayModel, } from '../utils/tabDisplay'; import { ReadSQLFile, WriteSQLFile } from '../../wailsjs/go/app/App'; import { getSQLFileTabPath, hasSQLFileTabUnsavedChanges, isSQLFileMissingErrorMessage, isSQLFileMissingReadResult, isSQLFileQueryTab, normalizeSQLFileReadContent, } from '../utils/sqlFileTabDirty'; import { clearSQLFileTabDraft, getSQLFileTabDraft } from '../utils/sqlFileTabDrafts'; const getTabKindLabel = (tab: TabData): string => { if (tab.type === 'query') return t('tab_manager.kind_badge.query'); if (tab.type === 'table') return t('tab_manager.kind_badge.table'); if (tab.type === 'design') return t('tab_manager.kind_badge.design'); if (tab.type === 'table-overview') return t('tab_manager.kind_badge.table_overview'); if (tab.type === 'table-export') return t('tab_manager.kind_badge.table_export'); if (tab.type === 'sql-analysis') return t('tab_manager.kind_badge.sql_analysis'); if (tab.type.startsWith('redis')) return t('tab_manager.kind_badge.redis'); if (tab.type.startsWith('jvm')) return t('tab_manager.kind_badge.jvm'); if (tab.type === 'trigger') return t('tab_manager.kind_badge.trigger'); if (tab.type === 'view-def') { return tab.viewKind === 'materialized' ? t('tab_manager.kind_badge.materialized_view') : t('tab_manager.kind_badge.view'); } if (tab.type === 'event-def') return t('tab_manager.kind_badge.event'); if (tab.type === 'routine-def') return t('tab_manager.kind_badge.routine'); if (tab.type === 'sequence-def') return t('tab_manager.kind_badge.sequence'); if (tab.type === 'package-def') return t('tab_manager.kind_badge.package'); return t('tab_manager.kind_badge.fallback'); }; export const TAB_WORKBENCH_CLASS_NAME = 'tab-workbench'; const getTabKindTooltipLabel = (tab: TabData): string => { if (tab.type === 'query') return t('tab_manager.hover.kind.query'); if (tab.type === 'table') return t('tab_manager.hover.kind.table'); if (tab.type === 'design') return t('tab_manager.hover.kind.design'); if (tab.type === 'table-overview') return t('tab_manager.hover.kind.table_overview'); if (tab.type === 'table-export') return t('tab_manager.hover.kind.table_export'); if (tab.type === 'sql-analysis') return t('tab_manager.hover.kind.sql_analysis'); if (tab.type === 'redis-keys') return t('tab_manager.hover.kind.redis_keys'); if (tab.type === 'redis-command') return t('tab_manager.hover.kind.redis_command'); if (tab.type === 'redis-monitor') return t('tab_manager.hover.kind.redis_monitor'); if (tab.type === 'jvm-overview') return t('tab_manager.hover.kind.jvm_overview'); if (tab.type === 'jvm-resource') return t('tab_manager.hover.kind.jvm_resource'); if (tab.type === 'jvm-audit') return t('tab_manager.hover.kind.jvm_audit'); if (tab.type === 'jvm-diagnostic') return t('tab_manager.hover.kind.jvm_diagnostic'); if (tab.type === 'jvm-monitoring') return t('tab_manager.hover.kind.jvm_monitoring'); if (tab.type === 'trigger') return t('tab_manager.hover.kind.trigger'); if (tab.type === 'view-def') { return tab.viewKind === 'materialized' ? t('tab_manager.hover.kind.materialized_view') : t('tab_manager.hover.kind.view'); } if (tab.type === 'event-def') return t('tab_manager.hover.kind.event'); if (tab.type === 'routine-def') return t('tab_manager.hover.kind.routine'); if (tab.type === 'sequence-def') return t('tab_manager.hover.kind.sequence'); if (tab.type === 'package-def') return t('tab_manager.hover.kind.package'); return t('tab_manager.hover.kind.fallback'); }; const getTabObjectLabel = (tab: TabData): string => { if (tab.tableName) return tab.tableName; if (tab.viewName) return tab.viewName; if (tab.eventName) return tab.eventName; if (tab.routineName) return tab.routineName; if (tab.sequenceName) return tab.sequenceName; if (tab.packageName) return tab.packageName; if (tab.triggerName) return tab.triggerName; if (tab.resourcePath) return tab.resourcePath; if (tab.filePath) return tab.filePath; if (tab.type === 'sql-analysis') return tab.title; if (tab.type.startsWith('redis')) return `db${tab.redisDB ?? 0}`; return ''; }; const getCloseOtherTabIds = (tabs: TabData[], id: string): string[] => tabs.filter((tab) => tab.id !== id).map((tab) => tab.id); const getCloseTabsToLeftIds = (tabs: TabData[], id: string): string[] => { const index = tabs.findIndex((tab) => tab.id === id); if (index <= 0) return []; return tabs.slice(0, index).map((tab) => tab.id); }; const getCloseTabsToRightIds = (tabs: TabData[], id: string): string[] => { const index = tabs.findIndex((tab) => tab.id === id); if (index < 0 || index >= tabs.length - 1) return []; return tabs.slice(index + 1).map((tab) => tab.id); }; export const stopTabHoverDragPropagation = (event: React.SyntheticEvent) => { event.stopPropagation(); }; export const resolveTabHoverOpen = (isHoverInfoOpen: boolean, isTabMenuOpen: boolean) => isHoverInfoOpen && !isTabMenuOpen; export const openTabDisplaySettings = () => { if (typeof window === 'undefined') { return; } window.dispatchEvent(new CustomEvent('gonavi:open-tab-display-settings')); }; export const shouldShowV2ConnectionLabel = (displayTitle: string, connectionLabel?: string): boolean => { const normalizedConnectionLabel = String(connectionLabel || '').trim(); if (!normalizedConnectionLabel) { return false; } const normalizedDisplayTitle = String(displayTitle || '').trim(); if (!normalizedDisplayTitle) { return true; } const escapedConnectionLabel = normalizedConnectionLabel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const prefixedConnectionPattern = new RegExp(`^\\[${escapedConnectionLabel}(?:\\s*[|\\]])`, 'i'); return !prefixedConnectionPattern.test(normalizedDisplayTitle); }; export const resolveTabHoverTitle = (displayModel: TabDisplayModel | undefined, fallbackTitle: string): string => { if (!displayModel) { return fallbackTitle; } const objectPart = [...displayModel.primaryParts, ...displayModel.secondaryParts] .find((part) => part.key === 'object'); if (objectPart?.text) { return objectPart.text; } const primaryText = displayModel.primaryParts .filter((part) => part.key !== 'kind') .map((part) => part.text) .join(' ') .trim(); return primaryText || displayModel.primaryText || fallbackTitle; }; type TabHoverInfoProps = { tab: TabData; displayModel?: TabDisplayModel; displayTitle: string; connectionLabel?: string; hostSummary?: string; }; export const TabHoverInfo: React.FC = ({ tab, displayModel, displayTitle, connectionLabel, hostSummary, }) => { const objectLabel = getTabObjectLabel(tab); const hoverTitle = resolveTabHoverTitle(displayModel, displayTitle); const schemaPart = displayModel ? [...displayModel.primaryParts, ...displayModel.secondaryParts].find((part) => part.key === 'schema') : undefined; const rows = [ [t('tab_manager.hover.label.type'), getTabKindTooltipLabel(tab)], [t('tab_manager.hover.label.connection'), connectionLabel || t('tab_manager.hover.fallback.unbound_connection')], ['Host', hostSummary || t('tab_manager.hover.fallback.host_not_configured')], [t('tab_manager.hover.label.database'), tab.dbName || t('tab_manager.hover.fallback.database_not_specified')], ['Schema', schemaPart?.value], [t('tab_manager.hover.label.object'), objectLabel], ].filter(([, value]) => Boolean(value)); return (
{getTabKindLabel(tab)} {hoverTitle}
{rows.map(([label, value]) => (
{label} {value}
))}
); }; type SortableTabLabelProps = { tab: TabData; displayModel: TabDisplayModel; displayTitle: string; menuItems: MenuProps['items']; connectionLabel?: string; hostSummary?: string; isV2Ui?: boolean; onClose?: () => void; }; const renderV2TabDisplayPart = (part: TabDisplayPart) => { if (part.key === 'kind') { return ( {part.text} ); } return ( {part.text} ); }; const SortableTabLabel: React.FC = ({ tab, displayModel, displayTitle, menuItems, connectionLabel, hostSummary, isV2Ui, onClose, }) => { const [isHoverInfoOpen, setIsHoverInfoOpen] = useState(false); const [isTabMenuOpen, setIsTabMenuOpen] = useState(false); const handleTabLabelContextMenu = (event: React.MouseEvent) => { event.preventDefault(); setIsHoverInfoOpen(false); setIsTabMenuOpen(true); }; const handleTabMenuOpenChange = (open: boolean) => { setIsTabMenuOpen(open); setIsHoverInfoOpen(false); }; const handleHoverInfoOpenChange = (open: boolean) => { setIsHoverInfoOpen(open && !isTabMenuOpen); }; const tabDisplayPartCount = displayModel.primaryParts.length + displayModel.secondaryParts.length; const showSecondaryLine = isV2Ui && displayModel.layout === 'double' && Boolean(displayModel.secondaryText); const labelNode = ( = 4 ? ' gn-v2-tab-label-rich' : ''}`} onContextMenu={handleTabLabelContextMenu} title={isV2Ui ? undefined : displayTitle} > {isV2Ui ? ( {displayModel.primaryParts.length > 0 ? displayModel.primaryParts.map(renderV2TabDisplayPart) : displayModel.primaryText} {showSecondaryLine ? ( {displayModel.secondaryText} ) : null} ) : ( {displayTitle} )} {isV2Ui && onClose ? ( ) : null} ); const wrappedLabel = isV2Ui ? ( )} placement="bottomLeft" mouseEnterDelay={1.2} open={resolveTabHoverOpen(isHoverInfoOpen, isTabMenuOpen)} onOpenChange={handleHoverInfoOpenChange} destroyOnHidden rootClassName="gn-v2-tab-hover-tooltip" > {labelNode} ) : labelNode; return ( {wrappedLabel} ); }; type DraggableTabNodeProps = { node: React.ReactElement; }; const DraggableTabNode: React.FC = ({ node }) => { const tabId = String(node.key || '').trim(); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: tabId }); const style: React.CSSProperties = { ...(node.props.style || {}), transform: CSS.Transform.toString(transform), transition: transition || 'transform 180ms cubic-bezier(0.22, 1, 0.36, 1)', opacity: isDragging ? 0.88 : 1, cursor: isDragging ? 'grabbing' : 'grab', touchAction: 'none', zIndex: isDragging ? 2 : node.props.style?.zIndex, }; return React.cloneElement(node, { ref: setNodeRef, style, ...attributes, ...listeners, className: `${node.props.className || ''} tab-dnd-node${isDragging ? ' is-dragging' : ''}`, }); }; const TabContent: React.FC<{ tab: TabData; isActive: boolean }> = React.memo(({ tab, isActive }) => { if (tab.type === 'query') { return ; } if (tab.type === 'table') { return ; } if (tab.type === 'design') { return ; } if (tab.type === 'redis-keys') { return ; } if (tab.type === 'redis-command') { return ; } if (tab.type === 'redis-monitor') { return ; } if (tab.type === 'trigger') { return ; } if (tab.type === 'view-def' || tab.type === 'event-def' || tab.type === 'routine-def' || tab.type === 'sequence-def' || tab.type === 'package-def') { return ; } if (tab.type === 'table-overview') { return ; } if (tab.type === 'table-export') { return ; } if (tab.type === 'sql-analysis') { return ; } if (tab.type === 'jvm-overview') { return ; } if (tab.type === 'jvm-resource') { return ; } if (tab.type === 'jvm-audit') { return ; } if (tab.type === 'jvm-diagnostic') { return ; } if (tab.type === 'jvm-monitoring') { return ; } return null; }); const TabManager: React.FC = React.memo(() => { const tabs = useStore(state => state.tabs); const connections = useStore(state => state.connections); const theme = useStore(state => state.theme); const appearance = useStore(state => state.appearance); const languagePreference = useStore(state => state.languagePreference); const activeTabId = useStore(state => state.activeTabId); const setActiveTab = useStore(state => state.setActiveTab); const addTab = useStore(state => state.addTab); const closeTab = useStore(state => state.closeTab); const closeOtherTabs = useStore(state => state.closeOtherTabs); const closeTabsToLeft = useStore(state => state.closeTabsToLeft); const closeTabsToRight = useStore(state => state.closeTabsToRight); const closeAllTabs = useStore(state => state.closeAllTabs); const moveTab = useStore(state => state.moveTab); const setAIPanelVisible = useStore(state => state.setAIPanelVisible); const tabsNavBorderColor = theme === 'dark' ? 'rgba(255, 255, 255, 0.09)' : 'rgba(0, 0, 0, 0.08)'; const [draggingTabId, setDraggingTabId] = useState(null); const suppressClickUntilRef = useRef(0); const sensors = useSensors( useSensor(PointerSensor, { activationConstraint: { distance: 8 }, }) ); const isV2Ui = appearance.uiVersion === 'v2'; const hasTabs = tabs.length > 0; const pendingCloseTabIdsRef = useRef>(new Set()); const onChange = (newActiveKey: string) => { setActiveTab(newActiveKey); }; const requestCloseSQLFileTabs = useCallback(async ( targetTabs: TabData[], closeConfirmedTabs: () => void, ) => { const candidateTabs = targetTabs.filter(isSQLFileQueryTab); if (candidateTabs.length === 0) { closeConfirmedTabs(); return; } const closeConfirmedTabsAndClearDrafts = () => { closeConfirmedTabs(); candidateTabs.forEach((tab) => clearSQLFileTabDraft(tab.id)); }; const dirtyTabs: Array<{ tab: TabData; draft: string }> = []; const missingFileTabs: Array<{ tab: TabData; filePath: string }> = []; for (const tab of candidateTabs) { const filePath = getSQLFileTabPath(tab); if (!filePath) continue; try { const res = await ReadSQLFile(filePath); if (!res.success) { if (isSQLFileMissingReadResult(res)) { missingFileTabs.push({ tab, filePath }); continue; } message.error(t('tab_manager.sql_file_close.read_failed_cancel_close', { detail: res.message || filePath })); return; } const draft = getSQLFileTabDraft(tab.id, String(tab.query ?? '')); if (hasSQLFileTabUnsavedChanges({ ...tab, query: draft }, normalizeSQLFileReadContent(res.data))) { dirtyTabs.push({ tab, draft }); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); if (isSQLFileMissingErrorMessage(errorMessage)) { missingFileTabs.push({ tab, filePath }); continue; } message.error(t('tab_manager.sql_file_close.read_failed_cancel_close', { detail: errorMessage })); return; } } const confirmDirtyTabsOrClose = () => { if (dirtyTabs.length === 0) { closeConfirmedTabsAndClearDrafts(); return; } const firstDirtyTab = dirtyTabs[0].tab; const dirtyFilePath = getSQLFileTabPath(firstDirtyTab); const dirtyLabel = dirtyTabs.length === 1 ? t('tab_manager.sql_file_close.dirty_single_label', { title: firstDirtyTab.title || dirtyFilePath }) : t('tab_manager.sql_file_close.dirty_multiple_label', { count: dirtyTabs.length }); let destroyConfirm: (() => void) | null = null; const confirmRef = Modal.confirm({ title: t('tab_manager.sql_file_close.save_confirm_title'), content: t('tab_manager.sql_file_close.save_confirm_content', { label: dirtyLabel }), okText: t('tab_manager.sql_file_close.save_and_close'), cancelText: t('common.cancel'), closable: true, maskClosable: true, okButtonProps: { type: 'primary' }, footer: (_, { OkBtn, CancelBtn }) => ( <> ), onOk: async () => { try { for (const { tab, draft } of dirtyTabs) { const filePath = getSQLFileTabPath(tab); if (!filePath) continue; const res = await WriteSQLFile(filePath, draft); if (!res.success) { throw new Error(t('tab_manager.sql_file_close.save_failed', { title: tab.title || filePath, detail: res.message || t('tab_manager.sql_file_close.unknown_error'), })); } } message.success(t('tab_manager.sql_file_close.saved')); closeConfirmedTabsAndClearDrafts(); } catch (error) { message.error(error instanceof Error ? error.message : String(error)); throw error; } }, }); destroyConfirm = confirmRef.destroy; }; if (missingFileTabs.length > 0) { const firstMissing = missingFileTabs[0]; const missingLabel = missingFileTabs.length === 1 ? t('tab_manager.sql_file_close.missing_single_label', { title: firstMissing.tab.title || firstMissing.filePath }) : t('tab_manager.sql_file_close.missing_multiple_label', { count: missingFileTabs.length }); Modal.confirm({ title: t('tab_manager.sql_file_close.missing_confirm_title'), content: t('tab_manager.sql_file_close.missing_confirm_content', { label: missingLabel }), okText: dirtyTabs.length > 0 ? t('tab_manager.sql_file_close.continue_close') : t('tab_manager.sql_file_close.close_tabs'), cancelText: t('common.cancel'), closable: true, maskClosable: true, okButtonProps: { danger: true }, onOk: () => { confirmDirtyTabsOrClose(); }, }); return; } confirmDirtyTabsOrClose(); }, []); const closeTabsWithSQLFilePrompt = useCallback((targetIds: string[], closeConfirmedTabs: () => void) => { const uniqueIds = Array.from(new Set(targetIds.map((id) => String(id || '').trim()).filter(Boolean))); if (uniqueIds.length === 0) return; const dedupeKey = uniqueIds.slice().sort().join('\n'); if (pendingCloseTabIdsRef.current.has(dedupeKey)) return; pendingCloseTabIdsRef.current.add(dedupeKey); const targetTabs = tabs.filter((tab) => uniqueIds.includes(tab.id)); void requestCloseSQLFileTabs(targetTabs, closeConfirmedTabs).finally(() => { pendingCloseTabIdsRef.current.delete(dedupeKey); }); }, [requestCloseSQLFileTabs, tabs]); const onEdit = (targetKey: React.MouseEvent | React.KeyboardEvent | string, action: 'add' | 'remove') => { if (action === 'remove') { const id = String(targetKey || ''); closeTabsWithSQLFilePrompt([id], () => closeTab(id)); } }; const handleDragStart = (event: DragStartEvent) => { const sourceId = String(event.active.id || '').trim(); setDraggingTabId(sourceId || null); }; const handleDragEnd = (event: DragEndEvent) => { const sourceId = String(event.active.id || '').trim(); const targetId = String(event.over?.id || '').trim(); setDraggingTabId(null); if (!sourceId || !targetId || sourceId === targetId) { return; } suppressClickUntilRef.current = Date.now() + 120; moveTab(sourceId, targetId); }; const handleDragCancel = () => { setDraggingTabId(null); }; React.useEffect(() => { const handleGlobalInsertSql = (e: any) => { const { sql, runImmediately, connectionId: eventConnId, dbName: eventDbName } = e.detail; if (!sql) return; const activeTab = tabs.find(t => t.id === activeTabId); // 🔧 runImmediately(点击"执行")始终新建独立 tab,避免追加到已有 tab 导致 SQL 重复 if (runImmediately) { const newTabId = 'tab-' + Date.now(); const resolvedConnId = eventConnId || activeTab?.connectionId || (connections.length > 0 ? connections[0].id : ''); const resolvedDbName = eventConnId ? (eventDbName || '') : (activeTab?.dbName || ''); addTab({ id: newTabId, type: 'query', title: t('query.new'), query: sql, connectionId: resolvedConnId, dbName: resolvedDbName }); setActiveTab(newTabId); setTimeout(() => { window.dispatchEvent(new CustomEvent('gonavi:insert-sql-to-tab', { detail: { tabId: newTabId, sql, runImmediately: true, connectionId: resolvedConnId, dbName: resolvedDbName } })); }, 300); return; } // 插入模式:追加到已有 tab 或新建 tab if (activeTab && activeTab.type === 'query') { window.dispatchEvent(new CustomEvent('gonavi:insert-sql-to-tab', { detail: { tabId: activeTab.id, sql, runImmediately: false, connectionId: eventConnId, dbName: eventDbName } })); } else { const newTabId = 'tab-' + Date.now(); const resolvedConnId = eventConnId || activeTab?.connectionId || (connections.length > 0 ? connections[0].id : ''); const resolvedDbName = eventConnId ? (eventDbName || '') : (activeTab?.dbName || ''); addTab({ id: newTabId, type: 'query', title: t('query.new'), query: sql, connectionId: resolvedConnId, dbName: resolvedDbName }); setActiveTab(newTabId); } }; window.addEventListener('gonavi:insert-sql', handleGlobalInsertSql); return () => window.removeEventListener('gonavi:insert-sql', handleGlobalInsertSql); }, [tabs, activeTabId, addTab, setActiveTab, connections]); const tabIds = useMemo(() => tabs.map((tab) => tab.id), [tabs]); const hasDoubleLineTabLabel = useMemo(() => ( tabs.some((tab) => { const connection = connections.find((conn) => conn.id === tab.connectionId); const displayModel = buildTabDisplayModel(tab, connection, appearance.tabDisplay, t); return displayModel.layout === 'double' && Boolean(displayModel.secondaryText); }) ), [appearance.tabDisplay, connections, tabs]); const renderTabBar: TabsProps['renderTabBar'] = (tabBarProps, DefaultTabBar) => ( {(node) => } ); const items = useMemo(() => tabs.map((tab, index) => { const connection = connections.find((conn) => conn.id === tab.connectionId); const displayModel = buildTabDisplayModel(tab, connection, appearance.tabDisplay, t); const displayTitle = displayModel.fullTitle; const hostSummary = resolveConnectionHostSummary(connection?.config); const tabIsActive = tab.id === activeTabId; const menuItems: MenuProps['items'] = [ { key: 'tab-display-settings', icon: , label: t('tab_manager.menu.tab_display_settings'), onClick: openTabDisplaySettings, }, { type: 'divider' }, { key: 'close-other', label: t('tab_manager.menu.close_other'), disabled: tabs.length <= 1, onClick: () => closeTabsWithSQLFilePrompt(getCloseOtherTabIds(tabs, tab.id), () => closeOtherTabs(tab.id)), }, { key: 'close-left', label: t('tab_manager.menu.close_left'), disabled: index === 0, onClick: () => closeTabsWithSQLFilePrompt(getCloseTabsToLeftIds(tabs, tab.id), () => closeTabsToLeft(tab.id)), }, { key: 'close-right', label: t('tab_manager.menu.close_right'), disabled: index === tabs.length - 1, onClick: () => closeTabsWithSQLFilePrompt(getCloseTabsToRightIds(tabs, tab.id), () => closeTabsToRight(tab.id)), }, { type: 'divider' }, { key: 'close-all', label: t('tab_manager.menu.close_all'), disabled: tabs.length === 0, onClick: () => closeTabsWithSQLFilePrompt(tabs.map((item) => item.id), () => closeAllTabs()), }, ]; return { label: ( closeTabsWithSQLFilePrompt([tab.id], () => closeTab(tab.id))} /> ), key: tab.id, closable: !isV2Ui, children: , }; }), [tabs, connections, appearance.tabDisplay, activeTabId, closeOtherTabs, closeTabsToLeft, closeTabsToRight, closeAllTabs, closeTab, closeTabsWithSQLFilePrompt, isV2Ui, languagePreference]); const handleOpenConnectionModal = () => { const target = document.querySelector('[data-gonavi-create-connection-action="true"]'); target?.click(); }; const handleOpenAI = () => { setAIPanelVisible(true); }; const EmptyWorkbench = (
{t('tab_manager.empty.eyebrow.workbench')} {t('tab_manager.empty.eyebrow.connections', { count: connections.length })}

{t('tab_manager.empty.hero.title')}

{t('tab_manager.empty.hero.description')}

{t('tab_manager.empty.quick.heading')}
); return (
{isV2Ui && !hasTabs ? ( EmptyWorkbench ) : ( { if (Date.now() < suppressClickUntilRef.current) return; onChange(newActiveKey); }} activeKey={activeTabId || undefined} onEdit={onEdit} items={items} hideAdd renderTabBar={renderTabBar} /> )}
); }); export default TabManager;