mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-06-23 06:53:52 +08:00
♻️ refactor(sidebar): 抽出外部 SQL 文件流程
This commit is contained in:
@@ -67,6 +67,7 @@ const readSidebarSource = () => [
|
||||
readSourceFile('./sidebar/sidebarLegacyNodeMenu.tsx'),
|
||||
readSourceFile('./sidebar/sidebarMetadataLoaders.ts'),
|
||||
readSourceFile('./sidebar/useSidebarBatchExport.ts'),
|
||||
readSourceFile('./sidebar/SidebarExternalSqlWorkflow.tsx'),
|
||||
readSourceFile('./sidebarV2Utils.ts'),
|
||||
].join('\n');
|
||||
const readLegacyNodeMenuSource = () => readSourceFile('./sidebar/sidebarLegacyNodeMenu.tsx');
|
||||
@@ -2335,7 +2336,7 @@ describe('Sidebar locate toolbar', () => {
|
||||
const externalSqlReadEnd = source.indexOf('const externalSQLTrees = externalSQLDirectoryResults.reduce', externalSqlReadStart);
|
||||
const externalSqlReadSource = source.slice(externalSqlReadStart, externalSqlReadEnd);
|
||||
const externalSqlFlowStart = source.indexOf('const handleAddExternalSQLDirectory = async (node: any) => {');
|
||||
const externalSqlFlowEnd = source.indexOf('const handleCreateDatabase = async () => {', externalSqlFlowStart);
|
||||
const externalSqlFlowEnd = source.indexOf('const cancelSQLFileExecution = () => {', externalSqlFlowStart);
|
||||
const externalSqlFlowSource = source.slice(externalSqlFlowStart, externalSqlFlowEnd);
|
||||
const treeTitleStart = source.indexOf('const renderV2TreeTitle = (node: any, hoverTitle: string, statusBadge: React.ReactNode) => {');
|
||||
const treeTitleEnd = source.indexOf('const selectConnectionFromRail', treeTitleStart);
|
||||
|
||||
@@ -31,6 +31,19 @@ import {
|
||||
type BatchObjectFilterType,
|
||||
type BatchSelectionScope,
|
||||
} from './sidebar/useSidebarBatchExport';
|
||||
import {
|
||||
ExternalSQLFileModal,
|
||||
SQLFileExecutionModal,
|
||||
useSidebarExternalSqlWorkflow,
|
||||
} from './sidebar/SidebarExternalSqlWorkflow';
|
||||
export {
|
||||
buildSQLFileExecutionFooter,
|
||||
SQLFileExecutionProgressContent,
|
||||
} from './sidebar/SidebarExternalSqlWorkflow';
|
||||
export type {
|
||||
SQLFileExecutionProgressState,
|
||||
SQLFileExecutionStatus,
|
||||
} from './sidebar/SidebarExternalSqlWorkflow';
|
||||
import {
|
||||
V2_RAIL_UNGROUPED_CONNECTION_GROUP_ID,
|
||||
formatSidebarRowCount,
|
||||
@@ -61,7 +74,7 @@ export {
|
||||
} from './sidebar/sidebarHelpers';
|
||||
import React, { useEffect, useState, useMemo, useRef, useCallback, useDeferredValue } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Tree, message, Dropdown, MenuProps, Input, Button, Form, Badge, Checkbox, Space, Select, Popover, Tooltip, Progress, Switch } from 'antd';
|
||||
import { Tree, message, Dropdown, MenuProps, Input, Button, Form, Badge, Checkbox, Space, Select, Popover, Tooltip, Switch } from 'antd';
|
||||
import {
|
||||
DatabaseOutlined,
|
||||
TableOutlined,
|
||||
@@ -115,7 +128,7 @@ import {
|
||||
import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme';
|
||||
import { SavedConnection, SavedQuery, ExternalSQLDirectory, ExternalSQLTreeEntry, JVMCapability, JVMResourceSummary } from '../types';
|
||||
import { getDbIcon } from './DatabaseIcons';
|
||||
import { DBGetDatabases, DBGetTables, DBQuery, DBShowCreateTable, DBReleaseConnection, ExportTableWithOptions, OpenSQLFile, ExecuteSQLFile, CancelSQLFileExecution, CreateDatabase, CreateSchema, RenameDatabase, DropDatabase, RenameTable, DropTable, DropView, DropFunction, RenameView, SelectSQLDirectory, ListSQLDirectory, ReadSQLFile, CreateSQLFile, CreateSQLDirectory, DeleteSQLFile, DeleteSQLDirectory, RenameSQLFile, RenameSQLDirectory, JVMProbeCapabilities, GetDriverStatusList } from '../../wailsjs/go/app/App';
|
||||
import { DBGetDatabases, DBGetTables, DBQuery, DBShowCreateTable, DBReleaseConnection, ExportTableWithOptions, CreateDatabase, CreateSchema, RenameDatabase, DropDatabase, RenameTable, DropTable, DropView, DropFunction, RenameView, ListSQLDirectory, JVMProbeCapabilities, GetDriverStatusList } from '../../wailsjs/go/app/App';
|
||||
import { getTableDataDangerActionMeta, supportsTableTruncateAction, type TableDataDangerActionKind } from './tableDataDangerActions';
|
||||
import { EventsOn } from '../../wailsjs/runtime/runtime';
|
||||
import { isMacLikePlatform, normalizeOpacityForPlatform, resolveAppearanceValues } from '../utils/appearance';
|
||||
@@ -149,7 +162,7 @@ import {
|
||||
} from '../utils/tableExportTab';
|
||||
import { useExportProgressDialog } from './ExportProgressModal';
|
||||
import { getShortcutPlatform, resolveShortcutDisplay } from '../utils/shortcuts';
|
||||
import { buildExternalSQLDirectoryId, buildExternalSQLRootNode, buildExternalSQLTabId, type ExternalSQLTreeNode } from '../utils/externalSqlTree';
|
||||
import { buildExternalSQLRootNode, type ExternalSQLTreeNode } from '../utils/externalSqlTree';
|
||||
import { getCurrentLanguage, t } from '../i18n';
|
||||
import { SIDEBAR_SQL_EDITOR_DRAG_MIME, encodeSidebarSqlEditorDragPayload } from '../utils/sidebarSqlDrag';
|
||||
import { buildSqlServerObjectDefinitionQueries } from '../utils/sqlServerObjectDefinition';
|
||||
@@ -158,11 +171,9 @@ import MessagePublishModal from './MessagePublishModal';
|
||||
import {
|
||||
SIDEBAR_CONTEXT_MENU_FALLBACK_HEIGHT,
|
||||
SIDEBAR_CONTEXT_MENU_FALLBACK_WIDTH,
|
||||
isExternalSQLDirectoryModalMode,
|
||||
normalizeMySQLViewDDLForEditing,
|
||||
resolveSidebarContextMenuPosition,
|
||||
resolveSidebarObjectDragText,
|
||||
type ExternalSQLFileModalMode,
|
||||
type SearchScope,
|
||||
} from './sidebarCoreUtils';
|
||||
export { resolveSidebarContextMenuPosition } from './sidebarCoreUtils';
|
||||
@@ -254,97 +265,6 @@ const SIDEBAR_LOCATE_LOAD_WAIT_ATTEMPTS = 160;
|
||||
|
||||
// resolveV2ObjectGroupTitle 已迁移到 ./sidebar/sidebarHelpers
|
||||
|
||||
export type SQLFileExecutionStatus = 'running' | 'done' | 'cancelled' | 'error';
|
||||
|
||||
export type SQLFileExecutionProgressState = {
|
||||
fileSizeMB: string;
|
||||
status: SQLFileExecutionStatus;
|
||||
executed: number;
|
||||
failed: number;
|
||||
percent: number;
|
||||
currentSQL: string;
|
||||
resultMessage: string;
|
||||
};
|
||||
|
||||
const resolveSQLFileExecutionStatusLabel = (status: SQLFileExecutionStatus): string => {
|
||||
switch (status) {
|
||||
case 'done':
|
||||
return `✅ ${t('sidebar.sql_file_exec.status.done')}`;
|
||||
case 'cancelled':
|
||||
return `⚠️ ${t('sidebar.sql_file_exec.status.cancelled')}`;
|
||||
case 'error':
|
||||
return `❌ ${t('sidebar.sql_file_exec.status.error')}`;
|
||||
case 'running':
|
||||
default:
|
||||
return t('sidebar.sql_file_exec.status.running');
|
||||
}
|
||||
};
|
||||
|
||||
export const buildSQLFileExecutionFooter = ({
|
||||
status,
|
||||
onCancelExecution,
|
||||
onClose,
|
||||
}: {
|
||||
status: SQLFileExecutionStatus;
|
||||
onCancelExecution: () => void;
|
||||
onClose: () => void;
|
||||
}): React.ReactNode[] => {
|
||||
if (status === 'running') {
|
||||
return [
|
||||
<Button key="cancel" danger onClick={onCancelExecution}>
|
||||
{t('sidebar.sql_file_exec.cancel')}
|
||||
</Button>,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
<Button key="close" type="primary" onClick={onClose}>
|
||||
{t('sidebar.action.close')}
|
||||
</Button>,
|
||||
];
|
||||
};
|
||||
|
||||
export const SQLFileExecutionProgressContent: React.FC<SQLFileExecutionProgressState> = ({
|
||||
fileSizeMB,
|
||||
status,
|
||||
executed,
|
||||
failed,
|
||||
percent,
|
||||
currentSQL,
|
||||
resultMessage,
|
||||
}) => (
|
||||
<>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Progress
|
||||
percent={Math.round(percent)}
|
||||
status={status === 'error' ? 'exception' : status === 'done' ? 'success' : 'active'}
|
||||
strokeColor={status === 'cancelled' ? '#faad14' : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, lineHeight: '22px', marginBottom: 8 }}>
|
||||
<div>{t('sidebar.sql_file_exec.file_size')}<strong>{fileSizeMB} MB</strong></div>
|
||||
<div>{t('sidebar.sql_file_exec.status_label')}<strong>{resolveSQLFileExecutionStatusLabel(status)}</strong></div>
|
||||
<div>
|
||||
{t('sidebar.sql_file_exec.executed_label')}
|
||||
<strong style={{ color: '#52c41a' }}>{executed}</strong>
|
||||
{t('sidebar.sql_file_exec.rows_separator')}
|
||||
<strong style={{ color: failed > 0 ? '#ff4d4f' : undefined }}>{failed}</strong>
|
||||
{t('sidebar.sql_file_exec.rows_suffix')}
|
||||
</div>
|
||||
</div>
|
||||
{currentSQL && status === 'running' && (
|
||||
<div style={{ fontSize: 12, color: 'rgba(128,128,128,0.8)', background: 'rgba(128,128,128,0.06)', borderRadius: 6, padding: '6px 10px', marginTop: 8, fontFamily: 'var(--gn-font-mono)', wordBreak: 'break-all', maxHeight: 60, overflow: 'hidden' }}>
|
||||
{currentSQL}
|
||||
</div>
|
||||
)}
|
||||
{resultMessage && status !== 'running' && (
|
||||
<div style={{ fontSize: 12, marginTop: 12, maxHeight: 200, overflow: 'auto', whiteSpace: 'pre-wrap', background: 'rgba(128,128,128,0.06)', borderRadius: 6, padding: '8px 12px' }}>
|
||||
{resultMessage}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
// shouldLoadSidebarNodeOnExpand 已迁移到 ./sidebar/sidebarHelpers
|
||||
|
||||
// resolveSidebarTableNameForCopy 已迁移到 ./sidebar/sidebarHelpers
|
||||
@@ -946,10 +866,6 @@ const Sidebar: React.FC<{
|
||||
const [isRenameSavedQueryModalOpen, setIsRenameSavedQueryModalOpen] = useState(false);
|
||||
const [renameSavedQueryForm] = Form.useForm();
|
||||
const [renameSavedQueryTarget, setRenameSavedQueryTarget] = useState<SavedQuery | null>(null);
|
||||
const [isExternalSQLFileModalOpen, setIsExternalSQLFileModalOpen] = useState(false);
|
||||
const [externalSQLFileForm] = Form.useForm();
|
||||
const [externalSQLFileModalMode, setExternalSQLFileModalMode] = useState<ExternalSQLFileModalMode>('create');
|
||||
const [externalSQLFileTarget, setExternalSQLFileTarget] = useState<any>(null);
|
||||
// Connection Tag Modals
|
||||
const [isCreateTagModalOpen, setIsCreateTagModalOpen] = useState(false);
|
||||
const [createTagForm] = Form.useForm();
|
||||
@@ -1306,6 +1222,36 @@ const Sidebar: React.FC<{
|
||||
void refreshGlobalExternalSQLRootNode(false);
|
||||
}, [refreshGlobalExternalSQLRootNode]);
|
||||
|
||||
const {
|
||||
handleRunSQLFile,
|
||||
handleOpenSQLFileFromToolbar,
|
||||
openExternalSQLFile,
|
||||
openCreateExternalSQLFileModal,
|
||||
openRenameExternalSQLFileModal,
|
||||
openCreateExternalSQLDirectoryModal,
|
||||
openRenameExternalSQLDirectoryModal,
|
||||
handleDeleteExternalSQLFile,
|
||||
handleDeleteExternalSQLDirectory,
|
||||
handleAddExternalSQLDirectory,
|
||||
handleRemoveExternalSQLDirectory,
|
||||
handleRefreshExternalSQLDirectory,
|
||||
externalSQLFileModalProps,
|
||||
sqlFileExecutionModalProps,
|
||||
} = useSidebarExternalSqlWorkflow({
|
||||
connections,
|
||||
externalSQLDirectories,
|
||||
activeTab,
|
||||
connectionIds,
|
||||
selectedNodesRef,
|
||||
addTab,
|
||||
saveExternalSQLDirectory,
|
||||
deleteExternalSQLDirectory,
|
||||
refreshGlobalExternalSQLRootNode,
|
||||
setExpandedKeys,
|
||||
setAutoExpandParent,
|
||||
getActiveContext: () => useStore.getState().activeContext,
|
||||
});
|
||||
|
||||
const getNodeDatabaseContext = (node: any): { connectionId: string; dbName: string; dbNodeKey: string } | null => {
|
||||
if (!node) return null;
|
||||
if (node.type === 'database') {
|
||||
@@ -2649,130 +2595,6 @@ const Sidebar: React.FC<{
|
||||
}, wasClosed ? 350 : 0);
|
||||
};
|
||||
|
||||
const handleRunSQLFile = async (node: any) => {
|
||||
const res = await OpenSQLFile();
|
||||
if (res.success) {
|
||||
const data = normalizeSQLFileDialogData(res.data);
|
||||
// 大文件:后端返回文件路径,走流式执行
|
||||
if (data.isLargeFile) {
|
||||
const connId = node.type === 'connection' ? node.key : node.dataRef?.id;
|
||||
const dbName = node.dataRef?.dbName || '';
|
||||
const conn = connections.find(c => c.id === connId);
|
||||
if (!conn) {
|
||||
message.error(t('sidebar.message.connection_config_not_found'));
|
||||
return;
|
||||
}
|
||||
startSQLFileExecution(conn.config, dbName, data.filePath, data.fileSizeMB || '');
|
||||
return;
|
||||
}
|
||||
// 小文件:加载到编辑器
|
||||
const { dbName, id } = node.dataRef;
|
||||
const connectionId = node.type === 'connection' ? String(node.key) : String(id || node.dataRef.id || '');
|
||||
addTab({
|
||||
id: data.filePath ? buildExternalSQLTabId(connectionId, dbName || '', data.filePath) : `query-${Date.now()}`,
|
||||
title: data.fileName || t('sidebar.sql_file_exec.title'),
|
||||
type: 'query',
|
||||
connectionId,
|
||||
dbName: dbName,
|
||||
query: data.content,
|
||||
filePath: data.filePath || undefined,
|
||||
});
|
||||
} else if (res.message !== '已取消') {
|
||||
message.error(t('sidebar.message.read_file_failed', { error: res.message }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenSQLFileFromToolbar = async () => {
|
||||
const ctx = useStore.getState().activeContext;
|
||||
if (!ctx?.connectionId) {
|
||||
message.warning(t('sidebar.message.select_connection_or_database_first'));
|
||||
return;
|
||||
}
|
||||
const res = await OpenSQLFile();
|
||||
if (res.success) {
|
||||
const data = normalizeSQLFileDialogData(res.data);
|
||||
// 大文件:后端流式执行
|
||||
if (data.isLargeFile) {
|
||||
const conn = connections.find(c => c.id === ctx.connectionId);
|
||||
if (!conn) {
|
||||
message.error(t('sidebar.message.connection_config_not_found'));
|
||||
return;
|
||||
}
|
||||
startSQLFileExecution(conn.config, ctx.dbName || '', data.filePath, data.fileSizeMB || '');
|
||||
return;
|
||||
}
|
||||
// 小文件
|
||||
addTab({
|
||||
id: data.filePath ? buildExternalSQLTabId(ctx.connectionId, ctx.dbName || '', data.filePath) : `query-${Date.now()}`,
|
||||
title: data.fileName || t('sidebar.sql_file_exec.title'),
|
||||
type: 'query',
|
||||
connectionId: ctx.connectionId,
|
||||
dbName: ctx.dbName || undefined,
|
||||
query: data.content,
|
||||
filePath: data.filePath || undefined,
|
||||
});
|
||||
} else if (res.message !== '已取消') {
|
||||
message.error(t('sidebar.message.read_file_failed', { error: res.message }));
|
||||
}
|
||||
};
|
||||
|
||||
// SQL 文件流式执行状态
|
||||
const [sqlFileExecState, setSqlFileExecState] = useState<{
|
||||
open: boolean;
|
||||
jobId: string;
|
||||
fileSizeMB: string;
|
||||
status: SQLFileExecutionStatus;
|
||||
executed: number;
|
||||
failed: number;
|
||||
total: number;
|
||||
percent: number;
|
||||
currentSQL: string;
|
||||
resultMessage: string;
|
||||
}>({
|
||||
open: false, jobId: '', fileSizeMB: '', status: 'running',
|
||||
executed: 0, failed: 0, total: 0, percent: 0, currentSQL: '', resultMessage: ''
|
||||
});
|
||||
|
||||
const startSQLFileExecution = (config: any, dbName: string, filePath: string, fileSizeMB: string) => {
|
||||
const jobId = `sqlfile-${Date.now()}`;
|
||||
setSqlFileExecState({
|
||||
open: true, jobId, fileSizeMB, status: 'running',
|
||||
executed: 0, failed: 0, total: 0, percent: 0, currentSQL: '', resultMessage: ''
|
||||
});
|
||||
|
||||
// 监听进度事件
|
||||
const offProgress = EventsOn('sqlfile:progress', (event: any) => {
|
||||
if (!event || event.jobId !== jobId) return;
|
||||
setSqlFileExecState(prev => ({
|
||||
...prev,
|
||||
status: event.status || prev.status,
|
||||
executed: typeof event.executed === 'number' ? event.executed : prev.executed,
|
||||
failed: typeof event.failed === 'number' ? event.failed : prev.failed,
|
||||
total: typeof event.total === 'number' ? event.total : prev.total,
|
||||
percent: typeof event.percent === 'number' ? Math.min(100, event.percent) : prev.percent,
|
||||
currentSQL: typeof event.currentSQL === 'string' ? event.currentSQL : prev.currentSQL,
|
||||
}));
|
||||
});
|
||||
|
||||
// 异步执行
|
||||
ExecuteSQLFile(config, dbName, filePath, jobId).then(res => {
|
||||
offProgress();
|
||||
setSqlFileExecState(prev => ({
|
||||
...prev,
|
||||
status: res.success ? 'done' : (prev.status === 'cancelled' ? 'cancelled' : 'error'),
|
||||
percent: 100,
|
||||
resultMessage: res.message || '',
|
||||
}));
|
||||
}).catch(err => {
|
||||
offProgress();
|
||||
setSqlFileExecState(prev => ({
|
||||
...prev,
|
||||
status: 'error',
|
||||
resultMessage: String(err?.message || err),
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
const refreshDatabaseNode = async (dbNodeKey: string) => {
|
||||
if (!dbNodeKey) {
|
||||
return;
|
||||
@@ -2783,384 +2605,6 @@ const Sidebar: React.FC<{
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeExternalSQLFileName = (rawName: unknown): string => {
|
||||
const name = String(rawName || '').trim();
|
||||
if (!name) return '';
|
||||
return /\.sql$/i.test(name) ? name : `${name}.sql`;
|
||||
};
|
||||
|
||||
const normalizeExternalSQLDirectoryName = (rawName: unknown): string => {
|
||||
return String(rawName || '').trim();
|
||||
};
|
||||
|
||||
const getExternalSQLParentDirectoryPath = (node: any): string => {
|
||||
const path = String(node?.dataRef?.path || '').trim();
|
||||
if (node?.type === 'external-sql-directory' || node?.type === 'external-sql-folder') {
|
||||
return path;
|
||||
}
|
||||
if (node?.type === 'external-sql-file') {
|
||||
const index = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
|
||||
return index > 0 ? path.slice(0, index) : '';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const resolveExternalSQLExecutionContext = (): { connectionId: string; dbName: string } => {
|
||||
const activeStoreContext = useStore.getState().activeContext;
|
||||
const selectedConnectionId = selectedNodesRef.current
|
||||
.map((node) => resolveSidebarNodeConnectionId(node, connectionIds))
|
||||
.find(Boolean) || '';
|
||||
return {
|
||||
connectionId: String(
|
||||
activeStoreContext?.connectionId
|
||||
|| activeTab?.connectionId
|
||||
|| selectedConnectionId
|
||||
|| '',
|
||||
).trim(),
|
||||
dbName: String(
|
||||
activeStoreContext?.dbName
|
||||
|| activeTab?.dbName
|
||||
|| '',
|
||||
).trim(),
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeSQLFileDialogData = (data: unknown): { content: string; filePath: string; fileName: string; isLargeFile: boolean; fileSizeMB?: string } => {
|
||||
if (data && typeof data === 'object') {
|
||||
const payload = data as Record<string, unknown>;
|
||||
const filePath = String(payload.filePath || '').trim();
|
||||
return {
|
||||
content: String(payload.content ?? ''),
|
||||
filePath,
|
||||
fileName: String(payload.name || filePath.split(/[\\/]/).filter(Boolean).pop() || t('sidebar.sql_file_exec.title')).trim(),
|
||||
isLargeFile: payload.isLargeFile === true,
|
||||
fileSizeMB: String(payload.fileSizeMB || '').trim() || undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: String(data || ''),
|
||||
filePath: '',
|
||||
fileName: t('sidebar.sql_file_exec.title'),
|
||||
isLargeFile: false,
|
||||
};
|
||||
};
|
||||
|
||||
const openExternalSQLFile = async (fileNode: any) => {
|
||||
const fileContext = {
|
||||
connectionId: String(fileNode?.dataRef?.connectionId || '').trim(),
|
||||
dbName: String(fileNode?.dataRef?.dbName || '').trim(),
|
||||
};
|
||||
const fallbackContext = resolveExternalSQLExecutionContext();
|
||||
const connectionId = fileContext.connectionId || fallbackContext.connectionId;
|
||||
const dbName = fileContext.dbName || fallbackContext.dbName;
|
||||
const filePath = String(fileNode?.dataRef?.path || '').trim();
|
||||
const fileName = String(fileNode?.dataRef?.name || fileNode?.title || t('sidebar.sql_file.default_name')).trim() || t('sidebar.sql_file.default_name');
|
||||
if (!filePath) {
|
||||
message.error(t('sidebar.message.sql_file_path_incomplete'));
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await ReadSQLFile(filePath);
|
||||
if (!res.success) {
|
||||
if (res.message !== '已取消') {
|
||||
message.error(t('sidebar.message.read_sql_file_failed', { error: res.message }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const data = res.data;
|
||||
if (data && typeof data === 'object' && data.isLargeFile) {
|
||||
if (!connectionId) {
|
||||
message.warning(t('sidebar.message.select_host_before_large_sql_file'));
|
||||
return;
|
||||
}
|
||||
const conn = connections.find((item) => item.id === connectionId);
|
||||
if (!conn) {
|
||||
message.error(t('sidebar.message.connection_config_not_found'));
|
||||
return;
|
||||
}
|
||||
startSQLFileExecution(conn.config, dbName, data.filePath, data.fileSizeMB);
|
||||
return;
|
||||
}
|
||||
|
||||
addTab({
|
||||
id: buildExternalSQLTabId(connectionId, dbName, filePath),
|
||||
title: fileName,
|
||||
type: 'query',
|
||||
connectionId,
|
||||
dbName: dbName || undefined,
|
||||
query: String(data || ''),
|
||||
filePath,
|
||||
});
|
||||
};
|
||||
|
||||
const openCreateExternalSQLFileModal = (node: any) => {
|
||||
const directoryPath = getExternalSQLParentDirectoryPath(node);
|
||||
if (!directoryPath) {
|
||||
message.error(t('sidebar.message.external_sql_file_parent_missing'));
|
||||
return;
|
||||
}
|
||||
setExternalSQLFileModalMode('create');
|
||||
setExternalSQLFileTarget(node);
|
||||
externalSQLFileForm.setFieldsValue({ name: 'new-query.sql' });
|
||||
setIsExternalSQLFileModalOpen(true);
|
||||
};
|
||||
|
||||
const openRenameExternalSQLFileModal = (node: any) => {
|
||||
const currentName = String(node?.dataRef?.name || node?.title || '').trim();
|
||||
if (!currentName) {
|
||||
message.error(t('sidebar.message.external_sql_file_rename_target_missing'));
|
||||
return;
|
||||
}
|
||||
setExternalSQLFileModalMode('rename');
|
||||
setExternalSQLFileTarget(node);
|
||||
externalSQLFileForm.setFieldsValue({ name: currentName });
|
||||
setIsExternalSQLFileModalOpen(true);
|
||||
};
|
||||
|
||||
const openCreateExternalSQLDirectoryModal = (node: any) => {
|
||||
const directoryPath = getExternalSQLParentDirectoryPath(node);
|
||||
if (!directoryPath) {
|
||||
message.error(t('sidebar.message.external_sql_directory_parent_missing'));
|
||||
return;
|
||||
}
|
||||
setExternalSQLFileModalMode('create-directory');
|
||||
setExternalSQLFileTarget(node);
|
||||
externalSQLFileForm.setFieldsValue({ name: 'new-folder' });
|
||||
setIsExternalSQLFileModalOpen(true);
|
||||
};
|
||||
|
||||
const openRenameExternalSQLDirectoryModal = (node: any) => {
|
||||
const currentName = String(node?.dataRef?.name || node?.title || '').trim();
|
||||
if (!currentName) {
|
||||
message.error(t('sidebar.message.external_sql_directory_rename_target_missing'));
|
||||
return;
|
||||
}
|
||||
setExternalSQLFileModalMode('rename-directory');
|
||||
setExternalSQLFileTarget(node);
|
||||
externalSQLFileForm.setFieldsValue({ name: currentName });
|
||||
setIsExternalSQLFileModalOpen(true);
|
||||
};
|
||||
|
||||
const handleExternalSQLFileModalOk = async () => {
|
||||
try {
|
||||
const values = await externalSQLFileForm.validateFields();
|
||||
const isDirectoryMode = isExternalSQLDirectoryModalMode(externalSQLFileModalMode);
|
||||
const name = isDirectoryMode
|
||||
? normalizeExternalSQLDirectoryName(values.name)
|
||||
: normalizeExternalSQLFileName(values.name);
|
||||
if (!name) {
|
||||
message.error(t(isDirectoryMode ? 'sidebar.message.sql_directory_name_required' : 'sidebar.message.sql_file_name_required'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (externalSQLFileModalMode === 'create') {
|
||||
const directoryPath = getExternalSQLParentDirectoryPath(externalSQLFileTarget);
|
||||
if (!directoryPath) {
|
||||
message.error(t('sidebar.message.external_sql_file_parent_missing'));
|
||||
return;
|
||||
}
|
||||
const res = await CreateSQLFile(directoryPath, name);
|
||||
if (!res.success) {
|
||||
message.error(t('sidebar.message.create_sql_file_failed', { error: res.message }));
|
||||
return;
|
||||
}
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
message.success(t('sidebar.message.sql_file_created'));
|
||||
} else if (externalSQLFileModalMode === 'rename') {
|
||||
const filePath = String(externalSQLFileTarget?.dataRef?.path || '').trim();
|
||||
if (!filePath) {
|
||||
message.error(t('sidebar.message.external_sql_file_rename_target_missing'));
|
||||
return;
|
||||
}
|
||||
const res = await RenameSQLFile(filePath, name);
|
||||
if (!res.success) {
|
||||
message.error(t('sidebar.message.rename_sql_file_failed', { error: res.message }));
|
||||
return;
|
||||
}
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
message.success(t('sidebar.message.sql_file_renamed'));
|
||||
} else if (externalSQLFileModalMode === 'create-directory') {
|
||||
const directoryPath = getExternalSQLParentDirectoryPath(externalSQLFileTarget);
|
||||
if (!directoryPath) {
|
||||
message.error(t('sidebar.message.external_sql_directory_parent_missing'));
|
||||
return;
|
||||
}
|
||||
const res = await CreateSQLDirectory(directoryPath, name);
|
||||
if (!res.success) {
|
||||
message.error(t('sidebar.message.create_sql_directory_failed', { error: res.message }));
|
||||
return;
|
||||
}
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
message.success(t('sidebar.message.sql_directory_created'));
|
||||
} else {
|
||||
const directoryPath = String(externalSQLFileTarget?.dataRef?.path || '').trim();
|
||||
if (!directoryPath) {
|
||||
message.error(t('sidebar.message.external_sql_directory_rename_target_missing'));
|
||||
return;
|
||||
}
|
||||
const res = await RenameSQLDirectory(directoryPath, name);
|
||||
if (!res.success) {
|
||||
message.error(t('sidebar.message.rename_sql_directory_failed', { error: res.message }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (externalSQLFileTarget?.type === 'external-sql-directory') {
|
||||
const payload = (res.data && typeof res.data === 'object') ? res.data as Record<string, unknown> : {};
|
||||
const nextPath = String(payload.directoryPath || payload.path || '').trim();
|
||||
const nextName = String(payload.name || name).trim();
|
||||
const oldDirectoryId = String(externalSQLFileTarget?.dataRef?.id || '').trim();
|
||||
if (!nextPath || !oldDirectoryId) {
|
||||
message.error(t('sidebar.message.external_sql_directory_rename_sync_failed'));
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
return;
|
||||
}
|
||||
const nextDirectory: ExternalSQLDirectory = {
|
||||
id: buildExternalSQLDirectoryId('', '', nextPath),
|
||||
name: nextName || nextPath.split(/[\\/]/).filter(Boolean).pop() || t('sidebar.sql_directory.default_name'),
|
||||
path: nextPath,
|
||||
createdAt: Number(externalSQLFileTarget?.dataRef?.createdAt) || Date.now(),
|
||||
};
|
||||
deleteExternalSQLDirectory(oldDirectoryId);
|
||||
saveExternalSQLDirectory(nextDirectory);
|
||||
const nextDirectories = [
|
||||
...externalSQLDirectories.filter((item) => item.id !== oldDirectoryId),
|
||||
nextDirectory,
|
||||
];
|
||||
await refreshGlobalExternalSQLRootNode(false, nextDirectories);
|
||||
} else {
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
}
|
||||
message.success(t('sidebar.message.sql_directory_renamed'));
|
||||
}
|
||||
|
||||
setIsExternalSQLFileModalOpen(false);
|
||||
setExternalSQLFileTarget(null);
|
||||
externalSQLFileForm.resetFields();
|
||||
} catch {
|
||||
// Validate failed
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteExternalSQLFile = (node: any) => {
|
||||
const filePath = String(node?.dataRef?.path || '').trim();
|
||||
const fileName = String(node?.dataRef?.name || node?.title || t('sidebar.sql_file.default_name')).trim();
|
||||
if (!filePath) {
|
||||
message.error(t('sidebar.message.external_sql_file_delete_target_missing'));
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: t('sidebar.modal.confirm_delete_sql_file.title'),
|
||||
content: t('sidebar.modal.confirm_delete_sql_file.content', { name: fileName }),
|
||||
okText: t('sidebar.action.delete'),
|
||||
cancelText: t('sidebar.action.cancel'),
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
const res = await DeleteSQLFile(filePath);
|
||||
if (!res.success) {
|
||||
message.error(t('sidebar.message.delete_sql_file_failed', { error: res.message }));
|
||||
return;
|
||||
}
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
message.success(t('sidebar.message.sql_file_deleted'));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteExternalSQLDirectory = (node: any) => {
|
||||
const directoryPath = String(node?.dataRef?.path || '').trim();
|
||||
const directoryName = String(node?.dataRef?.name || node?.title || t('sidebar.sql_directory.default_name')).trim();
|
||||
if (!directoryPath) {
|
||||
message.error(t('sidebar.message.external_sql_directory_delete_target_missing'));
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: t('sidebar.modal.confirm_delete_sql_directory.title'),
|
||||
content: t('sidebar.modal.confirm_delete_sql_directory.content', { name: directoryName }),
|
||||
okText: t('sidebar.action.delete'),
|
||||
cancelText: t('sidebar.action.cancel'),
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
const res = await DeleteSQLDirectory(directoryPath);
|
||||
if (!res.success) {
|
||||
message.error(t('sidebar.message.delete_sql_directory_failed', { error: res.message }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (node?.type === 'external-sql-directory') {
|
||||
const directoryId = String(node?.dataRef?.id || '').trim();
|
||||
if (directoryId) {
|
||||
deleteExternalSQLDirectory(directoryId);
|
||||
const nextDirectories = externalSQLDirectories.filter((item) => item.id !== directoryId);
|
||||
await refreshGlobalExternalSQLRootNode(false, nextDirectories);
|
||||
} else {
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
}
|
||||
} else {
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
}
|
||||
message.success(t('sidebar.message.sql_directory_deleted'));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddExternalSQLDirectory = async (node: any) => {
|
||||
const currentDirectory = externalSQLDirectories[0]?.path || '';
|
||||
const selection = await SelectSQLDirectory(currentDirectory);
|
||||
if (!selection.success) {
|
||||
if (selection.message !== '已取消') {
|
||||
message.error(t('sidebar.message.select_sql_directory_failed', { error: selection.message }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = (selection.data && typeof selection.data === 'object') ? selection.data as Record<string, unknown> : {};
|
||||
const path = String(payload.path || '').trim();
|
||||
const name = String(payload.name || '').trim();
|
||||
if (!path) {
|
||||
message.error(t('sidebar.message.sql_directory_path_invalid'));
|
||||
return;
|
||||
}
|
||||
|
||||
const directoryId = buildExternalSQLDirectoryId('', '', path);
|
||||
const nextDirectory: ExternalSQLDirectory = {
|
||||
id: directoryId,
|
||||
name: name || path.split(/[\\/]/).filter(Boolean).pop() || t('sidebar.sql_directory.default_name'),
|
||||
path,
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
saveExternalSQLDirectory(nextDirectory);
|
||||
|
||||
const nextDirectories = [
|
||||
...externalSQLDirectories.filter((item) => item.path.replace(/\\/g, '/').toLowerCase() !== path.replace(/\\/g, '/').toLowerCase()),
|
||||
nextDirectory,
|
||||
];
|
||||
setExpandedKeys((prev) => Array.from(new Set([...prev, 'external-sql-root'])));
|
||||
setAutoExpandParent(false);
|
||||
await refreshGlobalExternalSQLRootNode(false, nextDirectories);
|
||||
message.success(t('sidebar.message.external_sql_directory_added'));
|
||||
};
|
||||
|
||||
const handleRemoveExternalSQLDirectory = async (node: any) => {
|
||||
const directoryId = String(node?.dataRef?.id || '').trim();
|
||||
if (!directoryId) {
|
||||
message.error(t('sidebar.message.external_sql_directory_not_found'));
|
||||
return;
|
||||
}
|
||||
deleteExternalSQLDirectory(directoryId);
|
||||
const nextDirectories = externalSQLDirectories.filter((item) => item.id !== directoryId);
|
||||
await refreshGlobalExternalSQLRootNode(false, nextDirectories);
|
||||
message.success(t('sidebar.message.external_sql_directory_removed'));
|
||||
};
|
||||
|
||||
const handleRefreshExternalSQLDirectory = async (node: any) => {
|
||||
void node;
|
||||
await refreshGlobalExternalSQLRootNode(true);
|
||||
message.success(t('sidebar.message.external_sql_directory_refreshed'));
|
||||
};
|
||||
|
||||
const handleCreateDatabase = async () => {
|
||||
try {
|
||||
const values = await createDbForm.validateFields();
|
||||
@@ -6733,48 +6177,7 @@ const Sidebar: React.FC<{
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={
|
||||
externalSQLFileModalMode === 'create'
|
||||
? t('sidebar.external_sql_modal.title.create_file')
|
||||
: externalSQLFileModalMode === 'rename'
|
||||
? t('sidebar.external_sql_modal.title.rename_file')
|
||||
: externalSQLFileModalMode === 'create-directory'
|
||||
? t('sidebar.external_sql_modal.title.create_directory')
|
||||
: t('sidebar.external_sql_modal.title.rename_directory')
|
||||
}
|
||||
open={isExternalSQLFileModalOpen}
|
||||
onOk={handleExternalSQLFileModalOk}
|
||||
onCancel={() => {
|
||||
setIsExternalSQLFileModalOpen(false);
|
||||
setExternalSQLFileTarget(null);
|
||||
externalSQLFileForm.resetFields();
|
||||
}}
|
||||
okText={t(externalSQLFileModalMode === 'create' || externalSQLFileModalMode === 'create-directory' ? 'sidebar.external_sql_modal.action.create' : 'sidebar.external_sql_modal.action.rename')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
<Form form={externalSQLFileForm} layout="vertical">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={isExternalSQLDirectoryModalMode(externalSQLFileModalMode) ? t('sidebar.external_sql_modal.field.directory_name') : t('sidebar.external_sql_modal.field.sql_file_name')}
|
||||
rules={[
|
||||
{ required: true, message: isExternalSQLDirectoryModalMode(externalSQLFileModalMode) ? t('sidebar.external_sql_modal.validation.directory_name_required') : t('sidebar.external_sql_modal.validation.sql_file_name_required') },
|
||||
{
|
||||
validator: async (_, value) => {
|
||||
const name = String(value || '').trim();
|
||||
if (!name) return;
|
||||
if (/[\\/]/.test(name) || name === '.' || name === '..') {
|
||||
throw new Error(isExternalSQLDirectoryModalMode(externalSQLFileModalMode) ? t('sidebar.external_sql_modal.validation.directory_name_no_separator') : t('sidebar.external_sql_modal.validation.sql_file_name_no_separator'));
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
extra={isExternalSQLDirectoryModalMode(externalSQLFileModalMode) ? t('sidebar.external_sql_modal.help.directory') : t('sidebar.external_sql_modal.help.sql_file')}
|
||||
>
|
||||
<Input {...noAutoCapInputProps} placeholder={isExternalSQLDirectoryModalMode(externalSQLFileModalMode) ? t('sidebar.external_sql_modal.placeholder.directory_name') : t('sidebar.external_sql_modal.placeholder.sql_file_name')} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
<ExternalSQLFileModal {...externalSQLFileModalProps} />
|
||||
|
||||
<Modal
|
||||
title={renderSidebarModalTitle(<TableOutlined />, "批量操作表", "按对象批量导出结构、数据或完整备份。")}
|
||||
@@ -7072,38 +6475,11 @@ const Sidebar: React.FC<{
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* SQL 文件流式执行进度 Modal */}
|
||||
<Modal
|
||||
<SQLFileExecutionModal
|
||||
title={v2OpenExternalSqlFileLabel}
|
||||
open={sqlFileExecState.open}
|
||||
centered
|
||||
closable={sqlFileExecState.status !== 'running'}
|
||||
maskClosable={false}
|
||||
footer={buildSQLFileExecutionFooter({
|
||||
status: sqlFileExecState.status,
|
||||
onCancelExecution: () => {
|
||||
CancelSQLFileExecution(sqlFileExecState.jobId);
|
||||
setSqlFileExecState(prev => ({ ...prev, status: 'cancelled' }));
|
||||
},
|
||||
onClose: () => setSqlFileExecState(prev => ({ ...prev, open: false })),
|
||||
})}
|
||||
onCancel={() => {
|
||||
if (sqlFileExecState.status !== 'running') {
|
||||
setSqlFileExecState(prev => ({ ...prev, open: false }));
|
||||
}
|
||||
}}
|
||||
styles={{ content: modalPanelStyle, header: { background: 'transparent', borderBottom: 'none' }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none' } }}
|
||||
>
|
||||
<SQLFileExecutionProgressContent
|
||||
fileSizeMB={sqlFileExecState.fileSizeMB}
|
||||
status={sqlFileExecState.status}
|
||||
executed={sqlFileExecState.executed}
|
||||
failed={sqlFileExecState.failed}
|
||||
percent={sqlFileExecState.percent}
|
||||
currentSQL={sqlFileExecState.currentSQL}
|
||||
resultMessage={sqlFileExecState.resultMessage}
|
||||
/>
|
||||
</Modal>
|
||||
modalPanelStyle={modalPanelStyle}
|
||||
{...sqlFileExecutionModalProps}
|
||||
/>
|
||||
<FindInDatabaseModal
|
||||
open={findInDbContext.open}
|
||||
onClose={() => setFindInDbContext({ open: false, connectionId: '', dbName: '' })}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
const source = readFileSync(new URL('./Sidebar.tsx', import.meta.url), 'utf8');
|
||||
const source = readFileSync(new URL('./sidebar/SidebarExternalSqlWorkflow.tsx', import.meta.url), 'utf8');
|
||||
|
||||
const externalSqlOpenBlock = source.slice(
|
||||
source.indexOf('const normalizeSQLFileDialogData ='),
|
||||
|
||||
817
frontend/src/components/sidebar/SidebarExternalSqlWorkflow.tsx
Normal file
817
frontend/src/components/sidebar/SidebarExternalSqlWorkflow.tsx
Normal file
@@ -0,0 +1,817 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button, Form, Input, Progress, message } from 'antd';
|
||||
import type { FormInstance } from 'antd/es/form';
|
||||
import Modal from '../common/ResizableDraggableModal';
|
||||
import type { SavedConnection, ExternalSQLDirectory } from '../../types';
|
||||
import { noAutoCapInputProps } from '../../utils/inputAutoCap';
|
||||
import { buildExternalSQLDirectoryId, buildExternalSQLTabId } from '../../utils/externalSqlTree';
|
||||
import { t } from '../../i18n';
|
||||
import { resolveSidebarNodeConnectionId } from '../sidebarV2Utils';
|
||||
import {
|
||||
isExternalSQLDirectoryModalMode,
|
||||
type ExternalSQLFileModalMode,
|
||||
} from '../sidebarCoreUtils';
|
||||
import {
|
||||
OpenSQLFile,
|
||||
ExecuteSQLFile,
|
||||
CancelSQLFileExecution,
|
||||
SelectSQLDirectory,
|
||||
ReadSQLFile,
|
||||
CreateSQLFile,
|
||||
CreateSQLDirectory,
|
||||
DeleteSQLFile,
|
||||
DeleteSQLDirectory,
|
||||
RenameSQLFile,
|
||||
RenameSQLDirectory,
|
||||
} from '../../../wailsjs/go/app/App';
|
||||
import { EventsOn } from '../../../wailsjs/runtime/runtime';
|
||||
|
||||
export type SQLFileExecutionStatus = 'running' | 'done' | 'cancelled' | 'error';
|
||||
|
||||
export type SQLFileExecutionProgressState = {
|
||||
fileSizeMB: string;
|
||||
status: SQLFileExecutionStatus;
|
||||
executed: number;
|
||||
failed: number;
|
||||
percent: number;
|
||||
currentSQL: string;
|
||||
resultMessage: string;
|
||||
};
|
||||
|
||||
type SQLFileExecutionState = SQLFileExecutionProgressState & {
|
||||
open: boolean;
|
||||
jobId: string;
|
||||
total: number;
|
||||
};
|
||||
|
||||
type ActiveExecutionContext = {
|
||||
connectionId?: string;
|
||||
dbName?: string;
|
||||
} | null | undefined;
|
||||
|
||||
type RefreshExternalSQLRootNode = (
|
||||
showLoading?: boolean,
|
||||
directoriesOverride?: ExternalSQLDirectory[],
|
||||
) => Promise<void>;
|
||||
|
||||
type UseSidebarExternalSqlWorkflowOptions = {
|
||||
connections: SavedConnection[];
|
||||
externalSQLDirectories: ExternalSQLDirectory[];
|
||||
activeTab: {
|
||||
connectionId?: string;
|
||||
dbName?: string;
|
||||
} | null;
|
||||
connectionIds: string[];
|
||||
selectedNodesRef: React.MutableRefObject<any[]>;
|
||||
addTab: (tab: any) => void;
|
||||
saveExternalSQLDirectory: (directory: ExternalSQLDirectory) => void;
|
||||
deleteExternalSQLDirectory: (directoryId: string) => void;
|
||||
refreshGlobalExternalSQLRootNode: RefreshExternalSQLRootNode;
|
||||
setExpandedKeys: React.Dispatch<React.SetStateAction<React.Key[]>>;
|
||||
setAutoExpandParent: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
getActiveContext: () => ActiveExecutionContext;
|
||||
};
|
||||
|
||||
type ExternalSQLFileModalProps = {
|
||||
open: boolean;
|
||||
mode: ExternalSQLFileModalMode;
|
||||
form: FormInstance;
|
||||
onOk: () => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
|
||||
type SQLFileExecutionModalProps = {
|
||||
title: React.ReactNode;
|
||||
state: SQLFileExecutionState;
|
||||
modalPanelStyle: React.CSSProperties;
|
||||
onCancelExecution: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
const createInitialSQLFileExecutionState = (): SQLFileExecutionState => ({
|
||||
open: false,
|
||||
jobId: '',
|
||||
fileSizeMB: '',
|
||||
status: 'running',
|
||||
executed: 0,
|
||||
failed: 0,
|
||||
total: 0,
|
||||
percent: 0,
|
||||
currentSQL: '',
|
||||
resultMessage: '',
|
||||
});
|
||||
|
||||
const normalizeExternalSQLFileName = (rawName: unknown): string => {
|
||||
const name = String(rawName || '').trim();
|
||||
if (!name) return '';
|
||||
return /\.sql$/i.test(name) ? name : `${name}.sql`;
|
||||
};
|
||||
|
||||
const normalizeExternalSQLDirectoryName = (rawName: unknown): string => {
|
||||
return String(rawName || '').trim();
|
||||
};
|
||||
|
||||
const getExternalSQLParentDirectoryPath = (node: any): string => {
|
||||
const path = String(node?.dataRef?.path || '').trim();
|
||||
if (node?.type === 'external-sql-directory' || node?.type === 'external-sql-folder') {
|
||||
return path;
|
||||
}
|
||||
if (node?.type === 'external-sql-file') {
|
||||
const index = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
|
||||
return index > 0 ? path.slice(0, index) : '';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const normalizeSQLFileDialogData = (data: unknown): { content: string; filePath: string; fileName: string; isLargeFile: boolean; fileSizeMB?: string } => {
|
||||
if (data && typeof data === 'object') {
|
||||
const payload = data as Record<string, unknown>;
|
||||
const filePath = String(payload.filePath || '').trim();
|
||||
return {
|
||||
content: String(payload.content ?? ''),
|
||||
filePath,
|
||||
fileName: String(payload.name || filePath.split(/[\\/]/).filter(Boolean).pop() || t('sidebar.sql_file_exec.title')).trim(),
|
||||
isLargeFile: payload.isLargeFile === true,
|
||||
fileSizeMB: String(payload.fileSizeMB || '').trim() || undefined,
|
||||
};
|
||||
}
|
||||
return {
|
||||
content: String(data || ''),
|
||||
filePath: '',
|
||||
fileName: t('sidebar.sql_file_exec.title'),
|
||||
isLargeFile: false,
|
||||
};
|
||||
};
|
||||
|
||||
const resolveSQLFileExecutionStatusLabel = (status: SQLFileExecutionStatus): string => {
|
||||
switch (status) {
|
||||
case 'done':
|
||||
return `✅ ${t('sidebar.sql_file_exec.status.done')}`;
|
||||
case 'cancelled':
|
||||
return `⚠️ ${t('sidebar.sql_file_exec.status.cancelled')}`;
|
||||
case 'error':
|
||||
return `❌ ${t('sidebar.sql_file_exec.status.error')}`;
|
||||
case 'running':
|
||||
default:
|
||||
return t('sidebar.sql_file_exec.status.running');
|
||||
}
|
||||
};
|
||||
|
||||
export const buildSQLFileExecutionFooter = ({
|
||||
status,
|
||||
onCancelExecution,
|
||||
onClose,
|
||||
}: {
|
||||
status: SQLFileExecutionStatus;
|
||||
onCancelExecution: () => void;
|
||||
onClose: () => void;
|
||||
}): React.ReactNode[] => {
|
||||
if (status === 'running') {
|
||||
return [
|
||||
<Button key="cancel" danger onClick={onCancelExecution}>
|
||||
{t('sidebar.sql_file_exec.cancel')}
|
||||
</Button>,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
<Button key="close" type="primary" onClick={onClose}>
|
||||
{t('sidebar.action.close')}
|
||||
</Button>,
|
||||
];
|
||||
};
|
||||
|
||||
export const SQLFileExecutionProgressContent: React.FC<SQLFileExecutionProgressState> = ({
|
||||
fileSizeMB,
|
||||
status,
|
||||
executed,
|
||||
failed,
|
||||
percent,
|
||||
currentSQL,
|
||||
resultMessage,
|
||||
}) => (
|
||||
<>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Progress
|
||||
percent={Math.round(percent)}
|
||||
status={status === 'error' ? 'exception' : status === 'done' ? 'success' : 'active'}
|
||||
strokeColor={status === 'cancelled' ? '#faad14' : undefined}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ fontSize: 13, lineHeight: '22px', marginBottom: 8 }}>
|
||||
<div>{t('sidebar.sql_file_exec.file_size')}<strong>{fileSizeMB} MB</strong></div>
|
||||
<div>{t('sidebar.sql_file_exec.status_label')}<strong>{resolveSQLFileExecutionStatusLabel(status)}</strong></div>
|
||||
<div>
|
||||
{t('sidebar.sql_file_exec.executed_label')}
|
||||
<strong style={{ color: '#52c41a' }}>{executed}</strong>
|
||||
{t('sidebar.sql_file_exec.rows_separator')}
|
||||
<strong style={{ color: failed > 0 ? '#ff4d4f' : undefined }}>{failed}</strong>
|
||||
{t('sidebar.sql_file_exec.rows_suffix')}
|
||||
</div>
|
||||
</div>
|
||||
{currentSQL && status === 'running' && (
|
||||
<div style={{ fontSize: 12, color: 'rgba(128,128,128,0.8)', background: 'rgba(128,128,128,0.06)', borderRadius: 6, padding: '6px 10px', marginTop: 8, fontFamily: 'var(--gn-font-mono)', wordBreak: 'break-all', maxHeight: 60, overflow: 'hidden' }}>
|
||||
{currentSQL}
|
||||
</div>
|
||||
)}
|
||||
{resultMessage && status !== 'running' && (
|
||||
<div style={{ fontSize: 12, marginTop: 12, maxHeight: 200, overflow: 'auto', whiteSpace: 'pre-wrap', background: 'rgba(128,128,128,0.06)', borderRadius: 6, padding: '8px 12px' }}>
|
||||
{resultMessage}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
export const ExternalSQLFileModal: React.FC<ExternalSQLFileModalProps> = ({
|
||||
open,
|
||||
mode,
|
||||
form,
|
||||
onOk,
|
||||
onCancel,
|
||||
}) => (
|
||||
<Modal
|
||||
title={
|
||||
mode === 'create'
|
||||
? t('sidebar.external_sql_modal.title.create_file')
|
||||
: mode === 'rename'
|
||||
? t('sidebar.external_sql_modal.title.rename_file')
|
||||
: mode === 'create-directory'
|
||||
? t('sidebar.external_sql_modal.title.create_directory')
|
||||
: t('sidebar.external_sql_modal.title.rename_directory')
|
||||
}
|
||||
open={open}
|
||||
onOk={onOk}
|
||||
onCancel={onCancel}
|
||||
okText={t(mode === 'create' || mode === 'create-directory' ? 'sidebar.external_sql_modal.action.create' : 'sidebar.external_sql_modal.action.rename')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={isExternalSQLDirectoryModalMode(mode) ? t('sidebar.external_sql_modal.field.directory_name') : t('sidebar.external_sql_modal.field.sql_file_name')}
|
||||
rules={[
|
||||
{ required: true, message: isExternalSQLDirectoryModalMode(mode) ? t('sidebar.external_sql_modal.validation.directory_name_required') : t('sidebar.external_sql_modal.validation.sql_file_name_required') },
|
||||
{
|
||||
validator: async (_, value) => {
|
||||
const name = String(value || '').trim();
|
||||
if (!name) return;
|
||||
if (/[\\/]/.test(name) || name === '.' || name === '..') {
|
||||
throw new Error(isExternalSQLDirectoryModalMode(mode) ? t('sidebar.external_sql_modal.validation.directory_name_no_separator') : t('sidebar.external_sql_modal.validation.sql_file_name_no_separator'));
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
extra={isExternalSQLDirectoryModalMode(mode) ? t('sidebar.external_sql_modal.help.directory') : t('sidebar.external_sql_modal.help.sql_file')}
|
||||
>
|
||||
<Input {...noAutoCapInputProps} placeholder={isExternalSQLDirectoryModalMode(mode) ? t('sidebar.external_sql_modal.placeholder.directory_name') : t('sidebar.external_sql_modal.placeholder.sql_file_name')} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
export const SQLFileExecutionModal: React.FC<SQLFileExecutionModalProps> = ({
|
||||
title,
|
||||
state,
|
||||
modalPanelStyle,
|
||||
onCancelExecution,
|
||||
onClose,
|
||||
}) => (
|
||||
<Modal
|
||||
title={title}
|
||||
open={state.open}
|
||||
centered
|
||||
closable={state.status !== 'running'}
|
||||
maskClosable={false}
|
||||
footer={buildSQLFileExecutionFooter({
|
||||
status: state.status,
|
||||
onCancelExecution,
|
||||
onClose,
|
||||
})}
|
||||
onCancel={() => {
|
||||
if (state.status !== 'running') {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
styles={{ content: modalPanelStyle, header: { background: 'transparent', borderBottom: 'none' }, body: { paddingTop: 8 }, footer: { background: 'transparent', borderTop: 'none' } }}
|
||||
>
|
||||
<SQLFileExecutionProgressContent
|
||||
fileSizeMB={state.fileSizeMB}
|
||||
status={state.status}
|
||||
executed={state.executed}
|
||||
failed={state.failed}
|
||||
percent={state.percent}
|
||||
currentSQL={state.currentSQL}
|
||||
resultMessage={state.resultMessage}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
export const useSidebarExternalSqlWorkflow = ({
|
||||
connections,
|
||||
externalSQLDirectories,
|
||||
activeTab,
|
||||
connectionIds,
|
||||
selectedNodesRef,
|
||||
addTab,
|
||||
saveExternalSQLDirectory,
|
||||
deleteExternalSQLDirectory,
|
||||
refreshGlobalExternalSQLRootNode,
|
||||
setExpandedKeys,
|
||||
setAutoExpandParent,
|
||||
getActiveContext,
|
||||
}: UseSidebarExternalSqlWorkflowOptions) => {
|
||||
const [isExternalSQLFileModalOpen, setIsExternalSQLFileModalOpen] = useState(false);
|
||||
const [externalSQLFileForm] = Form.useForm();
|
||||
const [externalSQLFileModalMode, setExternalSQLFileModalMode] = useState<ExternalSQLFileModalMode>('create');
|
||||
const [externalSQLFileTarget, setExternalSQLFileTarget] = useState<any>(null);
|
||||
const [sqlFileExecState, setSqlFileExecState] = useState<SQLFileExecutionState>(createInitialSQLFileExecutionState);
|
||||
|
||||
const startSQLFileExecution = (config: any, dbName: string, filePath: string, fileSizeMB: string) => {
|
||||
const jobId = `sqlfile-${Date.now()}`;
|
||||
setSqlFileExecState({
|
||||
open: true,
|
||||
jobId,
|
||||
fileSizeMB,
|
||||
status: 'running',
|
||||
executed: 0,
|
||||
failed: 0,
|
||||
total: 0,
|
||||
percent: 0,
|
||||
currentSQL: '',
|
||||
resultMessage: '',
|
||||
});
|
||||
|
||||
const offProgress = EventsOn('sqlfile:progress', (event: any) => {
|
||||
if (!event || event.jobId !== jobId) return;
|
||||
setSqlFileExecState(prev => ({
|
||||
...prev,
|
||||
status: event.status || prev.status,
|
||||
executed: typeof event.executed === 'number' ? event.executed : prev.executed,
|
||||
failed: typeof event.failed === 'number' ? event.failed : prev.failed,
|
||||
total: typeof event.total === 'number' ? event.total : prev.total,
|
||||
percent: typeof event.percent === 'number' ? Math.min(100, event.percent) : prev.percent,
|
||||
currentSQL: typeof event.currentSQL === 'string' ? event.currentSQL : prev.currentSQL,
|
||||
}));
|
||||
});
|
||||
|
||||
ExecuteSQLFile(config, dbName, filePath, jobId).then(res => {
|
||||
offProgress();
|
||||
setSqlFileExecState(prev => ({
|
||||
...prev,
|
||||
status: res.success ? 'done' : (prev.status === 'cancelled' ? 'cancelled' : 'error'),
|
||||
percent: 100,
|
||||
resultMessage: res.message || '',
|
||||
}));
|
||||
}).catch(err => {
|
||||
offProgress();
|
||||
setSqlFileExecState(prev => ({
|
||||
...prev,
|
||||
status: 'error',
|
||||
resultMessage: String(err?.message || err),
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
const handleRunSQLFile = async (node: any) => {
|
||||
const res = await OpenSQLFile();
|
||||
if (res.success) {
|
||||
const data = normalizeSQLFileDialogData(res.data);
|
||||
if (data.isLargeFile) {
|
||||
const connId = node.type === 'connection' ? node.key : node.dataRef?.id;
|
||||
const dbName = node.dataRef?.dbName || '';
|
||||
const conn = connections.find(c => c.id === connId);
|
||||
if (!conn) {
|
||||
message.error(t('sidebar.message.connection_config_not_found'));
|
||||
return;
|
||||
}
|
||||
startSQLFileExecution(conn.config, dbName, data.filePath, data.fileSizeMB || '');
|
||||
return;
|
||||
}
|
||||
|
||||
const { dbName, id } = node.dataRef;
|
||||
const connectionId = node.type === 'connection' ? String(node.key) : String(id || node.dataRef.id || '');
|
||||
addTab({
|
||||
id: data.filePath ? buildExternalSQLTabId(connectionId, dbName || '', data.filePath) : `query-${Date.now()}`,
|
||||
title: data.fileName || t('sidebar.sql_file_exec.title'),
|
||||
type: 'query',
|
||||
connectionId,
|
||||
dbName: dbName,
|
||||
query: data.content,
|
||||
filePath: data.filePath || undefined,
|
||||
});
|
||||
} else if (res.message !== '已取消') {
|
||||
message.error(t('sidebar.message.read_file_failed', { error: res.message }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenSQLFileFromToolbar = async () => {
|
||||
const ctx = getActiveContext();
|
||||
if (!ctx?.connectionId) {
|
||||
message.warning(t('sidebar.message.select_connection_or_database_first'));
|
||||
return;
|
||||
}
|
||||
const res = await OpenSQLFile();
|
||||
if (res.success) {
|
||||
const data = normalizeSQLFileDialogData(res.data);
|
||||
if (data.isLargeFile) {
|
||||
const conn = connections.find(c => c.id === ctx.connectionId);
|
||||
if (!conn) {
|
||||
message.error(t('sidebar.message.connection_config_not_found'));
|
||||
return;
|
||||
}
|
||||
startSQLFileExecution(conn.config, ctx.dbName || '', data.filePath, data.fileSizeMB || '');
|
||||
return;
|
||||
}
|
||||
|
||||
addTab({
|
||||
id: data.filePath ? buildExternalSQLTabId(ctx.connectionId, ctx.dbName || '', data.filePath) : `query-${Date.now()}`,
|
||||
title: data.fileName || t('sidebar.sql_file_exec.title'),
|
||||
type: 'query',
|
||||
connectionId: ctx.connectionId,
|
||||
dbName: ctx.dbName || undefined,
|
||||
query: data.content,
|
||||
filePath: data.filePath || undefined,
|
||||
});
|
||||
} else if (res.message !== '已取消') {
|
||||
message.error(t('sidebar.message.read_file_failed', { error: res.message }));
|
||||
}
|
||||
};
|
||||
|
||||
const resolveExternalSQLExecutionContext = (): { connectionId: string; dbName: string } => {
|
||||
const activeStoreContext = getActiveContext();
|
||||
const selectedConnectionId = selectedNodesRef.current
|
||||
.map((node) => resolveSidebarNodeConnectionId(node, connectionIds))
|
||||
.find(Boolean) || '';
|
||||
return {
|
||||
connectionId: String(
|
||||
activeStoreContext?.connectionId
|
||||
|| activeTab?.connectionId
|
||||
|| selectedConnectionId
|
||||
|| '',
|
||||
).trim(),
|
||||
dbName: String(
|
||||
activeStoreContext?.dbName
|
||||
|| activeTab?.dbName
|
||||
|| '',
|
||||
).trim(),
|
||||
};
|
||||
};
|
||||
|
||||
const openExternalSQLFile = async (fileNode: any) => {
|
||||
const fileContext = {
|
||||
connectionId: String(fileNode?.dataRef?.connectionId || '').trim(),
|
||||
dbName: String(fileNode?.dataRef?.dbName || '').trim(),
|
||||
};
|
||||
const fallbackContext = resolveExternalSQLExecutionContext();
|
||||
const connectionId = fileContext.connectionId || fallbackContext.connectionId;
|
||||
const dbName = fileContext.dbName || fallbackContext.dbName;
|
||||
const filePath = String(fileNode?.dataRef?.path || '').trim();
|
||||
const fileName = String(fileNode?.dataRef?.name || fileNode?.title || t('sidebar.sql_file.default_name')).trim() || t('sidebar.sql_file.default_name');
|
||||
if (!filePath) {
|
||||
message.error(t('sidebar.message.sql_file_path_incomplete'));
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await ReadSQLFile(filePath);
|
||||
if (!res.success) {
|
||||
if (res.message !== '已取消') {
|
||||
message.error(t('sidebar.message.read_sql_file_failed', { error: res.message }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const data = res.data;
|
||||
if (data && typeof data === 'object' && data.isLargeFile) {
|
||||
if (!connectionId) {
|
||||
message.warning(t('sidebar.message.select_host_before_large_sql_file'));
|
||||
return;
|
||||
}
|
||||
const conn = connections.find((item) => item.id === connectionId);
|
||||
if (!conn) {
|
||||
message.error(t('sidebar.message.connection_config_not_found'));
|
||||
return;
|
||||
}
|
||||
startSQLFileExecution(conn.config, dbName, data.filePath, data.fileSizeMB);
|
||||
return;
|
||||
}
|
||||
|
||||
addTab({
|
||||
id: buildExternalSQLTabId(connectionId, dbName, filePath),
|
||||
title: fileName,
|
||||
type: 'query',
|
||||
connectionId,
|
||||
dbName: dbName || undefined,
|
||||
query: String(data || ''),
|
||||
filePath,
|
||||
});
|
||||
};
|
||||
|
||||
const openCreateExternalSQLFileModal = (node: any) => {
|
||||
const directoryPath = getExternalSQLParentDirectoryPath(node);
|
||||
if (!directoryPath) {
|
||||
message.error(t('sidebar.message.external_sql_file_parent_missing'));
|
||||
return;
|
||||
}
|
||||
setExternalSQLFileModalMode('create');
|
||||
setExternalSQLFileTarget(node);
|
||||
externalSQLFileForm.setFieldsValue({ name: 'new-query.sql' });
|
||||
setIsExternalSQLFileModalOpen(true);
|
||||
};
|
||||
|
||||
const openRenameExternalSQLFileModal = (node: any) => {
|
||||
const currentName = String(node?.dataRef?.name || node?.title || '').trim();
|
||||
if (!currentName) {
|
||||
message.error(t('sidebar.message.external_sql_file_rename_target_missing'));
|
||||
return;
|
||||
}
|
||||
setExternalSQLFileModalMode('rename');
|
||||
setExternalSQLFileTarget(node);
|
||||
externalSQLFileForm.setFieldsValue({ name: currentName });
|
||||
setIsExternalSQLFileModalOpen(true);
|
||||
};
|
||||
|
||||
const openCreateExternalSQLDirectoryModal = (node: any) => {
|
||||
const directoryPath = getExternalSQLParentDirectoryPath(node);
|
||||
if (!directoryPath) {
|
||||
message.error(t('sidebar.message.external_sql_directory_parent_missing'));
|
||||
return;
|
||||
}
|
||||
setExternalSQLFileModalMode('create-directory');
|
||||
setExternalSQLFileTarget(node);
|
||||
externalSQLFileForm.setFieldsValue({ name: 'new-folder' });
|
||||
setIsExternalSQLFileModalOpen(true);
|
||||
};
|
||||
|
||||
const openRenameExternalSQLDirectoryModal = (node: any) => {
|
||||
const currentName = String(node?.dataRef?.name || node?.title || '').trim();
|
||||
if (!currentName) {
|
||||
message.error(t('sidebar.message.external_sql_directory_rename_target_missing'));
|
||||
return;
|
||||
}
|
||||
setExternalSQLFileModalMode('rename-directory');
|
||||
setExternalSQLFileTarget(node);
|
||||
externalSQLFileForm.setFieldsValue({ name: currentName });
|
||||
setIsExternalSQLFileModalOpen(true);
|
||||
};
|
||||
|
||||
const closeExternalSQLFileModal = () => {
|
||||
setIsExternalSQLFileModalOpen(false);
|
||||
setExternalSQLFileTarget(null);
|
||||
externalSQLFileForm.resetFields();
|
||||
};
|
||||
|
||||
const handleExternalSQLFileModalOk = async () => {
|
||||
try {
|
||||
const values = await externalSQLFileForm.validateFields();
|
||||
const isDirectoryMode = isExternalSQLDirectoryModalMode(externalSQLFileModalMode);
|
||||
const name = isDirectoryMode
|
||||
? normalizeExternalSQLDirectoryName(values.name)
|
||||
: normalizeExternalSQLFileName(values.name);
|
||||
if (!name) {
|
||||
message.error(t(isDirectoryMode ? 'sidebar.message.sql_directory_name_required' : 'sidebar.message.sql_file_name_required'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (externalSQLFileModalMode === 'create') {
|
||||
const directoryPath = getExternalSQLParentDirectoryPath(externalSQLFileTarget);
|
||||
if (!directoryPath) {
|
||||
message.error(t('sidebar.message.external_sql_file_parent_missing'));
|
||||
return;
|
||||
}
|
||||
const res = await CreateSQLFile(directoryPath, name);
|
||||
if (!res.success) {
|
||||
message.error(t('sidebar.message.create_sql_file_failed', { error: res.message }));
|
||||
return;
|
||||
}
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
message.success(t('sidebar.message.sql_file_created'));
|
||||
} else if (externalSQLFileModalMode === 'rename') {
|
||||
const filePath = String(externalSQLFileTarget?.dataRef?.path || '').trim();
|
||||
if (!filePath) {
|
||||
message.error(t('sidebar.message.external_sql_file_rename_target_missing'));
|
||||
return;
|
||||
}
|
||||
const res = await RenameSQLFile(filePath, name);
|
||||
if (!res.success) {
|
||||
message.error(t('sidebar.message.rename_sql_file_failed', { error: res.message }));
|
||||
return;
|
||||
}
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
message.success(t('sidebar.message.sql_file_renamed'));
|
||||
} else if (externalSQLFileModalMode === 'create-directory') {
|
||||
const directoryPath = getExternalSQLParentDirectoryPath(externalSQLFileTarget);
|
||||
if (!directoryPath) {
|
||||
message.error(t('sidebar.message.external_sql_directory_parent_missing'));
|
||||
return;
|
||||
}
|
||||
const res = await CreateSQLDirectory(directoryPath, name);
|
||||
if (!res.success) {
|
||||
message.error(t('sidebar.message.create_sql_directory_failed', { error: res.message }));
|
||||
return;
|
||||
}
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
message.success(t('sidebar.message.sql_directory_created'));
|
||||
} else {
|
||||
const directoryPath = String(externalSQLFileTarget?.dataRef?.path || '').trim();
|
||||
if (!directoryPath) {
|
||||
message.error(t('sidebar.message.external_sql_directory_rename_target_missing'));
|
||||
return;
|
||||
}
|
||||
const res = await RenameSQLDirectory(directoryPath, name);
|
||||
if (!res.success) {
|
||||
message.error(t('sidebar.message.rename_sql_directory_failed', { error: res.message }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (externalSQLFileTarget?.type === 'external-sql-directory') {
|
||||
const payload = (res.data && typeof res.data === 'object') ? res.data as Record<string, unknown> : {};
|
||||
const nextPath = String(payload.directoryPath || payload.path || '').trim();
|
||||
const nextName = String(payload.name || name).trim();
|
||||
const oldDirectoryId = String(externalSQLFileTarget?.dataRef?.id || '').trim();
|
||||
if (!nextPath || !oldDirectoryId) {
|
||||
message.error(t('sidebar.message.external_sql_directory_rename_sync_failed'));
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
return;
|
||||
}
|
||||
const nextDirectory: ExternalSQLDirectory = {
|
||||
id: buildExternalSQLDirectoryId('', '', nextPath),
|
||||
name: nextName || nextPath.split(/[\\/]/).filter(Boolean).pop() || t('sidebar.sql_directory.default_name'),
|
||||
path: nextPath,
|
||||
createdAt: Number(externalSQLFileTarget?.dataRef?.createdAt) || Date.now(),
|
||||
};
|
||||
deleteExternalSQLDirectory(oldDirectoryId);
|
||||
saveExternalSQLDirectory(nextDirectory);
|
||||
const nextDirectories = [
|
||||
...externalSQLDirectories.filter((item) => item.id !== oldDirectoryId),
|
||||
nextDirectory,
|
||||
];
|
||||
await refreshGlobalExternalSQLRootNode(false, nextDirectories);
|
||||
} else {
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
}
|
||||
message.success(t('sidebar.message.sql_directory_renamed'));
|
||||
}
|
||||
|
||||
closeExternalSQLFileModal();
|
||||
} catch {
|
||||
// Validate failed
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteExternalSQLFile = (node: any) => {
|
||||
const filePath = String(node?.dataRef?.path || '').trim();
|
||||
const fileName = String(node?.dataRef?.name || node?.title || t('sidebar.sql_file.default_name')).trim();
|
||||
if (!filePath) {
|
||||
message.error(t('sidebar.message.external_sql_file_delete_target_missing'));
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: t('sidebar.modal.confirm_delete_sql_file.title'),
|
||||
content: t('sidebar.modal.confirm_delete_sql_file.content', { name: fileName }),
|
||||
okText: t('sidebar.action.delete'),
|
||||
cancelText: t('sidebar.action.cancel'),
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
const res = await DeleteSQLFile(filePath);
|
||||
if (!res.success) {
|
||||
message.error(t('sidebar.message.delete_sql_file_failed', { error: res.message }));
|
||||
return;
|
||||
}
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
message.success(t('sidebar.message.sql_file_deleted'));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteExternalSQLDirectory = (node: any) => {
|
||||
const directoryPath = String(node?.dataRef?.path || '').trim();
|
||||
const directoryName = String(node?.dataRef?.name || node?.title || t('sidebar.sql_directory.default_name')).trim();
|
||||
if (!directoryPath) {
|
||||
message.error(t('sidebar.message.external_sql_directory_delete_target_missing'));
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: t('sidebar.modal.confirm_delete_sql_directory.title'),
|
||||
content: t('sidebar.modal.confirm_delete_sql_directory.content', { name: directoryName }),
|
||||
okText: t('sidebar.action.delete'),
|
||||
cancelText: t('sidebar.action.cancel'),
|
||||
okButtonProps: { danger: true },
|
||||
onOk: async () => {
|
||||
const res = await DeleteSQLDirectory(directoryPath);
|
||||
if (!res.success) {
|
||||
message.error(t('sidebar.message.delete_sql_directory_failed', { error: res.message }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (node?.type === 'external-sql-directory') {
|
||||
const directoryId = String(node?.dataRef?.id || '').trim();
|
||||
if (directoryId) {
|
||||
deleteExternalSQLDirectory(directoryId);
|
||||
const nextDirectories = externalSQLDirectories.filter((item) => item.id !== directoryId);
|
||||
await refreshGlobalExternalSQLRootNode(false, nextDirectories);
|
||||
} else {
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
}
|
||||
} else {
|
||||
await refreshGlobalExternalSQLRootNode(false);
|
||||
}
|
||||
message.success(t('sidebar.message.sql_directory_deleted'));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleAddExternalSQLDirectory = async (node: any) => {
|
||||
void node;
|
||||
const currentDirectory = externalSQLDirectories[0]?.path || '';
|
||||
const selection = await SelectSQLDirectory(currentDirectory);
|
||||
if (!selection.success) {
|
||||
if (selection.message !== '已取消') {
|
||||
message.error(t('sidebar.message.select_sql_directory_failed', { error: selection.message }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = (selection.data && typeof selection.data === 'object') ? selection.data as Record<string, unknown> : {};
|
||||
const path = String(payload.path || '').trim();
|
||||
const name = String(payload.name || '').trim();
|
||||
if (!path) {
|
||||
message.error(t('sidebar.message.sql_directory_path_invalid'));
|
||||
return;
|
||||
}
|
||||
|
||||
const directoryId = buildExternalSQLDirectoryId('', '', path);
|
||||
const nextDirectory: ExternalSQLDirectory = {
|
||||
id: directoryId,
|
||||
name: name || path.split(/[\\/]/).filter(Boolean).pop() || t('sidebar.sql_directory.default_name'),
|
||||
path,
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
saveExternalSQLDirectory(nextDirectory);
|
||||
|
||||
const nextDirectories = [
|
||||
...externalSQLDirectories.filter((item) => item.path.replace(/\\/g, '/').toLowerCase() !== path.replace(/\\/g, '/').toLowerCase()),
|
||||
nextDirectory,
|
||||
];
|
||||
setExpandedKeys((prev) => Array.from(new Set([...prev, 'external-sql-root'])));
|
||||
setAutoExpandParent(false);
|
||||
await refreshGlobalExternalSQLRootNode(false, nextDirectories);
|
||||
message.success(t('sidebar.message.external_sql_directory_added'));
|
||||
};
|
||||
|
||||
const handleRemoveExternalSQLDirectory = async (node: any) => {
|
||||
const directoryId = String(node?.dataRef?.id || '').trim();
|
||||
if (!directoryId) {
|
||||
message.error(t('sidebar.message.external_sql_directory_not_found'));
|
||||
return;
|
||||
}
|
||||
deleteExternalSQLDirectory(directoryId);
|
||||
const nextDirectories = externalSQLDirectories.filter((item) => item.id !== directoryId);
|
||||
await refreshGlobalExternalSQLRootNode(false, nextDirectories);
|
||||
message.success(t('sidebar.message.external_sql_directory_removed'));
|
||||
};
|
||||
|
||||
const handleRefreshExternalSQLDirectory = async (node: any) => {
|
||||
void node;
|
||||
await refreshGlobalExternalSQLRootNode(true);
|
||||
message.success(t('sidebar.message.external_sql_directory_refreshed'));
|
||||
};
|
||||
|
||||
const cancelSQLFileExecution = () => {
|
||||
CancelSQLFileExecution(sqlFileExecState.jobId);
|
||||
setSqlFileExecState(prev => ({ ...prev, status: 'cancelled' }));
|
||||
};
|
||||
|
||||
const closeSQLFileExecutionModal = () => {
|
||||
setSqlFileExecState(prev => ({ ...prev, open: false }));
|
||||
};
|
||||
|
||||
return {
|
||||
handleRunSQLFile,
|
||||
handleOpenSQLFileFromToolbar,
|
||||
openExternalSQLFile,
|
||||
openCreateExternalSQLFileModal,
|
||||
openRenameExternalSQLFileModal,
|
||||
openCreateExternalSQLDirectoryModal,
|
||||
openRenameExternalSQLDirectoryModal,
|
||||
handleExternalSQLFileModalOk,
|
||||
handleDeleteExternalSQLFile,
|
||||
handleDeleteExternalSQLDirectory,
|
||||
handleAddExternalSQLDirectory,
|
||||
handleRemoveExternalSQLDirectory,
|
||||
handleRefreshExternalSQLDirectory,
|
||||
externalSQLFileModalProps: {
|
||||
open: isExternalSQLFileModalOpen,
|
||||
mode: externalSQLFileModalMode,
|
||||
form: externalSQLFileForm,
|
||||
onOk: handleExternalSQLFileModalOk,
|
||||
onCancel: closeExternalSQLFileModal,
|
||||
},
|
||||
sqlFileExecutionModalProps: {
|
||||
state: sqlFileExecState,
|
||||
onCancelExecution: cancelSQLFileExecution,
|
||||
onClose: closeSQLFileExecutionModal,
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user