/
vshmidt
/
cloudbeaver
Обзор
Документация
Войти
/
vshmidt
/
cloudbeaver
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
devel
webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts
517 строк
18 KB
sergeyteleshev
Save scripts to custom locations (use import/export buttons) (#4258)
15 апр 2026, 16:12
Не верифицирован
15 апр 2026, 16:12
1660e62
Код
Авторство
О чём код?
/* * CloudBeaver - Cloud Database Manager * Copyright (C) 2020-2026 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ import type { IDataContextProvider } from '@cloudbeaver/core-data-context'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { WindowEventsService } from '@cloudbeaver/core-root'; import { getTextFileReadingProcess, throttle, withTimestamp } from '@cloudbeaver/core-utils'; import { ACTION_DOWNLOAD, ACTION_REDO, ACTION_SAVE, ACTION_UNDO, ACTION_UPLOAD, ActionService, type IAction, KEY_BINDING_REDO, KEY_BINDING_SAVE, KEY_BINDING_UNDO, KeyBindingService, menuExtractItems, MenuService, } from '@cloudbeaver/core-view'; import { ConnectionInfoResource, createConnectionParam, type Connection } from '@cloudbeaver/core-connections'; import { promptForFiles } from '@cloudbeaver/core-browser'; import { NotificationService } from '@cloudbeaver/core-events'; import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { ConfirmationDialog, importLazyComponent } from '@cloudbeaver/core-blocks'; import { ScriptExportService } from '@cloudbeaver/plugin-script-export'; import { ACTION_SQL_EDITOR_EXECUTE } from './actions/ACTION_SQL_EDITOR_EXECUTE.js'; import { ACTION_SQL_EDITOR_EXECUTE_NEW } from './actions/ACTION_SQL_EDITOR_EXECUTE_NEW.js'; import { ACTION_SQL_EDITOR_EXECUTE_SCRIPT } from './actions/ACTION_SQL_EDITOR_EXECUTE_SCRIPT.js'; import { ACTION_SQL_EDITOR_FORMAT } from './actions/ACTION_SQL_EDITOR_FORMAT.js'; import { ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN } from './actions/ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN.js'; import { KEY_BINDING_SQL_EDITOR_EXECUTE } from './actions/bindings/KEY_BINDING_SQL_EDITOR_EXECUTE.js'; import { KEY_BINDING_SQL_EDITOR_EXECUTE_NEW } from './actions/bindings/KEY_BINDING_SQL_EDITOR_EXECUTE_NEW.js'; import { KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT } from './actions/bindings/KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT.js'; import { KEY_BINDING_SQL_EDITOR_FORMAT } from './actions/bindings/KEY_BINDING_SQL_EDITOR_FORMAT.js'; import { KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN } from './actions/bindings/KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN.js'; import { DATA_CONTEXT_SQL_EDITOR_STATE } from './DATA_CONTEXT_SQL_EDITOR_STATE.js'; import { ESqlDataSourceFeatures } from './SqlDataSource/ESqlDataSourceFeatures.js'; import { SqlDataSourceService } from './SqlDataSource/SqlDataSourceService.js'; import { DATA_CONTEXT_SQL_EDITOR_DATA } from './SqlEditor/DATA_CONTEXT_SQL_EDITOR_DATA.js'; import { SQL_EDITOR_TOOLS_MENU } from './SqlEditor/SQL_EDITOR_TOOLS_MENU.js'; import { SQL_EDITOR_TOOLS_MORE_MENU } from './SqlEditor/SQL_EDITOR_TOOLS_MORE_MENU.js'; import { SQL_EDITOR_ACTIONS_MENU } from './SqlEditor/SQL_EDITOR_ACTIONS_MENU.js'; import { getSqlEditorName } from './getSqlEditorName.js'; import type { ISqlEditorTabState } from './ISqlEditorTabState.js'; import { SqlEditorSettingsService } from './SqlEditorSettingsService.js'; import { downloadSql } from './downloadSql.js'; const SYNC_DELAY = 5 * 60 * 1000; const EXECUTIONS_ACTIONS = [ ACTION_SQL_EDITOR_EXECUTE, ACTION_SQL_EDITOR_EXECUTE_NEW, ACTION_SQL_EDITOR_EXECUTE_SCRIPT, ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN, ]; const LOCAL_EXPORT_TAB_ID = 'sql-editor-local-export-tab'; const LocalExportPanel = importLazyComponent(() => import('./LocalExport/LocalExportPanel.js').then(module => module.LocalExportPanel)); @injectable(() => [ MenuService, ActionService, KeyBindingService, SqlDataSourceService, WindowEventsService, ConnectionInfoResource, SqlEditorSettingsService, NotificationService, CommonDialogService, ScriptExportService, ]) export class MenuBootstrap extends Bootstrap { constructor( private readonly menuService: MenuService, private readonly actionService: ActionService, private readonly keyBindingService: KeyBindingService, private readonly sqlDataSourceService: SqlDataSourceService, private readonly windowEventsService: WindowEventsService, private readonly connectionInfoResource: ConnectionInfoResource, private readonly sqlEditorSettingsService: SqlEditorSettingsService, private readonly notificationService: NotificationService, private readonly commonDialogService: CommonDialogService, private readonly scriptExportService: ScriptExportService, ) { super(); } override register(): void { this.windowEventsService.onFocusChange.addHandler(throttle(this.focusChangeHandler.bind(this), SYNC_DELAY, false)); this.actionService.addHandler({ id: 'sql-editor-base-handler', contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], actions: [ACTION_SAVE], isActionApplicable: (context, action): boolean => { const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const dataSource = this.sqlDataSourceService.get(state.editorId); if (action === ACTION_SAVE) { return dataSource?.isAutoSaveEnabled === false; } return false; }, handler: async (context, action) => { if (action === ACTION_SAVE) { const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const source = this.sqlDataSourceService.get(state.editorId); if (!source) { return; } await source.save(); } }, isDisabled: (context, action) => { if (action === ACTION_SAVE) { const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const source = this.sqlDataSourceService.get(state.editorId); if (!source) { return true; } return source.isLoading() || source.isSaved || source.isReadonly(); } return false; }, getActionInfo: (context, action) => { if (action === ACTION_SAVE) { return { ...action.info, label: '', }; } return action.info; }, }); this.menuService.addCreator({ menus: [SQL_EDITOR_TOOLS_MENU], contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], getItems: (context, items) => [...items, ACTION_SQL_EDITOR_FORMAT, SQL_EDITOR_TOOLS_MORE_MENU], orderItems(context, items) { const extracted = menuExtractItems(items, [SQL_EDITOR_TOOLS_MORE_MENU]); return [...items, ...extracted]; }, }); this.menuService.addCreator({ menus: [SQL_EDITOR_TOOLS_MENU], contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], getItems: (context, items) => [...items, ACTION_DOWNLOAD, ACTION_UPLOAD], }); this.actionService.addHandler({ id: 'sql-editor-actions-more', actions: [ACTION_DOWNLOAD, ACTION_UPLOAD], contexts: [DATA_CONTEXT_SQL_EDITOR_DATA, DATA_CONTEXT_SQL_EDITOR_STATE], isDisabled: (context, action) => { const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const dataSource = this.sqlDataSourceService.get(state.editorId); switch (action) { case ACTION_DOWNLOAD: return !dataSource?.script; case ACTION_UPLOAD: return !!dataSource?.isReadonly(); } return false; }, getActionInfo(context, action) { switch (action) { case ACTION_DOWNLOAD: return { ...action.info, icon: '/icons/export.svg', label: 'sql_editor_download_script_tooltip', tooltip: 'sql_editor_download_script_tooltip', }; case ACTION_UPLOAD: return { ...action.info, icon: '/icons/import.svg', label: 'sql_editor_upload_script_tooltip', tooltip: 'sql_editor_upload_script_tooltip', }; } return action.info; }, handler: this.sqlEditorActionHandler.bind(this), }); this.menuService.addCreator({ menus: [SQL_EDITOR_TOOLS_MENU], contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], isApplicable: context => { const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const dataSource = this.sqlDataSourceService.get(state.editorId); return !!dataSource?.hasFeature(ESqlDataSourceFeatures.script); }, getItems: (context, items) => [ACTION_SAVE, ...items], }); this.menuService.addCreator({ menus: [SQL_EDITOR_ACTIONS_MENU], contexts: [DATA_CONTEXT_SQL_EDITOR_DATA, DATA_CONTEXT_SQL_EDITOR_STATE], getItems: (context, items) => [...items, ...EXECUTIONS_ACTIONS], }); this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-save', binding: KEY_BINDING_SAVE, actions: [ACTION_SAVE], contexts: [DATA_CONTEXT_SQL_EDITOR_STATE], handler: async context => { const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; const source = this.sqlDataSourceService.get(state.editorId); if (!source) { return; } await source.save(); }, }); this.actionService.addHandler({ id: 'sql-editor-actions', actions: [...EXECUTIONS_ACTIONS, ACTION_SQL_EDITOR_FORMAT, ACTION_REDO, ACTION_UNDO], contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isActionApplicable: (contexts, action): boolean => { const sqlEditorData = contexts.get(DATA_CONTEXT_SQL_EDITOR_DATA)!; if (sqlEditorData.readonly && [ACTION_REDO, ACTION_UNDO].includes(action)) { return false; } if (!sqlEditorData.isExecutionAllowed && EXECUTIONS_ACTIONS.includes(action)) { return false; } if (action === ACTION_SQL_EDITOR_FORMAT) { return !!sqlEditorData.model.dataSource?.hasFeature(ESqlDataSourceFeatures.script); } if (action === ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN) { return !!sqlEditorData.model.dataSource?.hasFeature(ESqlDataSourceFeatures.query) && !!sqlEditorData.dialect?.supportsExplainExecutionPlan; } // TODO we have to add check for output action ? if ( !sqlEditorData.model.dataSource?.hasFeature(ESqlDataSourceFeatures.query) && [ACTION_SQL_EDITOR_EXECUTE, ACTION_SQL_EDITOR_EXECUTE_NEW, ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN].includes(action) ) { return false; } return true; }, isDisabled: (context, action) => { const data = context.get(DATA_CONTEXT_SQL_EDITOR_DATA)!; if (EXECUTIONS_ACTIONS.includes(action)) { return data.isDisabled || data.isScriptEmpty; } switch (action) { case ACTION_SQL_EDITOR_FORMAT: return data.isDisabled || data.isScriptEmpty || data.readonly; } return false; }, getActionInfo: (context, action) => { if (EXECUTIONS_ACTIONS.includes(action)) { return { ...action.info, label: '', }; } return action.info; }, handler: this.sqlEditorActionHandler.bind(this), }); this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-execute', binding: KEY_BINDING_SQL_EDITOR_EXECUTE, contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_SQL_EDITOR_EXECUTE, handler: this.sqlEditorActionHandler.bind(this), }); this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-execute-new', binding: KEY_BINDING_SQL_EDITOR_EXECUTE_NEW, contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_SQL_EDITOR_EXECUTE_NEW, handler: this.sqlEditorActionHandler.bind(this), }); this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-execute-script', binding: KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT, contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => { const sqlEditorData = contexts.get(DATA_CONTEXT_SQL_EDITOR_DATA); return action === ACTION_SQL_EDITOR_EXECUTE_SCRIPT && sqlEditorData?.isExecutionAllowed === true; }, handler: this.sqlEditorActionHandler.bind(this), }); this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-format', binding: KEY_BINDING_SQL_EDITOR_FORMAT, contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_SQL_EDITOR_FORMAT, handler: this.sqlEditorActionHandler.bind(this), }); this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-redo', binding: KEY_BINDING_REDO, contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_REDO, handler: this.sqlEditorActionHandler.bind(this), }); this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-undo', binding: KEY_BINDING_UNDO, contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_UNDO, handler: this.sqlEditorActionHandler.bind(this), }); this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-show-execution-plan', binding: KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN, contexts: [DATA_CONTEXT_SQL_EDITOR_DATA], isBindingApplicable: (contexts, action) => action === ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN, handler: this.sqlEditorActionHandler.bind(this), }); // this.menuService.addCreator({ // isApplicable: context => ( // context.get(DATA_CONTEXT_SQL_EDITOR_DATA) !== undefined // && context.get(DATA_CONTEXT_MENU) === MENU_TAB // ), // getItems: (context, items) => [ // KEY_BINDING_SQL_EDITOR_EXECUTE, // ...items, // ], // }); this.scriptExportService.tabsContainer.add({ key: LOCAL_EXPORT_TAB_ID, name: 'plugin_sql_editor_export_local_tab', order: 1, panel: () => LocalExportPanel, }); } private sqlEditorActionHandler(context: IDataContextProvider, action: IAction): void { const data = context.get(DATA_CONTEXT_SQL_EDITOR_DATA)!; switch (action) { case ACTION_DOWNLOAD: { const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; this.downloadSql(state); } break; case ACTION_UPLOAD: { const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!; this.uploadSql(state); } break; case ACTION_SQL_EDITOR_EXECUTE: data.executeQuery(); break; case ACTION_SQL_EDITOR_EXECUTE_NEW: data.executeQuery(true); break; case ACTION_SQL_EDITOR_EXECUTE_SCRIPT: data.executeScript(); break; case ACTION_SQL_EDITOR_FORMAT: data.formatScript(); break; case ACTION_UNDO: data.model.dataSource?.history.undo(); break; case ACTION_REDO: data.model.dataSource?.history.redo(); break; case ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN: data.showExecutionPlan(); break; } } private focusChangeHandler(focused: boolean) { if (focused) { const dataSources = this.sqlDataSourceService.dataSources.values(); for (const [_, dataSource] of dataSources) { dataSource.markOutdated(); } } } private async downloadSql(state: ISqlEditorTabState) { const dataSource = this.sqlDataSourceService.get(state.editorId); if (!dataSource) { return; } const executionContext = dataSource?.executionContext; let connection: Connection | undefined; if (executionContext) { connection = this.connectionInfoResource.get(createConnectionParam(executionContext.projectId, executionContext.connectionId)); } const name = getSqlEditorName(state, dataSource, connection); const script = dataSource.script; const hasOnlyLocalExport = this.scriptExportService.tabsContainer.has(LOCAL_EXPORT_TAB_ID) && this.scriptExportService.tabsContainer.getDisplayed().length === 1; if (hasOnlyLocalExport) { const fileName = withTimestamp(name); downloadSql(fileName, script); return; } await this.scriptExportService.openExportDialog({ script, fileName: name, editorId: state.editorId, projectId: executionContext?.projectId, connectionId: executionContext?.connectionId, }); } private async uploadSql(state: ISqlEditorTabState) { const dataSource = this.sqlDataSourceService.get(state.editorId); if (!dataSource) { return; } const files = await promptForFiles({ accept: '.sql,.txt' }); const file = files[0]; if (!file) { throw new Error('File is not found'); } const maxSize = this.sqlEditorSettingsService.maxFileSize; const size = Math.round(file.size / 1024); // kilobyte const aboveMaxSize = size > maxSize; if (aboveMaxSize) { this.notificationService.logInfo({ title: 'sql_editor_upload_script_max_size_title', message: `Max size: ${maxSize}KB\nFile size: ${size}KB`, autoClose: false, }); return; } const prevScript = dataSource.script.trim(); if (prevScript) { const payload = { title: 'ui_changes_might_be_lost', message: 'sql_editor_upload_script_unsaved_changes_dialog_message', showExtraAction: true, confirmActionText: 'ui_yes', }; const { status, result } = await this.commonDialogService.open(ConfirmationDialog, payload); if (status === DialogueStateResult.Resolved) { this.downloadSql(state); } else if (!result?.isExtraAction) { return; } } try { const script = await getTextFileReadingProcess(file).promise; dataSource.setScript(script); } catch (exception: any) { this.notificationService.logException(exception, 'Uploading script error'); } } }