Files
MyGoNavi/frontend/src/components/TabManager.tsx
Syngnat ec23d72332 🐛 fix(TabManager): 修复数据视图高度异常
- 补齐标签页工作台 flex 高度链

- 确保旧版 UI 与新版 UI 下 DataGrid 都能撑满父级

- 补充工作台高度布局回归测试
2026-05-23 18:04:18 +08:00

755 lines
27 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useMemo, useRef, useState } from 'react';
import { Button, Dropdown, Tabs, Tooltip } from 'antd';
import { AppstoreOutlined, CloseOutlined, ConsoleSqlOutlined, DatabaseOutlined, PlusOutlined, RobotOutlined } 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 JVMOverview from './JVMOverview';
import JVMResourceBrowser from './JVMResourceBrowser';
import JVMAuditViewer from './JVMAuditViewer';
import JVMDiagnosticConsole from './JVMDiagnosticConsole';
import JVMMonitoringDashboard from './JVMMonitoringDashboard';
import type { TabData } from '../types';
import { buildTabDisplayTitle } from '../utils/tabDisplay';
import { resolveConnectionHostSummary } from '../utils/tabDisplay';
import { resolveConnectionAccentColor } from '../utils/connectionVisual';
const getTabKindLabel = (tab: TabData): string => {
if (tab.type === 'query') return 'SQL';
if (tab.type === 'table') return 'TABLE';
if (tab.type === 'design') return 'DESIGN';
if (tab.type === 'table-overview') return 'DB';
if (tab.type.startsWith('redis')) return 'REDIS';
if (tab.type.startsWith('jvm')) return 'JVM';
if (tab.type === 'trigger') return 'TRG';
if (tab.type === 'view-def') return tab.viewKind === 'materialized' ? 'MV' : 'VIEW';
if (tab.type === 'routine-def') return 'FUNC';
return 'TAB';
};
export const TAB_WORKBENCH_CLASS_NAME = 'tab-workbench';
const getTabKindIcon = (tab: TabData): React.ReactNode => {
if (tab.type === 'query') return <ConsoleSqlOutlined />;
if (tab.type === 'table-overview') return <DatabaseOutlined />;
if (tab.type.startsWith('redis')) return <DatabaseOutlined />;
if (tab.type.startsWith('jvm')) return <AppstoreOutlined />;
return <DatabaseOutlined />;
};
const getTabKindTooltipLabel = (tab: TabData): string => {
if (tab.type === 'query') return 'SQL 查询';
if (tab.type === 'table') return '表数据';
if (tab.type === 'design') return '表设计';
if (tab.type === 'table-overview') return '表概览';
if (tab.type === 'redis-keys') return 'Redis Key';
if (tab.type === 'redis-command') return 'Redis 命令';
if (tab.type === 'redis-monitor') return 'Redis 监控';
if (tab.type === 'jvm-overview') return 'JVM 概览';
if (tab.type === 'jvm-resource') return 'JVM 资源';
if (tab.type === 'jvm-audit') return 'JVM 审计';
if (tab.type === 'jvm-diagnostic') return 'JVM 诊断';
if (tab.type === 'jvm-monitoring') return 'JVM 监控';
if (tab.type === 'trigger') return '触发器';
if (tab.type === 'view-def') return tab.viewKind === 'materialized' ? '物化视图' : '视图';
if (tab.type === 'routine-def') return '函数 / 过程';
return '标签页';
};
const getTabObjectLabel = (tab: TabData): string => {
if (tab.tableName) return tab.tableName;
if (tab.viewName) return tab.viewName;
if (tab.routineName) return tab.routineName;
if (tab.triggerName) return tab.triggerName;
if (tab.resourcePath) return tab.resourcePath;
if (tab.filePath) return tab.filePath;
if (tab.type.startsWith('redis')) return `db${tab.redisDB ?? 0}`;
return '';
};
export const stopTabHoverDragPropagation = (event: React.SyntheticEvent<HTMLElement>) => {
event.stopPropagation();
};
export const resolveTabHoverOpen = (isHoverInfoOpen: boolean, isTabMenuOpen: boolean) =>
isHoverInfoOpen && !isTabMenuOpen;
type TabHoverInfoProps = {
tab: TabData;
displayTitle: string;
connectionLabel?: string;
hostSummary?: string;
};
export const TabHoverInfo: React.FC<TabHoverInfoProps> = ({
tab,
displayTitle,
connectionLabel,
hostSummary,
}) => {
const objectLabel = getTabObjectLabel(tab);
const rows = [
['类型', getTabKindTooltipLabel(tab)],
['连接', connectionLabel || '未绑定连接'],
['Host', hostSummary || '未配置'],
['数据库', tab.dbName || '未指定'],
['对象', objectLabel],
].filter(([, value]) => Boolean(value));
return (
<div
className="gn-v2-tab-hover-card"
data-tab-hover-info="true"
onPointerDown={stopTabHoverDragPropagation}
onPointerMove={stopTabHoverDragPropagation}
onPointerUp={stopTabHoverDragPropagation}
onPointerDownCapture={stopTabHoverDragPropagation}
onPointerUpCapture={stopTabHoverDragPropagation}
onMouseDown={stopTabHoverDragPropagation}
onMouseMove={stopTabHoverDragPropagation}
onMouseUp={stopTabHoverDragPropagation}
onClick={stopTabHoverDragPropagation}
onClickCapture={stopTabHoverDragPropagation}
onTouchStart={stopTabHoverDragPropagation}
onTouchMove={stopTabHoverDragPropagation}
onTouchEnd={stopTabHoverDragPropagation}
>
<div className="gn-v2-tab-hover-head">
<span>{getTabKindLabel(tab)}</span>
<strong>{displayTitle}</strong>
</div>
<div className="gn-v2-tab-hover-rows">
{rows.map(([label, value]) => (
<div className="gn-v2-tab-hover-row" key={label}>
<span>{label}</span>
<strong>{value}</strong>
</div>
))}
</div>
</div>
);
};
type SortableTabLabelProps = {
tab: TabData;
displayTitle: string;
menuItems: MenuProps['items'];
accentColor?: string;
connectionLabel?: string;
hostSummary?: string;
isV2Ui?: boolean;
onClose?: () => void;
};
const SortableTabLabel: React.FC<SortableTabLabelProps> = ({
tab,
displayTitle,
menuItems,
accentColor,
connectionLabel,
hostSummary,
isV2Ui,
onClose,
}) => {
const [isHoverInfoOpen, setIsHoverInfoOpen] = useState(false);
const [isTabMenuOpen, setIsTabMenuOpen] = useState(false);
const labelStyle = accentColor
? ({ '--connection-accent': accentColor } as React.CSSProperties)
: undefined;
const handleTabLabelContextMenu = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
setIsHoverInfoOpen(false);
setIsTabMenuOpen(true);
};
const handleTabMenuOpenChange = (open: boolean) => {
setIsTabMenuOpen(open);
setIsHoverInfoOpen(false);
};
const handleHoverInfoOpenChange = (open: boolean) => {
setIsHoverInfoOpen(open && !isTabMenuOpen);
};
const labelNode = (
<span
className={`tab-dnd-label${accentColor ? ' has-connection-accent' : ''}${isV2Ui ? ' gn-v2-tab-label' : ''}`}
onContextMenu={handleTabLabelContextMenu}
title={isV2Ui ? undefined : displayTitle}
style={labelStyle}
>
{isV2Ui ? (
<span className="gn-v2-tab-kind-icon" aria-hidden="true">
{getTabKindIcon(tab)}
</span>
) : null}
{isV2Ui ? <span className="gn-v2-tab-kind">{getTabKindLabel(tab)}</span> : null}
{accentColor ? <span className="tab-connection-accent" aria-hidden="true" /> : null}
<span className="tab-title-text">{displayTitle}</span>
{isV2Ui && connectionLabel ? <span className="gn-v2-tab-conn">{connectionLabel}</span> : null}
{isV2Ui && onClose ? (
<button
type="button"
className="gn-v2-tab-close"
aria-label={`关闭 ${displayTitle}`}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
onClose();
}}
>
<CloseOutlined />
</button>
) : null}
</span>
);
const wrappedLabel = isV2Ui ? (
<Tooltip
title={(
<TabHoverInfo
tab={tab}
displayTitle={displayTitle}
connectionLabel={connectionLabel}
hostSummary={hostSummary}
/>
)}
placement="bottomLeft"
mouseEnterDelay={0.25}
open={resolveTabHoverOpen(isHoverInfoOpen, isTabMenuOpen)}
onOpenChange={handleHoverInfoOpenChange}
destroyOnHidden
rootClassName="gn-v2-tab-hover-tooltip"
>
{labelNode}
</Tooltip>
) : labelNode;
return (
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']} onOpenChange={handleTabMenuOpenChange}>
{wrappedLabel}
</Dropdown>
);
};
type DraggableTabNodeProps = {
node: React.ReactElement;
};
const DraggableTabNode: React.FC<DraggableTabNodeProps> = ({ 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 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 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<string | null>(null);
const suppressClickUntilRef = useRef<number>(0);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 8 },
})
);
const isV2Ui = appearance.uiVersion === 'v2';
const hasTabs = tabs.length > 0;
const onChange = (newActiveKey: string) => {
setActiveTab(newActiveKey);
};
const onEdit = (targetKey: React.MouseEvent | React.KeyboardEvent | string, action: 'add' | 'remove') => {
if (action === 'remove') {
closeTab(targetKey as string);
}
};
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: '新建查询',
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: '新建查询',
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 renderTabBar: TabsProps['renderTabBar'] = (tabBarProps, DefaultTabBar) => (
<DefaultTabBar {...tabBarProps}>
{(node) => <DraggableTabNode key={node.key} node={node} />}
</DefaultTabBar>
);
const items = useMemo(() => tabs.map((tab, index) => {
const connection = connections.find((conn) => conn.id === tab.connectionId);
const displayTitle = buildTabDisplayTitle(tab, connection);
const accentColor = connection ? resolveConnectionAccentColor(connection) : undefined;
const hostSummary = resolveConnectionHostSummary(connection?.config);
const tabIsActive = tab.id === activeTabId;
let content;
if (tab.type === 'query') {
content = <QueryEditor tab={tab} isActive={tabIsActive} />;
} else if (tab.type === 'table') {
content = <DataViewer tab={tab} isActive={tabIsActive} />;
} else if (tab.type === 'design') {
content = <TableDesigner tab={tab} />;
} else if (tab.type === 'redis-keys') {
content = <RedisViewer connectionId={tab.connectionId} redisDB={tab.redisDB ?? 0} />;
} else if (tab.type === 'redis-command') {
content = <RedisCommandEditor connectionId={tab.connectionId} redisDB={tab.redisDB ?? 0} />;
} else if (tab.type === 'redis-monitor') {
content = <RedisMonitor connectionId={tab.connectionId} redisDB={tab.redisDB ?? 0} />;
} else if (tab.type === 'trigger') {
content = <TriggerViewer tab={tab} />;
} else if (tab.type === 'view-def' || tab.type === 'routine-def') {
content = <DefinitionViewer tab={tab} />;
} else if (tab.type === 'table-overview') {
content = <TableOverview tab={tab} />;
} else if (tab.type === 'jvm-overview') {
content = <JVMOverview tab={tab} />;
} else if (tab.type === 'jvm-resource') {
content = <JVMResourceBrowser tab={tab} />;
} else if (tab.type === 'jvm-audit') {
content = <JVMAuditViewer tab={tab} />;
} else if (tab.type === 'jvm-diagnostic') {
content = <JVMDiagnosticConsole tab={tab} />;
} else if (tab.type === 'jvm-monitoring') {
content = <JVMMonitoringDashboard tab={tab} />;
}
const menuItems: MenuProps['items'] = [
{
key: 'close-other',
label: '关闭其他页',
disabled: tabs.length <= 1,
onClick: () => closeOtherTabs(tab.id),
},
{
key: 'close-left',
label: '关闭左侧',
disabled: index === 0,
onClick: () => closeTabsToLeft(tab.id),
},
{
key: 'close-right',
label: '关闭右侧',
disabled: index === tabs.length - 1,
onClick: () => closeTabsToRight(tab.id),
},
{ type: 'divider' },
{
key: 'close-all',
label: '关闭所有',
disabled: tabs.length === 0,
onClick: () => closeAllTabs(),
},
];
return {
label: (
<SortableTabLabel
tab={tab}
displayTitle={displayTitle}
menuItems={menuItems}
accentColor={accentColor}
connectionLabel={connection?.name}
hostSummary={hostSummary}
isV2Ui={isV2Ui}
onClose={() => closeTab(tab.id)}
/>
),
key: tab.id,
closable: !isV2Ui,
children: content,
};
}), [tabs, connections, activeTabId, closeOtherTabs, closeTabsToLeft, closeTabsToRight, closeAllTabs, closeTab, isV2Ui]);
const handleOpenConnectionModal = () => {
const target = document.querySelector<HTMLButtonElement>('[data-gonavi-create-connection-action="true"]');
target?.click();
};
const handleOpenAI = () => {
setAIPanelVisible(true);
};
const EmptyWorkbench = (
<div className="gn-v2-empty-workbench">
<section className="gn-v2-empty-hero" aria-label="GoNavi 起始工作台">
<div className="gn-v2-empty-eyebrow">
<span>WORKBENCH</span>
<span>{connections.length} connections</span>
</div>
<h1></h1>
<p> AI </p>
<div className="gn-v2-empty-actions">
<Button type="primary" icon={<PlusOutlined />} onClick={handleOpenConnectionModal}>
</Button>
<Button icon={<ConsoleSqlOutlined />} onClick={() => window.dispatchEvent(new CustomEvent('gonavi:create-query-tab'))}>
</Button>
<Button icon={<RobotOutlined />} onClick={handleOpenAI}>
AI
</Button>
</div>
</section>
<section className="gn-v2-empty-panel" aria-label="快捷工作流">
<div className="gn-v2-panel-heading">
<span></span>
<AppstoreOutlined />
</div>
<button type="button" onClick={handleOpenConnectionModal}>
<DatabaseOutlined />
<span>
<strong></strong>
<small>URISSH</small>
</span>
</button>
<button type="button" onClick={() => window.dispatchEvent(new CustomEvent('gonavi:create-query-tab'))}>
<ConsoleSqlOutlined />
<span>
<strong> SQL </strong>
<small></small>
</span>
</button>
<button type="button" onClick={handleOpenAI}>
<RobotOutlined />
<span>
<strong> AI </strong>
<small> SQL</small>
</span>
</button>
</section>
</div>
);
return (
<div className={`${TAB_WORKBENCH_CLASS_NAME}${isV2Ui ? ' gn-v2-tab-workbench' : ''}`}>
<style>{`
.${TAB_WORKBENCH_CLASS_NAME} {
height: 100%;
flex: 1 1 auto;
min-height: 0;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.main-tabs {
height: 100%;
flex: 1 1 auto;
min-height: 0;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.main-tabs .ant-tabs-nav {
flex: 0 0 auto;
margin: 0;
}
.main-tabs .ant-tabs-content-holder {
flex: 1 1 auto;
min-height: 0;
min-width: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.main-tabs .ant-tabs-content {
flex: 1 1 auto;
min-height: 0;
min-width: 0;
display: flex;
flex-direction: column;
}
.main-tabs .ant-tabs-tabpane {
flex: 1 1 auto;
min-height: 0;
min-width: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.main-tabs .ant-tabs-tabpane > div {
flex: 1 1 auto;
min-height: 0;
min-width: 0;
}
.main-tabs .ant-tabs-tabpane-hidden {
display: none !important;
}
.main-tabs .ant-tabs-nav::before {
border-bottom: 1px solid ${tabsNavBorderColor} !important;
}
.main-tabs .ant-tabs-tab {
transition: transform 180ms cubic-bezier(0.22, 1, 0.36, 1), background-color 120ms ease;
}
.main-tabs .tab-dnd-label {
user-select: none;
-webkit-user-select: none;
display: inline-flex;
align-items: center;
gap: 7px;
max-width: 100%;
}
.main-tabs .tab-dnd-label.has-connection-accent {
position: relative;
}
.main-tabs .tab-connection-accent {
width: 9px;
height: 9px;
border-radius: 999px;
background: var(--connection-accent);
box-shadow: 0 0 0 2px color-mix(in srgb, var(--connection-accent) 22%, transparent);
flex: 0 0 auto;
}
.main-tabs .tab-title-text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.main-tabs .tab-dnd-node.is-dragging,
.main-tabs .tab-dnd-node.is-dragging .tab-dnd-label {
cursor: grabbing !important;
}
body[data-theme='dark'] .main-tabs .ant-tabs-tab-btn:focus-visible {
outline: none !important;
border-radius: 6px;
box-shadow: 0 0 0 2px rgba(255, 214, 102, 0.72);
background: rgba(255, 214, 102, 0.16);
}
body[data-theme='light'] .main-tabs .ant-tabs-tab-btn:focus-visible {
outline: none !important;
border-radius: 6px;
box-shadow: 0 0 0 2px rgba(9, 109, 217, 0.32);
background: rgba(9, 109, 217, 0.08);
}
body[data-theme='light'] .main-tabs .ant-tabs-tab.ant-tabs-tab-active {
background: rgba(24, 144, 255, 0.10) !important;
border-color: rgba(24, 144, 255, 0.28) !important;
}
body[data-theme='dark'] .main-tabs .ant-tabs-tab.ant-tabs-tab-active {
background: rgba(255, 214, 102, 0.12) !important;
border-color: rgba(255, 214, 102, 0.4) !important;
}
body[data-ui-version='v2'] .main-tabs .ant-tabs-tab.ant-tabs-tab-active {
background: var(--gn-bg-panel) !important;
border-color: var(--gn-br-2) !important;
}
body[data-ui-version='v2'] .gn-v2-tab-hover-tooltip .ant-tooltip-inner {
min-width: 260px;
padding: 0;
}
body[data-ui-version='v2'] .gn-v2-tab-hover-tooltip {
pointer-events: auto;
}
body[data-ui-version='v2'] .gn-v2-tab-hover-card {
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px;
color: var(--gn-fg-2);
cursor: text;
user-select: text;
-webkit-user-select: text;
}
body[data-ui-version='v2'] .gn-v2-tab-hover-card * {
user-select: text;
-webkit-user-select: text;
}
body[data-ui-version='v2'] .gn-v2-tab-hover-head {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
body[data-ui-version='v2'] .gn-v2-tab-hover-head > span {
flex: 0 0 auto;
padding: 2px 6px;
border-radius: 5px;
background: var(--gn-bg-active);
color: var(--gn-accent-2);
font-family: var(--gn-font-mono);
font-size: 10px;
font-weight: 700;
line-height: 14px;
}
body[data-ui-version='v2'] .gn-v2-tab-hover-head > strong {
min-width: 0;
overflow: hidden;
color: var(--gn-fg-1);
font-size: var(--gn-font-size-sm, 12px);
font-weight: 700;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
body[data-ui-version='v2'] .gn-v2-tab-hover-rows {
display: grid;
gap: 5px;
}
body[data-ui-version='v2'] .gn-v2-tab-hover-row {
display: grid;
grid-template-columns: 52px minmax(0, 1fr);
align-items: start;
gap: 8px;
font-size: var(--gn-font-size-sm, 12px);
line-height: 18px;
}
body[data-ui-version='v2'] .gn-v2-tab-hover-row > span {
color: var(--gn-fg-5);
}
body[data-ui-version='v2'] .gn-v2-tab-hover-row > strong {
min-width: 0;
overflow-wrap: anywhere;
color: var(--gn-fg-2);
font-weight: 600;
}
`}</style>
{isV2Ui && !hasTabs ? (
EmptyWorkbench
) : (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
modifiers={[restrictToHorizontalAxis]}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
>
<SortableContext items={tabIds} strategy={horizontalListSortingStrategy}>
<Tabs
className={`main-tabs${isV2Ui ? ' gn-v2-main-tabs' : ''}`}
type="editable-card"
destroyOnHidden={false}
onChange={(newActiveKey) => {
if (Date.now() < suppressClickUntilRef.current) return;
onChange(newActiveKey);
}}
activeKey={activeTabId || undefined}
onEdit={onEdit}
items={items}
hideAdd
renderTabBar={renderTabBar}
/>
</SortableContext>
</DndContext>
)}
</div>
);
});
export default TabManager;