import React, { useCallback, useMemo, useRef } from 'react'; import { Button, Tooltip } from 'antd'; import { CloseOutlined, CompressOutlined } from '@ant-design/icons'; import { useStore } from '../store'; import { t } from '../i18n'; import { buildTabDisplayModel, resolveConnectionHostSummary, } from '../utils/tabDisplay'; import { clamp, DEFAULT_DETACHED_WINDOW_MIN_HEIGHT, DEFAULT_DETACHED_WINDOW_MIN_WIDTH, DETACHED_WINDOW_VIEWPORT_PADDING, resolveDetachedWindowTitle, } from '../utils/detachedWindow'; import WorkbenchTabContent from './WorkbenchTabContent'; const getTabKindLabel = (type: string): string => { if (type === 'query') return t('tab_manager.kind_badge.query'); if (type === 'table') return t('tab_manager.kind_badge.table'); if (type === 'design') return t('tab_manager.kind_badge.design'); if (type === 'table-overview') return t('tab_manager.kind_badge.table_overview'); if (type === 'table-export') return t('tab_manager.kind_badge.table_export'); if (type === 'sql-file-execution') return t('sidebar.sql_file_exec.title'); if (type === 'sql-analysis') return t('tab_manager.kind_badge.sql_analysis'); if (type.startsWith('redis')) return t('tab_manager.kind_badge.redis'); if (type.startsWith('jvm')) return t('tab_manager.kind_badge.jvm'); if (type === 'trigger') return t('tab_manager.kind_badge.trigger'); if (type === 'view-def') return t('tab_manager.kind_badge.view'); if (type === 'event-def') return t('tab_manager.kind_badge.event'); if (type === 'routine-def') return t('tab_manager.kind_badge.routine'); if (type === 'sequence-def') return t('tab_manager.kind_badge.sequence'); if (type === 'package-def') return t('tab_manager.kind_badge.package'); return t('tab_manager.kind_badge.fallback'); }; type DragMode = 'move' | 'resize-e' | 'resize-s' | 'resize-se'; const FloatingWorkbenchWindows: React.FC = () => { const tabs = useStore((state) => state.tabs); const connections = useStore((state) => state.connections); const appearance = useStore((state) => state.appearance); const theme = useStore((state) => state.theme); const detachedWorkbenchWindows = useStore((state) => state.detachedWorkbenchWindows); const activeTabId = useStore((state) => state.activeTabId); const attachWorkbenchTab = useStore((state) => state.attachWorkbenchTab); const closeTab = useStore((state) => state.closeTab); const updateDetachedWorkbenchBounds = useStore((state) => state.updateDetachedWorkbenchBounds); const focusDetachedWorkbenchTab = useStore((state) => state.focusDetachedWorkbenchTab); const dragRef = useRef<{ tabId: string; mode: DragMode; startX: number; startY: number; originX: number; originY: number; originW: number; originH: number; } | null>(null); const windowModels = useMemo(() => { return detachedWorkbenchWindows .map((windowState) => { const tab = tabs.find((item) => item.id === windowState.tabId); if (!tab) return null; const connection = connections.find((conn) => conn.id === tab.connectionId); const displayModel = buildTabDisplayModel(tab, connection, appearance.tabDisplay, t); const kindLabel = getTabKindLabel(tab.type); const objectLabel = tab.tableName || tab.viewName || tab.eventName || tab.routineName || tab.sequenceName || tab.packageName || tab.triggerName || tab.resourcePath || tab.filePath || ''; const title = resolveDetachedWindowTitle({ kindLabel, objectLabel, fallbackTitle: displayModel.fullTitle || tab.title || t('tab_manager.detached.title_fallback'), }); return { windowState, tab, title, hostSummary: resolveConnectionHostSummary(connection?.config), connectionName: connection?.name || '', isFocused: activeTabId === tab.id, }; }) .filter(Boolean) as Array<{ windowState: (typeof detachedWorkbenchWindows)[number]; tab: (typeof tabs)[number]; title: string; hostSummary: string; connectionName: string; isFocused: boolean; }>; }, [activeTabId, appearance.tabDisplay, connections, detachedWorkbenchWindows, tabs]); const startInteraction = useCallback(( event: React.PointerEvent, tabId: string, mode: DragMode, bounds: { x: number; y: number; width: number; height: number }, ) => { if (event.button !== 0) return; event.preventDefault(); event.stopPropagation(); focusDetachedWorkbenchTab(tabId); dragRef.current = { tabId, mode, startX: event.clientX, startY: event.clientY, originX: bounds.x, originY: bounds.y, originW: bounds.width, originH: bounds.height, }; const handleMove = (moveEvent: PointerEvent) => { const drag = dragRef.current; if (!drag) return; const dx = moveEvent.clientX - drag.startX; const dy = moveEvent.clientY - drag.startY; if (drag.mode === 'move') { const maxX = Math.max( DETACHED_WINDOW_VIEWPORT_PADDING, window.innerWidth - drag.originW - DETACHED_WINDOW_VIEWPORT_PADDING, ); const maxY = Math.max( DETACHED_WINDOW_VIEWPORT_PADDING, window.innerHeight - drag.originH - DETACHED_WINDOW_VIEWPORT_PADDING, ); updateDetachedWorkbenchBounds(drag.tabId, { x: clamp(drag.originX + dx, DETACHED_WINDOW_VIEWPORT_PADDING, maxX), y: clamp(drag.originY + dy, DETACHED_WINDOW_VIEWPORT_PADDING, maxY), }); return; } let nextW = drag.originW; let nextH = drag.originH; if (drag.mode === 'resize-e' || drag.mode === 'resize-se') { nextW = clamp( drag.originW + dx, DEFAULT_DETACHED_WINDOW_MIN_WIDTH, window.innerWidth - drag.originX - DETACHED_WINDOW_VIEWPORT_PADDING, ); } if (drag.mode === 'resize-s' || drag.mode === 'resize-se') { nextH = clamp( drag.originH + dy, DEFAULT_DETACHED_WINDOW_MIN_HEIGHT, window.innerHeight - drag.originY - DETACHED_WINDOW_VIEWPORT_PADDING, ); } updateDetachedWorkbenchBounds(drag.tabId, { width: nextW, height: nextH }); }; const stop = () => { dragRef.current = null; window.removeEventListener('pointermove', handleMove); window.removeEventListener('pointerup', stop); window.removeEventListener('pointercancel', stop); }; window.addEventListener('pointermove', handleMove); window.addEventListener('pointerup', stop); window.addEventListener('pointercancel', stop); }, [focusDetachedWorkbenchTab, updateDetachedWorkbenchBounds]); if (windowModels.length === 0) { return null; } const isDark = theme === 'dark'; return (
{windowModels.map(({ windowState, tab, title, hostSummary, connectionName, isFocused }) => (
focusDetachedWorkbenchTab(windowState.tabId)} >
startInteraction(event, windowState.tabId, 'move', windowState)} >
{title} {(connectionName || hostSummary) ? ( {[connectionName, hostSummary].filter(Boolean).join(' ยท ')} ) : null}
event.stopPropagation()}>
startInteraction(event, windowState.tabId, 'resize-e', windowState)} />
startInteraction(event, windowState.tabId, 'resize-s', windowState)} />
startInteraction(event, windowState.tabId, 'resize-se', windowState)} />
))}
); }; export default FloatingWorkbenchWindows;