mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-28 09:21:38 +08:00
♻️ refactor(sidebar): 拆分动作与搜索逻辑
This commit is contained in:
@@ -72,6 +72,11 @@ const readSidebarSource = () => [
|
||||
readSourceFile('./sidebar/SidebarEntityModals.tsx'),
|
||||
readSourceFile('./sidebar/SidebarTreeTitle.tsx'),
|
||||
readSourceFile('./sidebar/useSidebarV2ContextMenu.tsx'),
|
||||
readSourceFile('./sidebar/useSidebarObjectActions.tsx'),
|
||||
readSourceFile('./sidebar/useSidebarSearchModel.tsx'),
|
||||
readSourceFile('./sidebar/useSidebarV2ActionHandlers.tsx'),
|
||||
readSourceFile('./sidebar/useSidebarCommandSearchRunner.ts'),
|
||||
readSourceFile('./sidebar/useSidebarTitleRender.tsx'),
|
||||
readSourceFile('./sidebarV2Utils.ts'),
|
||||
].join('\n');
|
||||
const readLegacyNodeMenuSource = () => readSourceFile('./sidebar/sidebarLegacyNodeMenu.tsx');
|
||||
@@ -1319,17 +1324,17 @@ describe('Sidebar locate toolbar', () => {
|
||||
],
|
||||
}];
|
||||
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'all')[0].children?.map((node) => node.key)).toEqual([
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'all')[0].children?.map((node: { key: string }) => node.key)).toEqual([
|
||||
'conn-main-queries',
|
||||
'conn-main-tables',
|
||||
'conn-main-views',
|
||||
'conn-main-routines',
|
||||
'conn-main-events',
|
||||
]);
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'tables')[0].children?.map((node) => node.key)).toEqual(['conn-main-tables']);
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'views')[0].children?.map((node) => node.key)).toEqual(['conn-main-views']);
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'routines')[0].children?.map((node) => node.key)).toEqual(['conn-main-routines']);
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'events')[0].children?.map((node) => node.key)).toEqual(['conn-main-events']);
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'tables')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-tables']);
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'views')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-views']);
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'routines')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-routines']);
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'events')[0].children?.map((node: { key: string }) => node.key)).toEqual(['conn-main-events']);
|
||||
});
|
||||
|
||||
it('hides external SQL roots from v2 object kind filters', () => {
|
||||
@@ -1362,11 +1367,11 @@ describe('Sidebar locate toolbar', () => {
|
||||
},
|
||||
];
|
||||
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'all').map((node) => node.key)).toEqual([
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'all').map((node: { key: string }) => node.key)).toEqual([
|
||||
'conn-main',
|
||||
'external-sql-root',
|
||||
]);
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'tables').map((node) => node.key)).toEqual(['conn-main']);
|
||||
expect(filterV2ExplorerTreeByKind(tree, 'tables').map((node: { key: string }) => node.key)).toEqual(['conn-main']);
|
||||
});
|
||||
|
||||
it('adds rename to the saved query context menu', () => {
|
||||
@@ -2042,25 +2047,27 @@ describe('Sidebar locate toolbar', () => {
|
||||
|
||||
it('routes v2 database context menu shell copy through i18n wrappers in Sidebar', () => {
|
||||
const source = readSidebarSource();
|
||||
const createSchemaSource = source.slice(
|
||||
source.indexOf('const openCreateSchemaModal = (node: any) => {'),
|
||||
source.indexOf('const buildRuntimeConfig = (conn: any, overrideDatabase?: string, clearDatabase: boolean = false) => {'),
|
||||
const objectActionsSource = readSourceFile('./sidebar/useSidebarObjectActions.tsx');
|
||||
const v2ActionHandlersSource = readSourceFile('./sidebar/useSidebarV2ActionHandlers.tsx');
|
||||
const createSchemaSource = objectActionsSource.slice(
|
||||
objectActionsSource.indexOf('const openCreateSchemaModal = (node: any) => {'),
|
||||
objectActionsSource.indexOf('const openRenameSchemaModal = (node: any) => {'),
|
||||
);
|
||||
const runSqlSource = source.slice(
|
||||
source.indexOf('const handleRunSQLFile = async (node: any) => {'),
|
||||
source.indexOf('const handleOpenSQLFileFromToolbar = async () => {'),
|
||||
);
|
||||
const databaseShellSource = source.slice(
|
||||
source.indexOf('const handleRenameDatabase = async () => {'),
|
||||
source.indexOf('const handleRenameTable = async () => {'),
|
||||
const databaseShellSource = objectActionsSource.slice(
|
||||
objectActionsSource.indexOf('const handleRenameDatabase = async () => {'),
|
||||
objectActionsSource.indexOf('const handleRenameTable = async () => {'),
|
||||
);
|
||||
const databaseActionSource = source.slice(
|
||||
source.indexOf('const closeDatabaseNode = (node: any) => {'),
|
||||
source.indexOf('const refreshConnectionNode = (node: any) => {'),
|
||||
const databaseActionSource = v2ActionHandlersSource.slice(
|
||||
v2ActionHandlersSource.indexOf('const closeDatabaseNode = (node: any) => {'),
|
||||
v2ActionHandlersSource.indexOf('const openDatabaseQuery = (node: any) => {'),
|
||||
);
|
||||
const starRocksSource = source.slice(
|
||||
source.indexOf('const openCreateStarRocksMaterializedView = (node: any) => {'),
|
||||
source.indexOf('const openCreateStarRocksRollup = (node: any) => {'),
|
||||
const starRocksSource = objectActionsSource.slice(
|
||||
objectActionsSource.indexOf('const openCreateStarRocksMaterializedView = (node: any) => {'),
|
||||
objectActionsSource.indexOf('const openCreateStarRocksRollup = (node: any) => {'),
|
||||
);
|
||||
|
||||
expect(createSchemaSource).toContain("message.warning(t('sidebar.message.schema_create_unsupported'))");
|
||||
@@ -2354,9 +2361,9 @@ describe('Sidebar locate toolbar', () => {
|
||||
const externalSqlFileMenuStart = externalSqlDirectoryMenuEnd;
|
||||
const externalSqlFileMenuEnd = legacyMenuSource.indexOf('return [];', externalSqlFileMenuStart);
|
||||
const externalSqlFileMenuSource = legacyMenuSource.slice(externalSqlFileMenuStart, externalSqlFileMenuEnd);
|
||||
const titleRenderStart = source.indexOf('const titleRender = (node: any) => {');
|
||||
const titleRenderEnd = source.indexOf('const handleDrop = (info: any) => {', titleRenderStart);
|
||||
const titleRenderSource = source.slice(titleRenderStart, titleRenderEnd);
|
||||
const titleRenderSource = readSourceFile('./sidebar/useSidebarTitleRender.tsx');
|
||||
const titleRenderStart = titleRenderSource.indexOf('export const useSidebarTitleRender =');
|
||||
const titleRenderEnd = titleRenderSource.length;
|
||||
|
||||
[
|
||||
loadTablesStart,
|
||||
@@ -2621,11 +2628,11 @@ describe('Sidebar locate toolbar', () => {
|
||||
expect(objectGroupTitleSource).toContain(catalogLookup);
|
||||
});
|
||||
|
||||
const titleRenderStart = sidebarSource.indexOf('const titleRender = (node: any) => {');
|
||||
const titleRenderEnd = sidebarSource.indexOf('const handleDrop = (info: any) => {', titleRenderStart);
|
||||
const titleRenderSource = readSourceFile('./sidebar/useSidebarTitleRender.tsx');
|
||||
const titleRenderStart = titleRenderSource.indexOf('export const useSidebarTitleRender =');
|
||||
const titleRenderEnd = titleRenderSource.length;
|
||||
expect(titleRenderStart).toBeGreaterThanOrEqual(0);
|
||||
expect(titleRenderEnd).toBeGreaterThan(titleRenderStart);
|
||||
const titleRenderSource = sidebarSource.slice(titleRenderStart, titleRenderEnd);
|
||||
expect(titleRenderSource).toContain("} else if (node.type === 'object-group') {");
|
||||
expect(titleRenderSource).toContain('const objectGroupTitle = resolveV2ObjectGroupTitle(node);');
|
||||
expect(titleRenderSource).toContain('hoverTitle = objectGroupTitle;');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ import {
|
||||
formatSidebarRowCount,
|
||||
} from './Sidebar';
|
||||
|
||||
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
|
||||
const source = readFileSync(new URL('./sidebar/useSidebarV2ActionHandlers.tsx', import.meta.url), 'utf8');
|
||||
const toggleSidebarTablePinnedSource = source.slice(
|
||||
source.indexOf('const toggleSidebarTablePinned = (node: any, pinned?: boolean) => {'),
|
||||
source.indexOf("const handleTableGroupSortAction = (node: any, sortBy: 'name' | 'frequency') => {"),
|
||||
|
||||
168
frontend/src/components/sidebar/useSidebarCommandSearchRunner.ts
Normal file
168
frontend/src/components/sidebar/useSidebarCommandSearchRunner.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { useCallback, type MutableRefObject, type Dispatch, type SetStateAction } from 'react';
|
||||
|
||||
import type { SavedConnection } from '../../types';
|
||||
import { resolveSidebarNodeConnectionId, shouldRunV2CommandSearchEnter, type SidebarTreeNode as TreeNode, type V2CommandSearchItem } from '../sidebarV2Utils';
|
||||
|
||||
type UseSidebarCommandSearchRunnerArgs = {
|
||||
activeContext: any;
|
||||
activeTab: any;
|
||||
addTab: (tab: any) => void;
|
||||
closeV2CommandSearch: () => void;
|
||||
commandSearchFlatItems: V2CommandSearchItem[];
|
||||
connectionIds: string[];
|
||||
findTreeNodeByKeyRef: MutableRefObject<(nodes: TreeNode[], targetKey: React.Key) => TreeNode | null>;
|
||||
locateObjectInSidebar: (detail: unknown) => Promise<void>;
|
||||
loadDatabases: (node: any) => Promise<void>;
|
||||
mergeExpandedTreeKeys: (requiredKeys: React.Key[]) => void;
|
||||
onDoubleClick: (event: any, node: any) => void;
|
||||
scrollSidebarTreeToKey: (key: React.Key) => void;
|
||||
selectedNodesRef: MutableRefObject<any[]>;
|
||||
setActiveContext: (context: { connectionId: string; dbName: string } | null) => void;
|
||||
setSelectedKeys: Dispatch<SetStateAction<React.Key[]>>;
|
||||
setV2CommandActiveIndex: Dispatch<SetStateAction<number>>;
|
||||
treeDataRef: MutableRefObject<TreeNode[]>;
|
||||
v2CommandActiveIndex: number;
|
||||
};
|
||||
|
||||
export const useSidebarCommandSearchRunner = ({
|
||||
activeContext,
|
||||
activeTab,
|
||||
addTab,
|
||||
closeV2CommandSearch,
|
||||
commandSearchFlatItems,
|
||||
connectionIds,
|
||||
findTreeNodeByKeyRef,
|
||||
locateObjectInSidebar,
|
||||
loadDatabases,
|
||||
mergeExpandedTreeKeys,
|
||||
onDoubleClick,
|
||||
scrollSidebarTreeToKey,
|
||||
selectedNodesRef,
|
||||
setActiveContext,
|
||||
setSelectedKeys,
|
||||
setV2CommandActiveIndex,
|
||||
treeDataRef,
|
||||
v2CommandActiveIndex,
|
||||
}: UseSidebarCommandSearchRunnerArgs) => {
|
||||
const selectConnectionFromRail = useCallback((conn: SavedConnection) => {
|
||||
const key = conn.id;
|
||||
const connectionNode = findTreeNodeByKeyRef.current(treeDataRef.current, key);
|
||||
setSelectedKeys([key]);
|
||||
selectedNodesRef.current = connectionNode ? [connectionNode] : [];
|
||||
setActiveContext({ connectionId: key, dbName: '' });
|
||||
mergeExpandedTreeKeys([key]);
|
||||
const targetNode = connectionNode || {
|
||||
key,
|
||||
dataRef: conn,
|
||||
type: 'connection',
|
||||
};
|
||||
void loadDatabases(targetNode);
|
||||
}, [findTreeNodeByKeyRef, loadDatabases, mergeExpandedTreeKeys, selectedNodesRef, setActiveContext, setSelectedKeys, treeDataRef]);
|
||||
|
||||
const runCommandSearchItem = useCallback((item?: V2CommandSearchItem) => {
|
||||
if (!item) return;
|
||||
closeV2CommandSearch();
|
||||
if (item.kind === 'action') {
|
||||
item.onRun();
|
||||
return;
|
||||
}
|
||||
if (item.kind === 'recent') {
|
||||
addTab({
|
||||
id: `query-${Date.now()}`,
|
||||
title: '最近查询',
|
||||
type: 'query',
|
||||
connectionId: item.connectionId || activeContext?.connectionId || activeTab?.connectionId || '',
|
||||
dbName: item.dbName || activeContext?.dbName || activeTab?.dbName || '',
|
||||
query: item.sql,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const node = item.node;
|
||||
const dataRef = node.dataRef || {};
|
||||
if (node.type === 'connection') {
|
||||
selectConnectionFromRail(dataRef as SavedConnection);
|
||||
return;
|
||||
}
|
||||
if (node.type === 'database') {
|
||||
setActiveContext({ connectionId: resolveSidebarNodeConnectionId(node, connectionIds) || dataRef.id, dbName: dataRef.dbName });
|
||||
mergeExpandedTreeKeys([dataRef.id, node.key]);
|
||||
setSelectedKeys([node.key]);
|
||||
selectedNodesRef.current = [node];
|
||||
scrollSidebarTreeToKey(node.key);
|
||||
return;
|
||||
}
|
||||
if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view') {
|
||||
void locateObjectInSidebar({
|
||||
tabId: String(node.key || ''),
|
||||
connectionId: dataRef.id,
|
||||
dbName: dataRef.dbName,
|
||||
tableName: dataRef.tableName || dataRef.viewName,
|
||||
schemaName: dataRef.schemaName,
|
||||
objectGroup: node.type === 'table' ? 'tables' : (node.type === 'materialized-view' ? 'materializedViews' : 'views'),
|
||||
});
|
||||
onDoubleClick(null, node);
|
||||
return;
|
||||
}
|
||||
if (node.type === 'db-trigger' || node.type === 'db-event' || node.type === 'routine') {
|
||||
setActiveContext({ connectionId: dataRef.id, dbName: dataRef.dbName });
|
||||
setSelectedKeys([node.key]);
|
||||
selectedNodesRef.current = [node];
|
||||
scrollSidebarTreeToKey(node.key);
|
||||
onDoubleClick(null, node);
|
||||
}
|
||||
}, [
|
||||
activeContext,
|
||||
activeTab,
|
||||
addTab,
|
||||
closeV2CommandSearch,
|
||||
connectionIds,
|
||||
locateObjectInSidebar,
|
||||
mergeExpandedTreeKeys,
|
||||
onDoubleClick,
|
||||
scrollSidebarTreeToKey,
|
||||
selectConnectionFromRail,
|
||||
selectedNodesRef,
|
||||
setActiveContext,
|
||||
setSelectedKeys,
|
||||
]);
|
||||
|
||||
const handleV2CommandSearchKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
setV2CommandActiveIndex((prev) => {
|
||||
if (commandSearchFlatItems.length === 0) return 0;
|
||||
return Math.min(prev + 1, commandSearchFlatItems.length - 1);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (event.key === 'ArrowUp') {
|
||||
event.preventDefault();
|
||||
setV2CommandActiveIndex((prev) => Math.max(prev - 1, 0));
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Enter') {
|
||||
if (!shouldRunV2CommandSearchEnter({
|
||||
key: event.key,
|
||||
isComposing: event.nativeEvent.isComposing,
|
||||
keyCode: event.nativeEvent.keyCode,
|
||||
activeItemCount: commandSearchFlatItems.length,
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
runCommandSearchItem(commandSearchFlatItems[v2CommandActiveIndex]);
|
||||
return;
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
closeV2CommandSearch();
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
selectConnectionFromRail,
|
||||
runCommandSearchItem,
|
||||
handleV2CommandSearchKeyDown,
|
||||
};
|
||||
};
|
||||
1210
frontend/src/components/sidebar/useSidebarObjectActions.tsx
Normal file
1210
frontend/src/components/sidebar/useSidebarObjectActions.tsx
Normal file
File diff suppressed because it is too large
Load Diff
669
frontend/src/components/sidebar/useSidebarSearchModel.tsx
Normal file
669
frontend/src/components/sidebar/useSidebarSearchModel.tsx
Normal file
@@ -0,0 +1,669 @@
|
||||
import React, { useCallback, useEffect, useMemo, type Dispatch, type MutableRefObject, type SetStateAction } from 'react';
|
||||
import { Checkbox } from 'antd';
|
||||
import {
|
||||
BarsOutlined,
|
||||
CheckOutlined,
|
||||
ClockCircleOutlined,
|
||||
CloudOutlined,
|
||||
CodeOutlined,
|
||||
DatabaseOutlined,
|
||||
EyeOutlined,
|
||||
FilterOutlined,
|
||||
PlusOutlined,
|
||||
RobotOutlined,
|
||||
TableOutlined,
|
||||
TagOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons';
|
||||
|
||||
import { useStore } from '../../store';
|
||||
import type { SavedConnection } from '../../types';
|
||||
import { getCurrentLanguage, t } from '../../i18n';
|
||||
import { resolveShortcutDisplay } from '../../utils/shortcuts';
|
||||
import { resolveConnectionHostSummary, resolveConnectionHostTokens } from '../../utils/tabDisplay';
|
||||
import { resolveConnectionAccentColor, resolveConnectionIconType } from '../../utils/connectionVisual';
|
||||
import { getDbIcon } from '../DatabaseIcons';
|
||||
import {
|
||||
isV2SidebarObjectNode,
|
||||
parseV2CommandSearchQuery,
|
||||
type V2ExplorerFilter,
|
||||
} from './sidebarHelpers';
|
||||
import type { SearchScope } from '../sidebarCoreUtils';
|
||||
import {
|
||||
V2_TREE_HORIZONTAL_SCROLL_BOTTOM_RESERVE,
|
||||
estimateV2TreeHorizontalScrollWidth,
|
||||
filterV2CommandSearchTreeItems,
|
||||
filterV2ExplorerTreeByKind,
|
||||
resolveSidebarNodeConnectionId,
|
||||
resolveV2ActiveConnectionId,
|
||||
type SidebarTreeNode as TreeNode,
|
||||
type V2CommandSearchItem,
|
||||
} from '../sidebarV2Utils';
|
||||
|
||||
const SEARCH_SCOPE_OPTIONS: Array<{ value: SearchScope; labelKey: string }> = [
|
||||
{ value: 'smart', labelKey: 'sidebar.command_search.scope.smart' },
|
||||
{ value: 'object', labelKey: 'sidebar.command_search.scope.object' },
|
||||
{ value: 'database', labelKey: 'sidebar.command_search.scope.database' },
|
||||
{ value: 'host', labelKey: 'sidebar.command_search.scope.host' },
|
||||
{ value: 'tag', labelKey: 'sidebar.command_search.scope.tag' },
|
||||
];
|
||||
|
||||
const SEARCH_SCOPE_LABEL_KEY_MAP: Record<SearchScope, string> = SEARCH_SCOPE_OPTIONS.reduce((acc, option) => {
|
||||
acc[option.value] = option.labelKey;
|
||||
return acc;
|
||||
}, {} as Record<SearchScope, string>);
|
||||
|
||||
const SEARCH_SCOPE_ICON_MAP: Record<SearchScope, React.ReactNode> = {
|
||||
smart: <ThunderboltOutlined />,
|
||||
object: <TableOutlined />,
|
||||
database: <DatabaseOutlined />,
|
||||
host: <CloudOutlined />,
|
||||
tag: <TagOutlined />,
|
||||
};
|
||||
|
||||
type SidebarSearchModelArgs = {
|
||||
searchScopes: SearchScope[];
|
||||
setSearchScopes: Dispatch<SetStateAction<SearchScope[]>>;
|
||||
setSearchValue: Dispatch<SetStateAction<string>>;
|
||||
deferredSearchValue: string;
|
||||
deferredV2CommandSearchValue: string;
|
||||
v2CommandSearchValue: string;
|
||||
setV2CommandActiveIndex: Dispatch<SetStateAction<number>>;
|
||||
v2ExplorerFilter: V2ExplorerFilter;
|
||||
treeData: TreeNode[];
|
||||
treeViewportWidth: number;
|
||||
treeHeight: number;
|
||||
isV2Ui: boolean;
|
||||
connections: SavedConnection[];
|
||||
connectionIds: string[];
|
||||
selectedKeys: React.Key[];
|
||||
selectedNodesRef: MutableRefObject<any[]>;
|
||||
activeContext: any;
|
||||
activeTab: any;
|
||||
sqlLogs: any[];
|
||||
shortcutOptions: any;
|
||||
activeShortcutPlatform: any;
|
||||
overlayTheme: {
|
||||
sectionBorder: string;
|
||||
mutedText: string;
|
||||
titleText: string;
|
||||
shellBg: string;
|
||||
divider: string;
|
||||
};
|
||||
darkMode: boolean;
|
||||
onCreateConnection?: () => void;
|
||||
onToggleAI?: () => void;
|
||||
onToggleLogPanel?: () => void;
|
||||
setAIPanelVisible: (visible: boolean) => void;
|
||||
extractObjectName: (fullName: string) => string;
|
||||
};
|
||||
|
||||
export const useSidebarSearchModel = ({
|
||||
searchScopes,
|
||||
setSearchScopes,
|
||||
setSearchValue,
|
||||
deferredSearchValue,
|
||||
deferredV2CommandSearchValue,
|
||||
v2CommandSearchValue,
|
||||
setV2CommandActiveIndex,
|
||||
v2ExplorerFilter,
|
||||
treeData,
|
||||
treeViewportWidth,
|
||||
treeHeight,
|
||||
isV2Ui,
|
||||
connections,
|
||||
connectionIds,
|
||||
selectedKeys,
|
||||
selectedNodesRef,
|
||||
activeContext,
|
||||
activeTab,
|
||||
sqlLogs,
|
||||
shortcutOptions,
|
||||
activeShortcutPlatform,
|
||||
overlayTheme,
|
||||
darkMode,
|
||||
onCreateConnection,
|
||||
onToggleAI,
|
||||
onToggleLogPanel,
|
||||
setAIPanelVisible,
|
||||
extractObjectName,
|
||||
}: SidebarSearchModelArgs) => {
|
||||
const onSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = e.target;
|
||||
setSearchValue(value);
|
||||
};
|
||||
|
||||
const toggleSearchScope = (scope: SearchScope) => {
|
||||
setSearchScopes((prev) => {
|
||||
if (scope === 'smart') {
|
||||
return ['smart'];
|
||||
}
|
||||
const withoutSmart = prev.filter((item) => item !== 'smart');
|
||||
if (withoutSmart.includes(scope)) {
|
||||
const next = withoutSmart.filter((item) => item !== scope);
|
||||
return next.length > 0 ? next : ['smart'];
|
||||
}
|
||||
return [...withoutSmart, scope];
|
||||
});
|
||||
};
|
||||
|
||||
const setSearchScopeChecked = (scope: SearchScope, checked: boolean) => {
|
||||
if (scope === 'smart') {
|
||||
if (checked) {
|
||||
setSearchScopes(['smart']);
|
||||
} else if (searchScopes.length === 1 && searchScopes[0] === 'smart') {
|
||||
setSearchScopes(['smart']);
|
||||
} else {
|
||||
setSearchScopes((prev) => {
|
||||
const next = prev.filter((item) => item !== 'smart');
|
||||
return next.length > 0 ? next : ['smart'];
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
setSearchScopes((prev) => {
|
||||
const withoutSmart = prev.filter((item) => item !== 'smart');
|
||||
if (withoutSmart.includes(scope)) {
|
||||
return withoutSmart;
|
||||
}
|
||||
return [...withoutSmart, scope];
|
||||
});
|
||||
} else {
|
||||
setSearchScopes((prev) => {
|
||||
const next = prev.filter((item) => item !== scope && item !== 'smart');
|
||||
return next.length > 0 ? next : ['smart'];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const currentLanguage = getCurrentLanguage();
|
||||
|
||||
const searchScopeSummary = useMemo(() => {
|
||||
if (searchScopes.includes('smart')) {
|
||||
return t('sidebar.command_search.scope.summary_smart');
|
||||
}
|
||||
return searchScopes.map((scope) => t(SEARCH_SCOPE_LABEL_KEY_MAP[scope])).join(' + ');
|
||||
}, [searchScopes, currentLanguage]);
|
||||
|
||||
const searchScopePopoverContent = useMemo(() => {
|
||||
const smartSelected = searchScopes.includes('smart');
|
||||
const scopedOptions = SEARCH_SCOPE_OPTIONS.filter((option) => option.value !== 'smart');
|
||||
const borderColor = overlayTheme.sectionBorder.replace('1px solid ', '');
|
||||
const mutedTextColor = overlayTheme.mutedText;
|
||||
const titleColor = overlayTheme.titleText;
|
||||
const panelBg = overlayTheme.shellBg;
|
||||
const smartBg = smartSelected
|
||||
? (darkMode ? 'linear-gradient(135deg, rgba(255,214,102,0.22) 0%, rgba(255,179,71,0.16) 100%)' : 'linear-gradient(135deg, rgba(255,214,102,0.26) 0%, rgba(255,244,204,0.92) 100%)')
|
||||
: (darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.72)');
|
||||
const smartBorder = smartSelected
|
||||
? (darkMode ? 'rgba(255,214,102,0.42)' : 'rgba(245,176,65,0.34)')
|
||||
: borderColor;
|
||||
const getOptionCardStyle = (checked: boolean) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center' as const,
|
||||
justifyContent: 'space-between' as const,
|
||||
gap: 12,
|
||||
padding: '10px 12px',
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${checked ? (darkMode ? 'rgba(118,169,250,0.44)' : 'rgba(24,144,255,0.32)') : borderColor}`,
|
||||
background: checked
|
||||
? (darkMode ? 'rgba(64,124,255,0.18)' : 'rgba(24,144,255,0.08)')
|
||||
: (darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(255,255,255,0.76)'),
|
||||
transition: 'all 120ms ease',
|
||||
});
|
||||
return (
|
||||
<div style={{ minWidth: 280, display: 'flex', flexDirection: 'column', background: panelBg, padding: 14, gap: 12 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 12 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, letterSpacing: 0.4, color: mutedTextColor, textTransform: 'uppercase' }}>{t('sidebar.command_search.scope.title')}</div>
|
||||
<div style={{ marginTop: 4, fontSize: 13, lineHeight: 1.5, color: mutedTextColor }}>{t('sidebar.command_search.scope.description')}</div>
|
||||
</div>
|
||||
<div style={{ width: 32, height: 32, borderRadius: 10, display: 'grid', placeItems: 'center', background: darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(17,24,39,0.06)', color: darkMode ? '#ffd666' : '#1677ff', flexShrink: 0 }}>
|
||||
<FilterOutlined />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label style={{ display: 'block', cursor: 'pointer' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '12px 14px', borderRadius: 14, border: `1px solid ${smartBorder}`, background: smartBg, boxShadow: smartSelected ? (darkMode ? '0 10px 24px rgba(0,0,0,0.24)' : '0 10px 24px rgba(245,176,65,0.14)') : 'none' }}>
|
||||
<Checkbox
|
||||
checked={smartSelected}
|
||||
onChange={(e) => setSearchScopeChecked('smart', e.target.checked)}
|
||||
/>
|
||||
<div style={{ width: 30, height: 30, borderRadius: 10, display: 'grid', placeItems: 'center', background: darkMode ? 'rgba(255,214,102,0.16)' : 'rgba(255,214,102,0.3)', color: darkMode ? '#ffd666' : '#ad6800', flexShrink: 0 }}>
|
||||
{SEARCH_SCOPE_ICON_MAP.smart}
|
||||
</div>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
||||
<span style={{ fontSize: 14, fontWeight: 700, color: titleColor }}>{t('sidebar.command_search.scope.smart')}</span>
|
||||
<span style={{ padding: '2px 8px', borderRadius: 999, fontSize: 11, fontWeight: 700, color: darkMode ? '#ffe58f' : '#ad6800', background: darkMode ? 'rgba(255,214,102,0.16)' : 'rgba(255,214,102,0.35)' }}>{t('sidebar.command_search.scope.recommended')}</span>
|
||||
</div>
|
||||
<div style={{ marginTop: 3, fontSize: 12, lineHeight: 1.5, color: mutedTextColor }}>{t('sidebar.command_search.scope.smart_help')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<div style={{ height: 1, background: overlayTheme.divider, opacity: 0.9 }} />
|
||||
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, letterSpacing: 0.3, color: mutedTextColor, textTransform: 'uppercase' }}>{t('sidebar.command_search.scope.manual_title')}</div>
|
||||
<div style={{ fontSize: 12, color: mutedTextColor }}>{t('sidebar.command_search.scope.multi_select')}</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gap: 8 }}>
|
||||
{scopedOptions.map((option) => {
|
||||
const checked = searchScopes.includes(option.value);
|
||||
return (
|
||||
<label key={option.value} style={{ display: 'block', cursor: 'pointer' }}>
|
||||
<div style={getOptionCardStyle(checked)}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, minWidth: 0 }}>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={(e) => setSearchScopeChecked(option.value, e.target.checked)}
|
||||
/>
|
||||
<div style={{ width: 28, height: 28, borderRadius: 9, display: 'grid', placeItems: 'center', background: checked ? (darkMode ? 'rgba(118,169,250,0.2)' : 'rgba(24,144,255,0.12)') : (darkMode ? 'rgba(255,255,255,0.05)' : 'rgba(17,24,39,0.06)'), color: checked ? (darkMode ? '#91caff' : '#1677ff') : mutedTextColor, flexShrink: 0 }}>
|
||||
{SEARCH_SCOPE_ICON_MAP[option.value]}
|
||||
</div>
|
||||
<span style={{ fontSize: 14, fontWeight: 600, color: titleColor, whiteSpace: 'nowrap' }}>{t(option.labelKey)}</span>
|
||||
</div>
|
||||
<div style={{ width: 18, display: 'flex', justifyContent: 'center', color: checked ? (darkMode ? '#91caff' : '#1677ff') : 'transparent', flexShrink: 0 }}>
|
||||
<CheckOutlined />
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ padding: '10px 12px', borderRadius: 12, background: darkMode ? 'rgba(255,255,255,0.03)' : 'rgba(17,24,39,0.04)', color: mutedTextColor, fontSize: 12, lineHeight: 1.6 }}>
|
||||
{t('sidebar.command_search.scope.manual_help')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [darkMode, overlayTheme, searchScopes, currentLanguage]);
|
||||
|
||||
const getConnectionHostSearchText = (node: TreeNode): string => {
|
||||
if (node.type !== 'connection') return '';
|
||||
const config = node.dataRef?.config || {};
|
||||
return resolveConnectionHostTokens(config).join(' ');
|
||||
};
|
||||
|
||||
const getConnectionNameSearchText = (node: TreeNode): string => {
|
||||
if (node.type !== 'connection') return '';
|
||||
const name = node.dataRef?.name ?? node.title;
|
||||
return String(name || '').toLowerCase();
|
||||
};
|
||||
|
||||
const matchByScopes = (node: TreeNode, keyword: string, scopes: SearchScope[]): boolean => {
|
||||
const title = String(node.title || '').toLowerCase();
|
||||
if (scopes.includes('database') && node.type === 'database' && title.includes(keyword)) {
|
||||
return true;
|
||||
}
|
||||
if (scopes.includes('tag') && node.type === 'tag' && title.includes(keyword)) {
|
||||
return true;
|
||||
}
|
||||
if (scopes.includes('host') && node.type === 'connection' && getConnectionHostSearchText(node).includes(keyword)) {
|
||||
return true;
|
||||
}
|
||||
if (scopes.includes('object') && (isV2SidebarObjectNode(node) || node.type === 'object-group') && title.includes(keyword)) {
|
||||
return true;
|
||||
}
|
||||
if (node.type === 'external-sql-root' || node.type === 'external-sql-directory' || node.type === 'external-sql-folder' || node.type === 'external-sql-file') {
|
||||
const pathText = String(node?.dataRef?.path || '').toLowerCase();
|
||||
return title.includes(keyword) || pathText.includes(keyword);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const loop = (data: TreeNode[], keyword: string): TreeNode[] => {
|
||||
const isSmartMode = searchScopes.includes('smart');
|
||||
const result: TreeNode[] = [];
|
||||
data.forEach((item) => {
|
||||
const titleMatch = String(item.title || '').toLowerCase().includes(keyword);
|
||||
const smartMatch = item.type === 'connection'
|
||||
? getConnectionNameSearchText(item).includes(keyword) || getConnectionHostSearchText(item).includes(keyword)
|
||||
: titleMatch;
|
||||
const scopedMatch = matchByScopes(item, keyword, searchScopes);
|
||||
const selfMatch = isSmartMode ? smartMatch : scopedMatch;
|
||||
const filteredChildren = item.children ? loop(item.children, keyword) : [];
|
||||
|
||||
if (selfMatch) {
|
||||
const shouldKeepFullSubtree = isSmartMode
|
||||
|| item.type === 'connection'
|
||||
|| item.type === 'database'
|
||||
|| item.type === 'tag'
|
||||
|| item.type === 'external-sql-root'
|
||||
|| item.type === 'external-sql-directory'
|
||||
|| item.type === 'external-sql-folder';
|
||||
if (item.children && shouldKeepFullSubtree) {
|
||||
result.push(item);
|
||||
} else if (item.children && filteredChildren.length > 0) {
|
||||
result.push({ ...item, children: filteredChildren });
|
||||
} else {
|
||||
result.push(item);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (filteredChildren.length > 0) {
|
||||
result.push({ ...item, children: filteredChildren });
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
const displayTreeData = useMemo(() => {
|
||||
const keyword = deferredSearchValue.trim().toLowerCase();
|
||||
if (!keyword) return treeData;
|
||||
return loop(treeData, keyword);
|
||||
}, [deferredSearchValue, searchScopes, treeData]);
|
||||
|
||||
const commandSearchTreeItems = useMemo(() => {
|
||||
const result: V2CommandSearchItem[] = [];
|
||||
const visit = (nodes: TreeNode[]) => {
|
||||
nodes.forEach((node) => {
|
||||
const dataRef = node.dataRef || {};
|
||||
if (node.type === 'connection') {
|
||||
const conn = dataRef as SavedConnection;
|
||||
result.push({
|
||||
key: `node-${node.key}`,
|
||||
kind: 'node',
|
||||
title: String(node.title || conn.name || t('connection.unnamed')),
|
||||
meta: resolveConnectionHostSummary(conn.config) || conn.config?.type || t('connection.sidebar.menu.section'),
|
||||
icon: getDbIcon(resolveConnectionIconType(conn), resolveConnectionAccentColor(conn), 16),
|
||||
node,
|
||||
});
|
||||
} else if (node.type === 'database') {
|
||||
const conn = connections.find((item) => item.id === dataRef.id);
|
||||
result.push({
|
||||
key: `node-${node.key}`,
|
||||
kind: 'node',
|
||||
title: String(node.title || dataRef.dbName || t('database.unnamed')),
|
||||
meta: conn?.name || dataRef.id || t('database.label'),
|
||||
icon: <DatabaseOutlined />,
|
||||
node,
|
||||
});
|
||||
} else if (
|
||||
node.type === 'table'
|
||||
|| node.type === 'view'
|
||||
|| node.type === 'materialized-view'
|
||||
|| node.type === 'db-trigger'
|
||||
|| node.type === 'db-event'
|
||||
|| node.type === 'routine'
|
||||
) {
|
||||
const conn = connections.find((item) => item.id === dataRef.id);
|
||||
const objectName = String(dataRef.tableName || dataRef.viewName || dataRef.triggerName || dataRef.eventName || dataRef.routineName || node.title || '').trim();
|
||||
const displayName = String(node.title || extractObjectName(objectName) || objectName).trim();
|
||||
result.push({
|
||||
key: `node-${node.key}`,
|
||||
kind: 'node',
|
||||
title: displayName,
|
||||
meta: [conn?.name || dataRef.id, dataRef.dbName].filter(Boolean).join(' · '),
|
||||
icon: node.type === 'table'
|
||||
? <TableOutlined />
|
||||
: (node.type === 'db-event' ? <ClockCircleOutlined /> : (node.type === 'routine' ? <CodeOutlined /> : <EyeOutlined />)),
|
||||
node,
|
||||
});
|
||||
}
|
||||
if (node.children) visit(node.children);
|
||||
});
|
||||
};
|
||||
|
||||
visit(treeData);
|
||||
return result;
|
||||
}, [connections, treeData]);
|
||||
|
||||
const commandSearchRecentItems = useMemo<V2CommandSearchItem[]>(() => {
|
||||
return sqlLogs.slice(0, 5).map((log) => ({
|
||||
key: `recent-${log.id}`,
|
||||
kind: 'recent',
|
||||
title: log.sql.replace(/\s+/g, ' ').trim() || 'SQL 记录',
|
||||
meta: `${new Date(log.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} · ${log.duration}ms${log.dbName ? ` · ${log.dbName}` : ''}`,
|
||||
icon: <ClockCircleOutlined />,
|
||||
sql: log.sql,
|
||||
dbName: log.dbName,
|
||||
}));
|
||||
}, [sqlLogs]);
|
||||
|
||||
const commandSearchActionItems = useMemo<V2CommandSearchItem[]>(() => [
|
||||
{
|
||||
key: 'action-new-query',
|
||||
kind: 'action',
|
||||
title: t('query.new'),
|
||||
meta: '打开一个新的 SQL 编辑页',
|
||||
shortcut: resolveShortcutDisplay(shortcutOptions, 'newQueryTab', activeShortcutPlatform),
|
||||
icon: <PlusOutlined />,
|
||||
onRun: () => window.dispatchEvent(new CustomEvent('gonavi:create-query-tab')),
|
||||
},
|
||||
{
|
||||
key: 'action-new-connection',
|
||||
kind: 'action',
|
||||
title: '新建数据源',
|
||||
meta: '创建数据库、运行时或其他数据源连接',
|
||||
shortcut: resolveShortcutDisplay(shortcutOptions, 'newConnection', activeShortcutPlatform),
|
||||
icon: <ThunderboltOutlined />,
|
||||
onRun: () => onCreateConnection?.(),
|
||||
},
|
||||
{
|
||||
key: 'action-open-ai',
|
||||
kind: 'action',
|
||||
title: '打开 AI 数据洞察',
|
||||
meta: '让 AI 分析当前数据库上下文',
|
||||
shortcut: resolveShortcutDisplay(shortcutOptions, 'toggleAIPanel', activeShortcutPlatform),
|
||||
icon: <RobotOutlined />,
|
||||
onRun: () => onToggleAI?.(),
|
||||
},
|
||||
{
|
||||
key: 'action-open-sql-log',
|
||||
kind: 'action',
|
||||
title: '查看 SQL 执行日志',
|
||||
meta: '打开最近执行记录面板',
|
||||
shortcut: resolveShortcutDisplay(shortcutOptions, 'toggleLogPanel', activeShortcutPlatform),
|
||||
icon: <BarsOutlined />,
|
||||
onRun: () => onToggleLogPanel?.(),
|
||||
},
|
||||
], [activeShortcutPlatform, onCreateConnection, onToggleAI, onToggleLogPanel, shortcutOptions]);
|
||||
|
||||
const v2CommandSearchQuery = useMemo(
|
||||
() => parseV2CommandSearchQuery(deferredV2CommandSearchValue),
|
||||
[deferredV2CommandSearchValue],
|
||||
);
|
||||
const normalizedV2CommandSearchValue = v2CommandSearchQuery.normalizedKeyword;
|
||||
const v2CommandSearchObjectMode = v2CommandSearchQuery.mode === 'object';
|
||||
const v2CommandSearchAiMode = v2CommandSearchQuery.mode === 'ai';
|
||||
const filteredCommandSearchTreeItems = useMemo(() => {
|
||||
return filterV2CommandSearchTreeItems(commandSearchTreeItems, v2CommandSearchQuery);
|
||||
}, [commandSearchTreeItems, v2CommandSearchQuery]);
|
||||
|
||||
const filteredCommandSearchActionItems = useMemo(() => {
|
||||
if (v2CommandSearchObjectMode || v2CommandSearchAiMode) return [];
|
||||
if (!normalizedV2CommandSearchValue) return commandSearchActionItems;
|
||||
return commandSearchActionItems.filter((item) => {
|
||||
const haystack = `${item.title} ${item.meta}`.toLowerCase();
|
||||
return haystack.includes(normalizedV2CommandSearchValue);
|
||||
});
|
||||
}, [commandSearchActionItems, normalizedV2CommandSearchValue, v2CommandSearchAiMode, v2CommandSearchObjectMode]);
|
||||
|
||||
const filteredCommandSearchRecentItems = useMemo(() => {
|
||||
if (v2CommandSearchObjectMode || v2CommandSearchAiMode) return [];
|
||||
if (!normalizedV2CommandSearchValue) return commandSearchRecentItems;
|
||||
return commandSearchRecentItems.filter((item) => {
|
||||
const haystack = `${item.title} ${item.meta}`.toLowerCase();
|
||||
return haystack.includes(normalizedV2CommandSearchValue);
|
||||
});
|
||||
}, [commandSearchRecentItems, normalizedV2CommandSearchValue, v2CommandSearchAiMode, v2CommandSearchObjectMode]);
|
||||
|
||||
const commandSearchAiItem = useMemo<V2CommandSearchItem[]>(() => {
|
||||
if (!v2CommandSearchAiMode || !v2CommandSearchQuery.aiPrompt) return [];
|
||||
return [{
|
||||
key: 'action-ask-ai',
|
||||
kind: 'action',
|
||||
title: '让 AI 回答',
|
||||
meta: v2CommandSearchQuery.aiPrompt,
|
||||
shortcut: '↵',
|
||||
icon: <RobotOutlined />,
|
||||
onRun: () => {
|
||||
const wasClosed = !useStore.getState().aiPanelVisible;
|
||||
if (wasClosed) setAIPanelVisible(true);
|
||||
window.setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent('gonavi:ai:inject-prompt', {
|
||||
detail: { prompt: v2CommandSearchQuery.aiPrompt },
|
||||
}));
|
||||
}, wasClosed ? 350 : 0);
|
||||
},
|
||||
}];
|
||||
}, [setAIPanelVisible, v2CommandSearchAiMode, v2CommandSearchQuery.aiPrompt]);
|
||||
|
||||
const commandSearchFlatItems = useMemo(
|
||||
() => [
|
||||
...commandSearchAiItem,
|
||||
...filteredCommandSearchTreeItems,
|
||||
...filteredCommandSearchActionItems,
|
||||
...filteredCommandSearchRecentItems,
|
||||
],
|
||||
[commandSearchAiItem, filteredCommandSearchActionItems, filteredCommandSearchRecentItems, filteredCommandSearchTreeItems],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setV2CommandActiveIndex(0);
|
||||
}, [setV2CommandActiveIndex, v2CommandSearchValue, commandSearchFlatItems.length]);
|
||||
|
||||
const flattenConnectionNodes = useCallback((nodes: TreeNode[]): TreeNode[] => {
|
||||
const result: TreeNode[] = [];
|
||||
nodes.forEach((node) => {
|
||||
if (node.type === 'connection') {
|
||||
result.push(node);
|
||||
}
|
||||
if (node.children) {
|
||||
result.push(...flattenConnectionNodes(node.children));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}, []);
|
||||
|
||||
const activeConnectionId = resolveV2ActiveConnectionId({
|
||||
activeContextConnectionId: activeContext?.connectionId,
|
||||
activeTabConnectionId: activeTab?.connectionId,
|
||||
selectedKeys,
|
||||
connectionIds,
|
||||
fallbackConnectionId: selectedNodesRef.current
|
||||
.map((node) => resolveSidebarNodeConnectionId(node, connectionIds))
|
||||
.find(Boolean),
|
||||
});
|
||||
const activeConnection = connections.find((conn) => conn.id === activeConnectionId) || null;
|
||||
const activeConnectionDisplayName = String(activeConnection?.name || '').trim() || t('sidebar.active_connection.no_host_selected');
|
||||
const activeDatabaseDisplayName = useMemo(() => {
|
||||
if (activeContext && typeof activeContext === 'object' && 'dbName' in activeContext) {
|
||||
return String(activeContext.dbName || '').trim();
|
||||
}
|
||||
return String(activeTab?.dbName || '').trim();
|
||||
}, [activeContext, activeTab?.dbName]);
|
||||
const activeConnectionTreeData = useMemo(() => {
|
||||
const externalSQLNodes = displayTreeData.filter((node) => node.type === 'external-sql-root');
|
||||
if (!activeConnection) return displayTreeData;
|
||||
const activeConnectionNode = displayTreeData.find((node) => node.type === 'connection' && node.key === activeConnection.id);
|
||||
if (activeConnectionNode) {
|
||||
return [
|
||||
...(activeConnectionNode.children && activeConnectionNode.children.length > 0 ? activeConnectionNode.children : []),
|
||||
...externalSQLNodes,
|
||||
];
|
||||
}
|
||||
const filterTree = (nodes: TreeNode[]): TreeNode[] => nodes.flatMap((node) => {
|
||||
if (node.type === 'tag') {
|
||||
return filterTree(node.children || []);
|
||||
}
|
||||
if (node.type === 'connection') {
|
||||
if (node.key !== activeConnection.id) return [];
|
||||
return node.children && node.children.length > 0 ? filterTree(node.children) : [];
|
||||
}
|
||||
return [{ ...node, children: node.children ? filterTree(node.children) : undefined }];
|
||||
});
|
||||
|
||||
const filtered = filterTree(displayTreeData);
|
||||
return [...filtered, ...externalSQLNodes];
|
||||
}, [activeConnection, displayTreeData]);
|
||||
const v2VisibleTreeData = useMemo(() => {
|
||||
if (v2ExplorerFilter === 'all') {
|
||||
return displayTreeData;
|
||||
}
|
||||
return filterV2ExplorerTreeByKind(activeConnectionTreeData, v2ExplorerFilter);
|
||||
}, [activeConnectionTreeData, displayTreeData, v2ExplorerFilter]);
|
||||
const v2TreeHorizontalScrollWidth = useMemo(
|
||||
() => estimateV2TreeHorizontalScrollWidth(v2VisibleTreeData, treeViewportWidth),
|
||||
[treeViewportWidth, v2VisibleTreeData],
|
||||
);
|
||||
const effectiveTreeHeight = isV2Ui && v2TreeHorizontalScrollWidth
|
||||
? Math.max(1, treeHeight - V2_TREE_HORIZONTAL_SCROLL_BOTTOM_RESERVE)
|
||||
: treeHeight;
|
||||
const v2TreeMetrics = useMemo(() => {
|
||||
const databaseTableCounts = new Map<React.Key, number>();
|
||||
const objectGroupCounts = new Map<React.Key, number>();
|
||||
let activeObjectCount = 0;
|
||||
|
||||
const visitAndCount = (node: TreeNode): number => {
|
||||
const childCount = (node.children || []).reduce((total, child) => total + visitAndCount(child), 0);
|
||||
const totalCount = (isV2SidebarObjectNode(node) ? 1 : 0) + childCount;
|
||||
if (node.type === 'database') {
|
||||
const tableCount = (node.children || []).reduce((total, child) => {
|
||||
if (child.type === 'object-group' && child?.dataRef?.groupKey === 'tables') {
|
||||
return total + (Array.isArray(child.children) ? child.children.filter((item) => item.type === 'table').length : 0);
|
||||
}
|
||||
if (child?.dataRef?.groupKey === 'schema' && Array.isArray(child.children)) {
|
||||
return total + child.children.reduce((schemaTotal, schemaChild) => {
|
||||
if (schemaChild.type === 'object-group' && schemaChild?.dataRef?.groupKey === 'tables') {
|
||||
return schemaTotal + (Array.isArray(schemaChild.children) ? schemaChild.children.filter((item) => item.type === 'table').length : 0);
|
||||
}
|
||||
return schemaTotal;
|
||||
}, 0);
|
||||
}
|
||||
return total;
|
||||
}, 0);
|
||||
databaseTableCounts.set(node.key, tableCount);
|
||||
} else if (node.type === 'object-group') {
|
||||
objectGroupCounts.set(node.key, childCount);
|
||||
}
|
||||
return totalCount;
|
||||
};
|
||||
|
||||
activeObjectCount = v2VisibleTreeData.reduce((total, node) => total + visitAndCount(node), 0);
|
||||
|
||||
return {
|
||||
activeObjectCount,
|
||||
databaseTableCounts,
|
||||
objectGroupCounts,
|
||||
};
|
||||
}, [v2VisibleTreeData]);
|
||||
|
||||
return {
|
||||
onSearch,
|
||||
toggleSearchScope,
|
||||
setSearchScopeChecked,
|
||||
searchScopeSummary,
|
||||
searchScopePopoverContent,
|
||||
displayTreeData,
|
||||
commandSearchTreeItems,
|
||||
commandSearchRecentItems,
|
||||
commandSearchActionItems,
|
||||
v2CommandSearchQuery,
|
||||
normalizedV2CommandSearchValue,
|
||||
v2CommandSearchObjectMode,
|
||||
v2CommandSearchAiMode,
|
||||
filteredCommandSearchTreeItems,
|
||||
filteredCommandSearchActionItems,
|
||||
filteredCommandSearchRecentItems,
|
||||
commandSearchAiItem,
|
||||
commandSearchFlatItems,
|
||||
flattenConnectionNodes,
|
||||
activeConnectionId,
|
||||
activeConnection,
|
||||
activeConnectionDisplayName,
|
||||
activeDatabaseDisplayName,
|
||||
activeConnectionTreeData,
|
||||
v2VisibleTreeData,
|
||||
v2TreeHorizontalScrollWidth,
|
||||
effectiveTreeHeight,
|
||||
v2TreeMetrics,
|
||||
activeConnectionObjectCount: v2TreeMetrics.activeObjectCount,
|
||||
};
|
||||
};
|
||||
168
frontend/src/components/sidebar/useSidebarTitleRender.tsx
Normal file
168
frontend/src/components/sidebar/useSidebarTitleRender.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import React, { useCallback, type Dispatch, type MutableRefObject, type SetStateAction } from 'react';
|
||||
import { Badge, Button } from 'antd';
|
||||
import { PlusOutlined } from '@ant-design/icons';
|
||||
|
||||
import type { SavedConnection } from '../../types';
|
||||
import { t } from '../../i18n';
|
||||
import JVMModeBadge from '../jvm/JVMModeBadge';
|
||||
import { SIDEBAR_SQL_EDITOR_DRAG_MIME, encodeSidebarSqlEditorDragPayload } from '../../utils/sidebarSqlDrag';
|
||||
import {
|
||||
resolveSidebarObjectDragText,
|
||||
} from '../sidebarCoreUtils';
|
||||
import {
|
||||
shouldHideSchemaPrefix,
|
||||
splitQualifiedName,
|
||||
} from './sidebarMetadataLoaders';
|
||||
import { resolveV2ObjectGroupTitle } from './sidebarHelpers';
|
||||
|
||||
type UseSidebarTitleRenderArgs = {
|
||||
connectionStates: Record<string, 'success' | 'error'>;
|
||||
isV2Ui: boolean;
|
||||
renderV2TreeTitle: (node: any, hoverTitle: string, statusBadge: React.ReactNode) => React.ReactNode;
|
||||
handleAddExternalSQLDirectory: (node: any) => Promise<void>;
|
||||
snapshotTreeSelectionBeforeDrag: () => void;
|
||||
restoreTreeSelectionAfterDrag: () => void;
|
||||
treeDragSelectSuppressUntilRef: MutableRefObject<number>;
|
||||
setIsTreeDragging: Dispatch<SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
export const useSidebarTitleRender = ({
|
||||
connectionStates,
|
||||
isV2Ui,
|
||||
renderV2TreeTitle,
|
||||
handleAddExternalSQLDirectory,
|
||||
snapshotTreeSelectionBeforeDrag,
|
||||
restoreTreeSelectionAfterDrag,
|
||||
treeDragSelectSuppressUntilRef,
|
||||
setIsTreeDragging,
|
||||
}: UseSidebarTitleRenderArgs) => useCallback((node: any) => {
|
||||
let status: 'success' | 'error' | 'default' = 'default';
|
||||
if (node.type === 'connection' || node.type === 'database') {
|
||||
if (connectionStates[node.key] === 'success') status = 'success';
|
||||
else if (connectionStates[node.key] === 'error') status = 'error';
|
||||
}
|
||||
|
||||
const statusBadge = node.type === 'connection' || node.type === 'database' ? (
|
||||
isV2Ui
|
||||
? <span className={`gn-v2-tree-status is-${status}`} aria-hidden="true" />
|
||||
: <Badge status={status} style={{ marginLeft: 4, marginRight: 8 }} />
|
||||
) : null;
|
||||
|
||||
const displayTitle = String(node.title ?? '');
|
||||
const dragText = resolveSidebarObjectDragText(node);
|
||||
let hoverTitle = displayTitle;
|
||||
if (node.type === 'table' || node.type === 'view' || node.type === 'materialized-view' || node.type === 'db-event') {
|
||||
const rawTableName = String(node?.dataRef?.tableName || node?.dataRef?.viewName || node?.dataRef?.eventName || '').trim();
|
||||
const conn = node?.dataRef as SavedConnection | undefined;
|
||||
if (rawTableName && shouldHideSchemaPrefix(conn)) {
|
||||
if (splitQualifiedName(rawTableName).schemaName) {
|
||||
hoverTitle = rawTableName;
|
||||
}
|
||||
}
|
||||
} else if (node.type === 'object-group') {
|
||||
const objectGroupTitle = resolveV2ObjectGroupTitle(node);
|
||||
if (objectGroupTitle) {
|
||||
hoverTitle = objectGroupTitle;
|
||||
}
|
||||
} else if (node.type === 'external-sql-directory' || node.type === 'external-sql-folder' || node.type === 'external-sql-file') {
|
||||
hoverTitle = String(node?.dataRef?.path || displayTitle);
|
||||
}
|
||||
|
||||
if (node.type === 'jvm-mode') {
|
||||
return (
|
||||
<span
|
||||
title={hoverTitle}
|
||||
style={{ display: 'inline-flex', alignItems: 'center', gap: 8, minWidth: 0 }}
|
||||
>
|
||||
<JVMModeBadge
|
||||
mode={String(node?.dataRef?.providerMode || displayTitle)}
|
||||
label={displayTitle}
|
||||
reason={String(node?.dataRef?.reason || '').trim() || undefined}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (node.type === 'external-sql-root') {
|
||||
const externalSqlRootTitle = t('sidebar.external_sql.root');
|
||||
const addSqlDirectoryLabel = t('sidebar.menu.add_sql_directory');
|
||||
return (
|
||||
<span
|
||||
title={externalSqlRootTitle}
|
||||
className="gn-v2-tree-external-root"
|
||||
>
|
||||
<span
|
||||
className="gn-v2-tree-title"
|
||||
data-node-type={node.type}
|
||||
data-sidebar-node-key={String(node.key || '')}
|
||||
data-sidebar-node-type={String(node.type || '')}
|
||||
>
|
||||
<span className="gn-v2-tree-label">
|
||||
{statusBadge}
|
||||
{externalSqlRootTitle}
|
||||
</span>
|
||||
</span>
|
||||
<Button
|
||||
size="small"
|
||||
type="text"
|
||||
icon={<PlusOutlined />}
|
||||
title={addSqlDirectoryLabel}
|
||||
aria-label={addSqlDirectoryLabel}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
void handleAddExternalSQLDirectory(node);
|
||||
}}
|
||||
className="gn-v2-tree-external-root-action"
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (isV2Ui) {
|
||||
return renderV2TreeTitle(node, hoverTitle, statusBadge);
|
||||
}
|
||||
|
||||
if (dragText) {
|
||||
return (
|
||||
<span
|
||||
title={hoverTitle}
|
||||
draggable
|
||||
onDragStart={(event) => {
|
||||
snapshotTreeSelectionBeforeDrag();
|
||||
treeDragSelectSuppressUntilRef.current = Date.now() + 600;
|
||||
setIsTreeDragging(true);
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
event.dataTransfer.setData('text/plain', dragText);
|
||||
event.dataTransfer.setData(
|
||||
SIDEBAR_SQL_EDITOR_DRAG_MIME,
|
||||
encodeSidebarSqlEditorDragPayload({
|
||||
text: dragText,
|
||||
nodeType: node.type,
|
||||
connectionId: String(node?.dataRef?.id || ''),
|
||||
dbName: String(node?.dataRef?.dbName || ''),
|
||||
}),
|
||||
);
|
||||
}}
|
||||
onDragEnd={() => {
|
||||
restoreTreeSelectionAfterDrag();
|
||||
setIsTreeDragging(false);
|
||||
}}
|
||||
>
|
||||
{statusBadge}{displayTitle}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return <span title={hoverTitle}>{statusBadge}{displayTitle}</span>;
|
||||
}, [
|
||||
connectionStates,
|
||||
handleAddExternalSQLDirectory,
|
||||
isV2Ui,
|
||||
renderV2TreeTitle,
|
||||
restoreTreeSelectionAfterDrag,
|
||||
setIsTreeDragging,
|
||||
snapshotTreeSelectionBeforeDrag,
|
||||
treeDragSelectSuppressUntilRef,
|
||||
]);
|
||||
538
frontend/src/components/sidebar/useSidebarV2ActionHandlers.tsx
Normal file
538
frontend/src/components/sidebar/useSidebarV2ActionHandlers.tsx
Normal file
@@ -0,0 +1,538 @@
|
||||
import type { Dispatch, MutableRefObject, SetStateAction } from 'react';
|
||||
import { message } from 'antd';
|
||||
import type { FormInstance } from 'antd/es/form';
|
||||
|
||||
import Modal from '../common/ResizableDraggableModal';
|
||||
import { t } from '../../i18n';
|
||||
import type { SavedConnection } from '../../types';
|
||||
import { buildRpcConnectionConfig } from '../../utils/connectionRpcConfig';
|
||||
import { resolveConnectionAccentColor, resolveConnectionIconType } from '../../utils/connectionVisual';
|
||||
import { buildTableSelectQuery } from '../../utils/objectQueryTemplates';
|
||||
import { DBReleaseConnection } from '../../../wailsjs/go/app/App';
|
||||
import { getDbIcon } from '../DatabaseIcons';
|
||||
import { getMetadataDialect } from './sidebarMetadataLoaders';
|
||||
import {
|
||||
type V2DatabaseContextMenuActionKey,
|
||||
type V2ConnectionGroupContextMenuActionKey,
|
||||
type V2ConnectionContextMenuActionKey,
|
||||
type V2TableContextMenuActionKey,
|
||||
type V2TableGroupContextMenuActionKey,
|
||||
} from '../V2TableContextMenu';
|
||||
import {
|
||||
isSidebarTablePinned,
|
||||
type SidebarTreeNode as TreeNode,
|
||||
type V2RailConnectionGroup,
|
||||
} from '../sidebarV2Utils';
|
||||
|
||||
type UseSidebarV2ActionHandlersArgs = {
|
||||
connections: SavedConnection[];
|
||||
connectionTags: Array<{ id: string; name: string; connectionIds: string[] }>;
|
||||
pinnedSidebarTables: any[];
|
||||
loadingNodesRef: MutableRefObject<Set<string>>;
|
||||
treeDataRef: MutableRefObject<TreeNode[]>;
|
||||
findTreeNodeByKeyRef: MutableRefObject<(nodes: TreeNode[], targetKey: React.Key) => TreeNode | null>;
|
||||
refreshV2TableContextMenuStatsRef: MutableRefObject<(node: any) => void>;
|
||||
setConnectionStates: Dispatch<SetStateAction<Record<string, 'success' | 'error'>>>;
|
||||
setExpandedKeys: Dispatch<SetStateAction<React.Key[]>>;
|
||||
setLoadedKeys: Dispatch<SetStateAction<React.Key[]>>;
|
||||
setTargetConnection: Dispatch<SetStateAction<any>>;
|
||||
setIsCreateDbModalOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setRenameDbTarget: Dispatch<SetStateAction<any>>;
|
||||
setIsRenameDbModalOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setRenameTableTarget: Dispatch<SetStateAction<any>>;
|
||||
setIsRenameTableModalOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setRenameViewTarget: Dispatch<SetStateAction<any>>;
|
||||
setIsCreateTagModalOpen: Dispatch<SetStateAction<boolean>>;
|
||||
renameDbForm: FormInstance;
|
||||
renameTableForm: FormInstance;
|
||||
createTagForm: FormInstance;
|
||||
addTab: (tab: any) => void;
|
||||
closeTabsByDatabase: (connectionId: string, dbName: string) => void;
|
||||
closeTabsByConnection: (connectionId: string) => void;
|
||||
removeConnection: (connectionId: string) => void;
|
||||
removeConnectionTag: (tagId: string) => void;
|
||||
moveConnectionToTag: (connectionId: string, tagId: string | null) => void;
|
||||
setSidebarTablePinned: (connectionId: string, dbName: string, tableName: string, schemaName: string, pinned: boolean) => void;
|
||||
setTableSortPreference: (connectionId: string, dbName: string, sortBy: 'name' | 'frequency') => void;
|
||||
replaceTreeNodeChildren: (key: React.Key, children: TreeNode[] | undefined) => void;
|
||||
loadDatabases: (node: any) => Promise<void>;
|
||||
loadTables: (node: any) => Promise<void>;
|
||||
getDatabaseNodeRef: (connRef: any, dbName: string) => any;
|
||||
extractObjectName: (fullName: string) => string;
|
||||
openDesign: (node: any, initialTab: string, readOnly?: boolean) => void;
|
||||
openNewTableDesign: (node: any) => void;
|
||||
onDoubleClick: (event: any, node: any) => void;
|
||||
openMessagePublishModal: (node: any) => void;
|
||||
openTableDdlInDesigner: (node: any) => void;
|
||||
openTableInERView: (node: any) => void;
|
||||
handleCopyTableName: (node: any) => Promise<void>;
|
||||
handleCopyStructure: (node: any) => Promise<void>;
|
||||
handleCopyTableAsInsert: (node: any) => Promise<void>;
|
||||
openCreateStarRocksRollup: (node: any) => void;
|
||||
handleExport: (node: any, options: { format: string; xlsxMaxRowsPerSheet?: number }) => Promise<void>;
|
||||
openExportDialog: (node: any) => Promise<void>;
|
||||
injectTablePromptToAI: (node: any, promptKind: 'explain' | 'query') => Promise<void>;
|
||||
handleTableDataDangerAction: (node: any, action: 'truncate' | 'clear') => Promise<void>;
|
||||
handleDeleteTable: (node: any) => void;
|
||||
openCreateSchemaModal: (node: any) => void;
|
||||
openCreateStarRocksMaterializedView: (node: any) => void;
|
||||
openCreateStarRocksExternalCatalog: (node: any) => void;
|
||||
handleExportDatabaseSQL: (node: any, includeData: boolean) => Promise<void>;
|
||||
handleRunSQLFile: (node: any) => void;
|
||||
handleDeleteDatabase: (node: any) => void;
|
||||
onEditConnection?: (conn: SavedConnection) => void;
|
||||
handleDuplicateConnection: (conn: SavedConnection) => Promise<void>;
|
||||
buildConnectionRootQueryTabTitle: () => string;
|
||||
buildConnectionRootRedisCommandTabTitle: (redisDbLabel?: string) => string;
|
||||
buildConnectionRootRedisMonitorTabTitle: (redisDbLabel?: string) => string;
|
||||
};
|
||||
|
||||
export const 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,
|
||||
}: UseSidebarV2ActionHandlersArgs) => {
|
||||
const handleV2TableContextMenuAction = (node: any, action: V2TableContextMenuActionKey) => {
|
||||
switch (action) {
|
||||
case 'pin-table':
|
||||
case 'unpin-table': {
|
||||
toggleSidebarTablePinned(node, action === 'pin-table');
|
||||
return;
|
||||
}
|
||||
case 'open-data':
|
||||
case 'open-new-tab':
|
||||
onDoubleClick(null, node);
|
||||
return;
|
||||
case 'design-table':
|
||||
openDesign(node, 'columns', false);
|
||||
return;
|
||||
case 'new-query': {
|
||||
const tableName = String(node.dataRef?.tableName || '').trim();
|
||||
const queryTemplate = buildTableSelectQuery(getMetadataDialect(node.dataRef as SavedConnection), tableName);
|
||||
addTab({
|
||||
id: `query-${Date.now()}`,
|
||||
title: t('query.new'),
|
||||
type: 'query',
|
||||
connectionId: node.dataRef.id,
|
||||
dbName: node.dataRef.dbName,
|
||||
query: queryTemplate,
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'publish-message':
|
||||
openMessagePublishModal(node);
|
||||
return;
|
||||
case 'view-ddl':
|
||||
openTableDdlInDesigner(node);
|
||||
return;
|
||||
case 'view-er':
|
||||
openTableInERView(node);
|
||||
return;
|
||||
case 'copy-table-name':
|
||||
void handleCopyTableName(node);
|
||||
return;
|
||||
case 'copy-structure':
|
||||
void handleCopyStructure(node);
|
||||
return;
|
||||
case 'copy-insert':
|
||||
void handleCopyTableAsInsert(node);
|
||||
return;
|
||||
case 'rename-table':
|
||||
setRenameTableTarget(node);
|
||||
renameTableForm.setFieldsValue({ newName: extractObjectName(node.dataRef?.tableName || node.title) });
|
||||
setIsRenameTableModalOpen(true);
|
||||
return;
|
||||
case 'new-rollup':
|
||||
openCreateStarRocksRollup(node);
|
||||
return;
|
||||
case 'backup-table':
|
||||
void handleExport(node, { format: 'sql' });
|
||||
return;
|
||||
case 'refresh-stats':
|
||||
refreshV2TableContextMenuStatsRef.current(node);
|
||||
return;
|
||||
case 'export-data':
|
||||
void openExportDialog(node);
|
||||
return;
|
||||
case 'ai-explain':
|
||||
void injectTablePromptToAI(node, 'explain');
|
||||
return;
|
||||
case 'ai-generate-query':
|
||||
void injectTablePromptToAI(node, 'query');
|
||||
return;
|
||||
case 'truncate-table':
|
||||
void handleTableDataDangerAction(node, 'truncate');
|
||||
return;
|
||||
case 'drop-table':
|
||||
handleDeleteTable(node);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSidebarTablePinned = (node: any, pinned?: boolean) => {
|
||||
const conn = node?.dataRef || {};
|
||||
const tableName = String(conn.tableName || node?.title || '').trim();
|
||||
const dbName = String(conn.dbName || '').trim();
|
||||
if (!conn.id || !dbName || !tableName) return;
|
||||
const currentlyPinned = isSidebarTablePinned(
|
||||
pinnedSidebarTables,
|
||||
String(conn.id || ''),
|
||||
dbName,
|
||||
tableName,
|
||||
String(conn.schemaName || ''),
|
||||
);
|
||||
const shouldPin = pinned ?? !currentlyPinned;
|
||||
setSidebarTablePinned(conn.id, dbName, tableName, conn.schemaName || '', shouldPin);
|
||||
void loadTables(getDatabaseNodeRef(conn, dbName));
|
||||
message.success(shouldPin ? t('sidebar.message.table_pinned') : t('sidebar.message.table_unpinned'));
|
||||
};
|
||||
|
||||
const handleTableGroupSortAction = (node: any, sortBy: 'name' | 'frequency') => {
|
||||
const groupData = node.dataRef;
|
||||
setTableSortPreference(groupData.id, groupData.dbName, sortBy);
|
||||
const dbNode = {
|
||||
key: `${groupData.id}-${groupData.dbName}`,
|
||||
dataRef: groupData,
|
||||
};
|
||||
loadTables(dbNode);
|
||||
};
|
||||
|
||||
const handleV2TableGroupContextMenuAction = (node: any, action: V2TableGroupContextMenuActionKey) => {
|
||||
switch (action) {
|
||||
case 'new-table':
|
||||
openNewTableDesign(node);
|
||||
return;
|
||||
case 'sort-by-name':
|
||||
handleTableGroupSortAction(node, 'name');
|
||||
return;
|
||||
case 'sort-by-frequency':
|
||||
handleTableGroupSortAction(node, 'frequency');
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const closeDatabaseNode = (node: any) => {
|
||||
const dbConnId = String(node.dataRef?.id || '');
|
||||
const dbName = String(node.dataRef?.dbName || node.title || '').trim();
|
||||
loadingNodesRef.current.delete(`tables-${dbConnId}-${dbName}`);
|
||||
setConnectionStates(prev => {
|
||||
const next = { ...prev };
|
||||
delete next[node.key];
|
||||
return next;
|
||||
});
|
||||
setExpandedKeys(prev => prev.filter(k => k !== node.key && !k.toString().startsWith(`${node.key}-`)));
|
||||
setLoadedKeys(prev => prev.filter(k => k !== node.key && !k.toString().startsWith(`${node.key}-`)));
|
||||
replaceTreeNodeChildren(node.key, undefined);
|
||||
if (dbConnId && dbName) {
|
||||
closeTabsByDatabase(dbConnId, dbName);
|
||||
}
|
||||
message.success(t('sidebar.message.database_closed'));
|
||||
};
|
||||
|
||||
const openDatabaseQuery = (node: any) => {
|
||||
addTab({
|
||||
id: `query-${Date.now()}`,
|
||||
title: t('sidebar.tab.new_query_database', { database: node.title }),
|
||||
type: 'query',
|
||||
connectionId: node.dataRef.id,
|
||||
dbName: node.title,
|
||||
query: '',
|
||||
});
|
||||
};
|
||||
|
||||
const handleV2DatabaseContextMenuAction = (node: any, action: V2DatabaseContextMenuActionKey) => {
|
||||
switch (action) {
|
||||
case 'new-table':
|
||||
openNewTableDesign(node);
|
||||
return;
|
||||
case 'new-schema':
|
||||
openCreateSchemaModal(node);
|
||||
return;
|
||||
case 'new-materialized-view':
|
||||
openCreateStarRocksMaterializedView(node);
|
||||
return;
|
||||
case 'new-external-catalog':
|
||||
openCreateStarRocksExternalCatalog(node);
|
||||
return;
|
||||
case 'rename-db':
|
||||
setRenameDbTarget(node);
|
||||
renameDbForm.setFieldsValue({ newName: node.dataRef?.dbName || '' });
|
||||
setIsRenameDbModalOpen(true);
|
||||
return;
|
||||
case 'refresh':
|
||||
loadTables(node);
|
||||
return;
|
||||
case 'export-db-schema':
|
||||
void handleExportDatabaseSQL(node, false);
|
||||
return;
|
||||
case 'backup-db-sql':
|
||||
void handleExportDatabaseSQL(node, true);
|
||||
return;
|
||||
case 'disconnect-db':
|
||||
closeDatabaseNode(node);
|
||||
return;
|
||||
case 'new-query':
|
||||
openDatabaseQuery(node);
|
||||
return;
|
||||
case 'run-sql':
|
||||
handleRunSQLFile(node);
|
||||
return;
|
||||
case 'drop-db':
|
||||
handleDeleteDatabase(node);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const refreshConnectionNode = (node: any) => {
|
||||
const connKey = String(node?.key || node?.dataRef?.id || '');
|
||||
if (!connKey) return;
|
||||
setExpandedKeys(prev => prev.filter(k => k !== connKey && !k.toString().startsWith(`${connKey}-`)));
|
||||
setLoadedKeys(prev => prev.filter(k => k !== connKey && !k.toString().startsWith(`${connKey}-`)));
|
||||
Array.from(loadingNodesRef.current).forEach((loadingKey) => {
|
||||
if (loadingKey === `dbs-${connKey}` || loadingKey.startsWith(`tables-${connKey}-`)) {
|
||||
loadingNodesRef.current.delete(loadingKey);
|
||||
}
|
||||
});
|
||||
loadDatabases(node);
|
||||
};
|
||||
|
||||
const releaseConnectionResources = async (conn: SavedConnection | undefined) => {
|
||||
if (!conn?.config) return;
|
||||
const res = await DBReleaseConnection(buildRpcConnectionConfig(conn.config, { id: conn.id }) as any);
|
||||
if (res && res.success === false) {
|
||||
throw new Error(res.message || '释放连接失败');
|
||||
}
|
||||
};
|
||||
|
||||
const disconnectConnectionNode = async (node: any) => {
|
||||
const connKey = String(node?.key || node?.dataRef?.id || '');
|
||||
if (!connKey) return;
|
||||
const conn = (connections.find((item) => item.id === connKey) || node?.dataRef) as SavedConnection | undefined;
|
||||
Array.from(loadingNodesRef.current).forEach((loadingKey) => {
|
||||
if (loadingKey === `dbs-${connKey}` || loadingKey.startsWith(`tables-${connKey}-`)) {
|
||||
loadingNodesRef.current.delete(loadingKey);
|
||||
}
|
||||
});
|
||||
setConnectionStates(prev => {
|
||||
const next = { ...prev };
|
||||
Object.keys(next).forEach(k => {
|
||||
if (k === connKey || k.startsWith(`${connKey}-`)) {
|
||||
delete next[k];
|
||||
}
|
||||
});
|
||||
return next;
|
||||
});
|
||||
setExpandedKeys(prev => prev.filter(k => k !== connKey && !k.toString().startsWith(`${connKey}-`)));
|
||||
setLoadedKeys(prev => prev.filter(k => k !== connKey && !k.toString().startsWith(`${connKey}-`)));
|
||||
replaceTreeNodeChildren(connKey, undefined);
|
||||
closeTabsByConnection(connKey);
|
||||
try {
|
||||
await releaseConnectionResources(conn);
|
||||
} catch (error: any) {
|
||||
message.warning(error?.message || '连接已从侧边栏断开,但后端连接释放失败');
|
||||
}
|
||||
message.success(t('connection.sidebar.disconnect.success'));
|
||||
};
|
||||
|
||||
const deleteConnectionNode = (node: any) => {
|
||||
Modal.confirm({
|
||||
title: t('connection.sidebar.delete.confirmTitle'),
|
||||
content: t('connection.sidebar.delete.confirmContent', { name: node.title }),
|
||||
onOk: async () => {
|
||||
const connId = String(node.key);
|
||||
const backendApp = (window as any).go?.app?.App;
|
||||
if (typeof backendApp?.DeleteConnection !== 'function') {
|
||||
message.error(t('connection.sidebar.delete.backendUnavailable'));
|
||||
throw new Error('DeleteConnection unavailable');
|
||||
}
|
||||
try {
|
||||
await backendApp.DeleteConnection(connId);
|
||||
closeTabsByConnection(connId);
|
||||
removeConnection(connId);
|
||||
message.success(t('connection.sidebar.delete.success'));
|
||||
} catch (error: any) {
|
||||
message.error(error?.message || t('connection.sidebar.delete.failureFallback'));
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const createConnectionTreeNode = (conn: SavedConnection): TreeNode => ({
|
||||
title: conn.name,
|
||||
key: conn.id,
|
||||
icon: getDbIcon(resolveConnectionIconType(conn), resolveConnectionAccentColor(conn), 22),
|
||||
type: 'connection',
|
||||
dataRef: conn,
|
||||
isLeaf: false,
|
||||
});
|
||||
|
||||
const getConnectionNodeForAction = (conn: SavedConnection): TreeNode => {
|
||||
return findTreeNodeByKeyRef.current(treeDataRef.current, conn.id) || createConnectionTreeNode(conn);
|
||||
};
|
||||
|
||||
const handleV2ConnectionContextMenuAction = (node: any, action: V2ConnectionContextMenuActionKey) => {
|
||||
const connId = String(node?.key || node?.dataRef?.id || '');
|
||||
if (!connId) return;
|
||||
switch (action) {
|
||||
case 'new-db':
|
||||
setTargetConnection(node);
|
||||
setIsCreateDbModalOpen(true);
|
||||
return;
|
||||
case 'refresh':
|
||||
refreshConnectionNode(node);
|
||||
return;
|
||||
case 'new-query':
|
||||
addTab({
|
||||
id: `query-${Date.now()}`,
|
||||
title: buildConnectionRootQueryTabTitle(),
|
||||
type: 'query',
|
||||
connectionId: connId,
|
||||
dbName: undefined,
|
||||
query: '',
|
||||
});
|
||||
return;
|
||||
case 'open-sql-file':
|
||||
handleRunSQLFile(node);
|
||||
return;
|
||||
case 'new-command':
|
||||
addTab({
|
||||
id: `redis-cmd-${connId}-${Date.now()}`,
|
||||
title: buildConnectionRootRedisCommandTabTitle(),
|
||||
type: 'redis-command',
|
||||
connectionId: connId,
|
||||
redisDB: 0,
|
||||
});
|
||||
return;
|
||||
case 'open-monitor':
|
||||
addTab({
|
||||
id: `redis-monitor-${connId}-${Date.now()}`,
|
||||
title: buildConnectionRootRedisMonitorTabTitle(),
|
||||
type: 'redis-monitor',
|
||||
connectionId: connId,
|
||||
redisDB: 0,
|
||||
});
|
||||
return;
|
||||
case 'edit':
|
||||
if (onEditConnection) onEditConnection(node.dataRef);
|
||||
return;
|
||||
case 'copy-connection':
|
||||
void handleDuplicateConnection(node.dataRef as SavedConnection);
|
||||
return;
|
||||
case 'disconnect':
|
||||
void disconnectConnectionNode(node);
|
||||
return;
|
||||
case 'delete':
|
||||
deleteConnectionNode(node);
|
||||
return;
|
||||
case 'move-to-ungrouped':
|
||||
moveConnectionToTag(connId, null);
|
||||
return;
|
||||
default:
|
||||
if (action.startsWith('move-to-tag:')) {
|
||||
moveConnectionToTag(connId, action.slice('move-to-tag:'.length));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleV2ConnectionGroupContextMenuAction = (group: V2RailConnectionGroup, action: V2ConnectionGroupContextMenuActionKey) => {
|
||||
const tag = connectionTags.find((item) => item.id === group.id);
|
||||
if (!tag) return;
|
||||
if (action === 'edit-group') {
|
||||
createTagForm.setFieldsValue({ name: tag.name, connectionIds: tag.connectionIds });
|
||||
setRenameViewTarget({
|
||||
title: tag.name,
|
||||
key: `tag-${tag.id}`,
|
||||
type: 'tag',
|
||||
dataRef: tag,
|
||||
});
|
||||
setIsCreateTagModalOpen(true);
|
||||
return;
|
||||
}
|
||||
if (action === 'delete-group') {
|
||||
Modal.confirm({
|
||||
title: t('connection.sidebar.group.deleteConfirmTitle'),
|
||||
content: t('connection.sidebar.group.deleteConfirmContent', { name: tag.name }),
|
||||
onOk: () => {
|
||||
removeConnectionTag(tag.id);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
getConnectionNodeForAction,
|
||||
toggleSidebarTablePinned,
|
||||
handleV2TableContextMenuAction,
|
||||
handleTableGroupSortAction,
|
||||
handleV2TableGroupContextMenuAction,
|
||||
handleV2DatabaseContextMenuAction,
|
||||
disconnectConnectionNode,
|
||||
deleteConnectionNode,
|
||||
handleV2ConnectionContextMenuAction,
|
||||
handleV2ConnectionGroupContextMenuAction,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user