From 736fd39d08bb52862e7909f77728fb2e84cecbdb Mon Sep 17 00:00:00 2001 From: Alexey Date: Wed, 19 Apr 2023 19:01:44 +0300 Subject: [PATCH] CB-3300 feat: dataset editing (#1614) * CB-3300 feat: dataset editing * CB-3300 chore: remove unused styles --------- Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com> --- .../core-projects/src/ProjectsService.ts | 9 +++- webapp/packages/core-ui/src/Tabs/Tab/Tab.tsx | 2 +- .../packages/core-ui/src/Tabs/Tab/useTab.ts | 4 ++ .../core-ui/src/Tabs/TabsBootstrap.ts | 8 ++- .../packages/core-ui/src/Tabs/TabsContext.ts | 1 + .../packages/core-ui/src/Tabs/TabsState.tsx | 15 +++++- .../src/Action/Actions/ACTION_EDIT.ts | 13 +++++ webapp/packages/core-view/src/index.ts | 1 + webapp/packages/core-view/src/locales/en.ts | 1 + webapp/packages/core-view/src/locales/it.ts | 1 + webapp/packages/core-view/src/locales/ru.ts | 1 + .../src/PluginBootstrap.ts | 11 ++-- .../src/ResourceSqlDataSource.ts | 17 +++++-- .../src/SqlEditorTabService.ts | 32 ++++++++++-- .../src/locales/en.ts | 2 + .../src/locales/it.ts | 2 + .../src/locales/ru.ts | 2 + .../src/locales/zh.ts | 2 + .../plugin-sql-editor/src/MenuBootstrap.ts | 32 +++++++++++- .../src/SqlDataSource/BaseSqlDataSource.ts | 15 +++++- .../SqlDataSource/ESqlDataSourceFeatures.ts | 2 + .../src/SqlDataSource/ISqlDataSource.ts | 3 ++ .../LocalStorage/LocalStorageSqlDataSource.ts | 11 +++- .../src/SqlDataSource/MemorySqlDataSource.ts | 11 +++- .../SQL_EDITOR_ACTIONS_MENU_STYLES.ts | 4 +- .../src/SqlEditor/SQL_EDITOR_TOOLS_MENU.ts | 11 ++++ .../src/SqlEditor/SqlEditor.tsx | 50 ++++++++++++------- .../src/SqlEditor/SqlEditorActionsMenu.tsx | 2 +- .../src/SqlEditor/SqlEditorTools.tsx | 27 +++++----- .../src/SqlEditor/SqlEditorToolsMenu.tsx | 33 ++++++++++++ .../src/SqlEditor/useSqlEditor.ts | 21 +++++++- .../src/SqlResultTabs/SqlResultTabs.tsx | 18 +++++-- .../ACTION_SQL_EDITOR_EXECUTE_SCRIPT.ts | 1 + .../packages/plugin-sql-editor/src/index.ts | 3 +- 34 files changed, 304 insertions(+), 64 deletions(-) create mode 100644 webapp/packages/core-view/src/Action/Actions/ACTION_EDIT.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_TOOLS_MENU.ts create mode 100644 webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorToolsMenu.tsx diff --git a/webapp/packages/core-projects/src/ProjectsService.ts b/webapp/packages/core-projects/src/ProjectsService.ts index 0ea24ba39f..27c7bb02ba 100644 --- a/webapp/packages/core-projects/src/ProjectsService.ts +++ b/webapp/packages/core-projects/src/ProjectsService.ts @@ -11,7 +11,7 @@ import { computed, makeObservable } from 'mobx'; import { UserDataService, UserInfoResource } from '@cloudbeaver/core-authentication'; import { Dependency, injectable } from '@cloudbeaver/core-di'; import { Executor, ExecutorInterrupter, IExecutor, ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; -import { DataSynchronizationService, ServerEventId } from '@cloudbeaver/core-root'; +import { DataSynchronizationService, ServerConfigResource, ServerEventId } from '@cloudbeaver/core-root'; import { CachedMapAllKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; import { NavigationService } from '@cloudbeaver/core-ui'; import { isArraysEqual } from '@cloudbeaver/core-utils'; @@ -46,7 +46,7 @@ export class ProjectsService extends Dependency { get activeProjects(): ProjectInfo[] { let activeProjects: ProjectInfo[] = []; - if (activeProjects.length === 0 && this.activeProjectIds.length > 0) { + if (this.activeProjectIds.length > 0) { activeProjects = this.projectInfoResource .get(resourceKeyList(this.activeProjectIds)) .filter(Boolean) as ProjectInfo[]; @@ -79,6 +79,10 @@ export class ProjectsService extends Dependency { } get activeProjectIds(): string[] { + if (!this.serverConfigResource.distributed) { + return []; + } + return this.userProjectsSettings.activeProjectIds; } @@ -92,6 +96,7 @@ export class ProjectsService extends Dependency { readonly getActiveProjectTask: ISyncExecutor; constructor( + private readonly serverConfigResource: ServerConfigResource, private readonly projectInfoResource: ProjectInfoResource, private readonly userInfoResource: UserInfoResource, private readonly userDataService: UserDataService, diff --git a/webapp/packages/core-ui/src/Tabs/Tab/Tab.tsx b/webapp/packages/core-ui/src/Tabs/Tab/Tab.tsx index da8dd1f639..99929e594a 100644 --- a/webapp/packages/core-ui/src/Tabs/Tab/Tab.tsx +++ b/webapp/packages/core-ui/src/Tabs/Tab/Tab.tsx @@ -42,7 +42,7 @@ export const Tab = observer(function Tab({ const tab = useTab(tabId, onOpen, onClose, onClick); const info = tab.getInfo(); - const canClose = getComputed(() => !!onClose || tab.state.closable); + const canClose = getComputed(() => !!onClose || (tab.closable && tab.state.closable)); return styled(useStyles(BASE_TAB_STYLES, BASE_TAB_ACTION_STYLES, style))( diff --git a/webapp/packages/core-ui/src/Tabs/Tab/useTab.ts b/webapp/packages/core-ui/src/Tabs/Tab/useTab.ts index f2ccf04ee6..f0c6623394 100644 --- a/webapp/packages/core-ui/src/Tabs/Tab/useTab.ts +++ b/webapp/packages/core-ui/src/Tabs/Tab/useTab.ts @@ -52,6 +52,9 @@ export function useTab( get selected() { return this.state.state.selectedId === this.tabId; }, + get closable() { + return this.state.canClose(this.tabId); + }, getInfo() { return this.state.getTabInfo(this.tabId); }, @@ -68,6 +71,7 @@ export function useTab( }, }), { selected: computed, + closable: computed, state: observable.ref, tabId: observable.ref, }, { diff --git a/webapp/packages/core-ui/src/Tabs/TabsBootstrap.ts b/webapp/packages/core-ui/src/Tabs/TabsBootstrap.ts index db86e5bedf..984bb3eada 100644 --- a/webapp/packages/core-ui/src/Tabs/TabsBootstrap.ts +++ b/webapp/packages/core-ui/src/Tabs/TabsBootstrap.ts @@ -87,8 +87,14 @@ export class TabsBootstrap extends Bootstrap { this.menuService.addCreator({ isApplicable: context => { + const tab = context.tryGet(DATA_CONTEXT_TAB_ID); const state = context.tryGet(DATA_CONTEXT_TABS_CONTEXT); - return !!state?.enabledBaseActions && context.get(DATA_CONTEXT_MENU) === MENU_TAB; + return ( + !!tab + && !!state?.enabledBaseActions + && context.get(DATA_CONTEXT_MENU) === MENU_TAB + && state.canClose(tab) + ); }, getItems: (context, items) => [ ...items, diff --git a/webapp/packages/core-ui/src/Tabs/TabsContext.ts b/webapp/packages/core-ui/src/Tabs/TabsContext.ts index 0d77e323e5..afaf895baa 100644 --- a/webapp/packages/core-ui/src/Tabs/TabsContext.ts +++ b/webapp/packages/core-ui/src/Tabs/TabsContext.ts @@ -27,6 +27,7 @@ export interface ITabsContext> { closable: boolean; tabList?: string[]; enabledBaseActions?: boolean; + canClose: (tabId: string) => boolean; getTabInfo: (tabId: string) => ITabInfo | undefined; getTabState: (tabId: string, valueGetter?: MetadataValueGetter) => T; getLocalState: (tabId: string, valueGetter?: MetadataValueGetter) => T; diff --git a/webapp/packages/core-ui/src/Tabs/TabsState.tsx b/webapp/packages/core-ui/src/Tabs/TabsState.tsx index dd265270ce..440757011e 100644 --- a/webapp/packages/core-ui/src/Tabs/TabsState.tsx +++ b/webapp/packages/core-ui/src/Tabs/TabsState.tsx @@ -32,6 +32,7 @@ type Props> = ExtractContainerProps & React.PropsWith autoSelect?: boolean; tabList?: string[]; enabledBaseActions?: boolean; + canClose?: (tab: ITabData) => boolean; onChange?: (tab: ITabData) => void; onClose?: (tab: ITabData) => void; }>; @@ -50,6 +51,7 @@ export const TabsState = observer(function TabsState>({ enabledBaseActions, onChange: onOpen, onClose, + canClose, ...rest }: Props): React.ReactElement | null { const props = useMemo(() => rest as any as T, [...Object.values(rest)]); @@ -87,6 +89,7 @@ export const TabsState = observer(function TabsState>({ const dynamic = useObjectRef(() => ({ selectedId: selectedId || currentTabId, }), { + canClose, open: onOpen, close: onClose, props, @@ -176,8 +179,14 @@ export const TabsState = observer(function TabsState>({ }, [!isNull(state.selectedId) && !isUndefined(state.selectedId)]); const value = useObservableRef>(() => ({ + canClose(tabId) { + return dynamic.canClose?.({ + tabId, + props: dynamic.props, + }) ?? true; + }, getTabInfo(tabId: string) { - return dynamic.container?.getTabInfo(tabId); + return dynamic.container?.getTabInfo(tabId); }, getTabState(tabId: string, valueGetter?: MetadataValueGetter) { return dynamic.container?.getTabState( @@ -200,6 +209,10 @@ export const TabsState = observer(function TabsState>({ }); }, async close(tabId: string) { + if (!this.canClose(tabId)) { + return; + } + await closeExecutor.execute({ tabId, props: dynamic.props, diff --git a/webapp/packages/core-view/src/Action/Actions/ACTION_EDIT.ts b/webapp/packages/core-view/src/Action/Actions/ACTION_EDIT.ts new file mode 100644 index 0000000000..a1e757087b --- /dev/null +++ b/webapp/packages/core-view/src/Action/Actions/ACTION_EDIT.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { createAction } from '../createAction'; + +export const ACTION_EDIT = createAction('edit', { + label: 'core_view_action_edit_label', +}); diff --git a/webapp/packages/core-view/src/index.ts b/webapp/packages/core-view/src/index.ts index b0be10c7db..9c05e188bd 100644 --- a/webapp/packages/core-view/src/index.ts +++ b/webapp/packages/core-view/src/index.ts @@ -1,5 +1,6 @@ export * from './Action/Actions/ACTION_COLLAPSE_ALL'; export * from './Action/Actions/ACTION_DELETE'; +export * from './Action/Actions/ACTION_EDIT'; export * from './Action/Actions/ACTION_EXPORT'; export * from './Action/Actions/ACTION_FILTER'; export * from './Action/Actions/ACTION_LAYOUT'; diff --git a/webapp/packages/core-view/src/locales/en.ts b/webapp/packages/core-view/src/locales/en.ts index 95b34ebd2a..3a192c4ae1 100644 --- a/webapp/packages/core-view/src/locales/en.ts +++ b/webapp/packages/core-view/src/locales/en.ts @@ -3,4 +3,5 @@ export default [ ['core_view_zoom_in', 'Zoom In'], ['core_view_zoom_out', 'Zoom out'], ['core_view_action_new_folder', 'New folder'], + ['core_view_action_edit_label', 'Edit'], ]; diff --git a/webapp/packages/core-view/src/locales/it.ts b/webapp/packages/core-view/src/locales/it.ts index 95b34ebd2a..3a192c4ae1 100644 --- a/webapp/packages/core-view/src/locales/it.ts +++ b/webapp/packages/core-view/src/locales/it.ts @@ -3,4 +3,5 @@ export default [ ['core_view_zoom_in', 'Zoom In'], ['core_view_zoom_out', 'Zoom out'], ['core_view_action_new_folder', 'New folder'], + ['core_view_action_edit_label', 'Edit'], ]; diff --git a/webapp/packages/core-view/src/locales/ru.ts b/webapp/packages/core-view/src/locales/ru.ts index c8da73f25c..197566c821 100644 --- a/webapp/packages/core-view/src/locales/ru.ts +++ b/webapp/packages/core-view/src/locales/ru.ts @@ -3,4 +3,5 @@ export default [ ['core_view_zoom_in', 'Приблизить'], ['core_view_zoom_out', 'Отдалить'], ['core_view_action_new_folder', 'Создать папку'], + ['core_view_action_edit_label', 'Изменить'], ]; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts index 33e8dc35c1..1ae27f909e 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts @@ -19,7 +19,7 @@ import { ActionService, ACTION_SAVE, DATA_CONTEXT_MENU, MenuService } from '@clo import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; import { NavResourceNodeService, ResourceManagerService, getResourceKeyFromNodeId } from '@cloudbeaver/plugin-resource-manager'; import { ResourceManagerScriptsService, SaveScriptDialog, SCRIPTS_TYPE_ID } from '@cloudbeaver/plugin-resource-manager-scripts'; -import { DATA_CONTEXT_SQL_EDITOR_STATE, ESqlDataSourceFeatures, getSqlEditorName, ISqlDataSource, SqlDataSourceService, SqlEditorSettingsService, SQL_EDITOR_ACTIONS_MENU } from '@cloudbeaver/plugin-sql-editor'; +import { DATA_CONTEXT_SQL_EDITOR_STATE, ESqlDataSourceFeatures, getSqlEditorName, ISqlDataSource, SqlDataSourceService, SqlEditorSettingsService, MemorySqlDataSource, LocalStorageSqlDataSource, SQL_EDITOR_TOOLS_MENU } from '@cloudbeaver/plugin-sql-editor'; import { isSQLEditorTab, SqlEditorNavigatorService } from '@cloudbeaver/plugin-sql-editor-navigation-tab'; import { ResourceSqlDataSource } from './ResourceSqlDataSource'; @@ -66,7 +66,12 @@ export class PluginBootstrap extends Bootstrap { return false; } - return !(this.sqlDataSourceService.get(state.editorId) instanceof ResourceSqlDataSource); + const dataSource = this.sqlDataSourceService.get(state.editorId); + + return ( + dataSource instanceof MemorySqlDataSource + || dataSource instanceof LocalStorageSqlDataSource + ); } return false; @@ -182,7 +187,7 @@ export class PluginBootstrap extends Bootstrap { return ( this.resourceManagerService.enabled - && context.get(DATA_CONTEXT_MENU) === SQL_EDITOR_ACTIONS_MENU + && context.get(DATA_CONTEXT_MENU) === SQL_EDITOR_TOOLS_MENU && !!dataSource?.hasFeature(ESqlDataSourceFeatures.script) ); }, diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts index 73a86def9d..5e5b1d7990 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSource.ts @@ -89,14 +89,18 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { get features():ESqlDataSourceFeatures[] { if (this.isReadonly()) { - return [ESqlDataSourceFeatures.script]; + return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable]; } - return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.setName]; + return [ + ESqlDataSourceFeatures.script, + ESqlDataSourceFeatures.query, + ESqlDataSourceFeatures.executable, + ESqlDataSourceFeatures.setName, + ]; } private _script: string; - private saved: boolean; private actions?: IResourceActions; private info?: IResourceInfo; private lastAction?: () => Promise; @@ -114,7 +118,6 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { super(); this.state = state; this._script = ''; - this.saved = true; this.loaded = false; this.resourceUseKeyId = null; this.scheduler = new TaskScheduler(() => true); @@ -207,7 +210,6 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { this._script = script; super.setScript(script); - this.saved = false; if (previous !== script) { this.debouncedWrite(); @@ -347,6 +349,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { this.markUpdated(); this.loaded = true; super.setScript(this.script); + this.saved = true; } catch (exception: any) { this.exception = exception; } finally { @@ -377,6 +380,10 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { }); } + async save(): Promise { + await this.write(); + } + private async saveProperties() { await this.scheduler.schedule(undefined, async () => { if (!this.actions || !this.resourceKey) { diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts index e7c5e579ee..5c4fbc3de5 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts @@ -27,6 +27,7 @@ import { objectSchemaSetter, } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import { Executor, ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { objectNavNodeProvider, NodeManagerUtils, NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; @@ -61,7 +62,8 @@ export class SqlEditorTabService extends Bootstrap { private readonly navNodeInfoResource: NavNodeInfoResource, private readonly sqlDataSourceService: SqlDataSourceService, private readonly connectionsManagerService: ConnectionsManagerService, - private readonly containerResource: ContainerResource + private readonly containerResource: ContainerResource, + private readonly commonDialogService: CommonDialogService, ) { super(); @@ -498,10 +500,32 @@ export class SqlEditorTabService extends Bootstrap { private async handleCanTabClose(editorTab: ITab) { const canCloseTabs = await this.sqlResultTabsService.canCloseResultTabs(editorTab.handlerState); - if (canCloseTabs) { - const contexts = await this.onCanClose.execute(editorTab); - if (ExecutorInterrupter.isInterrupted(contexts)) { + if (!canCloseTabs) { + return false; + } + + const contexts = await this.onCanClose.execute(editorTab); + if (ExecutorInterrupter.isInterrupted(contexts)) { + return false; + } + + const dataSource = this.sqlDataSourceService.get(editorTab.handlerState.editorId); + + if (dataSource?.isSaved() === false) { + const result = await this.commonDialogService.open(ConfirmationDialog, { + title: 'plugin_sql_editor_navigation_tab_data_source_save_confirmation_title', + subTitle: dataSource.name ?? undefined, + message: 'plugin_sql_editor_navigation_tab_data_source_save_confirmation_message', + confirmActionText: 'ui_yes', + extraStatus: 'no', + }); + + if (result === DialogueStateResult.Rejected) { return false; + } else if (result === DialogueStateResult.Resolved) { + await dataSource.save(); + } else { + await dataSource.reset(); } } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/en.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/en.ts index 2f14005785..aa3d5ae2ce 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/en.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/en.ts @@ -2,4 +2,6 @@ export default [ ['plugin_sql_editor_navigation_tab_action_sql_editor_new', 'SQL'], ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip', 'Open SQL Editor'], ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip_context', 'Open SQL Editor for {arg:connection}'], + ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_title', 'Save changes'], + ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_message', 'Do you want to save changes?'], ]; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/it.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/it.ts index 2f14005785..aa3d5ae2ce 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/it.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/it.ts @@ -2,4 +2,6 @@ export default [ ['plugin_sql_editor_navigation_tab_action_sql_editor_new', 'SQL'], ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip', 'Open SQL Editor'], ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip_context', 'Open SQL Editor for {arg:connection}'], + ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_title', 'Save changes'], + ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_message', 'Do you want to save changes?'], ]; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/ru.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/ru.ts index b5ebf18034..bb2e8a0dea 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/ru.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/ru.ts @@ -2,4 +2,6 @@ export default [ ['plugin_sql_editor_navigation_tab_action_sql_editor_new', 'SQL'], ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip', 'Открыть SQL Редактор'], ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip_context', 'Открыть SQL Редактор для {arg:connection}'], + ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_title', 'Сохранить изменения'], + ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_message', 'Вы хотите сохранить изменения?'], ]; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/zh.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/zh.ts index 2f14005785..aa3d5ae2ce 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/zh.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/locales/zh.ts @@ -2,4 +2,6 @@ export default [ ['plugin_sql_editor_navigation_tab_action_sql_editor_new', 'SQL'], ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip', 'Open SQL Editor'], ['plugin_sql_editor_navigation_tab_action_sql_editor_new_tooltip_context', 'Open SQL Editor for {arg:connection}'], + ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_title', 'Save changes'], + ['plugin_sql_editor_navigation_tab_data_source_save_confirmation_message', 'Do you want to save changes?'], ]; diff --git a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts index 05844f812d..c992e4b365 100644 --- a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts @@ -22,6 +22,7 @@ import { KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT } from './actions/bindings/KEY_BI import { KEY_BINDING_SQL_EDITOR_FORMAT } from './actions/bindings/KEY_BINDING_SQL_EDITOR_FORMAT'; import { KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN } from './actions/bindings/KEY_BINDING_SQL_EDITOR_SHOW_EXECUTION_PLAN'; import { ScriptPreviewService } from './ScriptPreview/ScriptPreviewService'; +import { ESqlDataSourceFeatures } from './SqlDataSource/ESqlDataSourceFeatures'; import { DATA_CONTEXT_SQL_EDITOR_DATA } from './SqlEditor/DATA_CONTEXT_SQL_EDITOR_DATA'; @injectable() @@ -53,6 +54,29 @@ export class MenuBootstrap extends Bootstrap { return false; } + if ( + !sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.executable) + && [ + ACTION_SQL_EDITOR_EXECUTE, + ACTION_SQL_EDITOR_EXECUTE_NEW, + ACTION_SQL_EDITOR_EXECUTE_SCRIPT, + ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN, + ].includes(action) + ) { + return false; + } + + if ( + !sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.query) + && [ + ACTION_SQL_EDITOR_EXECUTE, + ACTION_SQL_EDITOR_EXECUTE_NEW, + ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN, + ].includes(action) + ) { + return false; + } + return ( [ ACTION_SQL_EDITOR_EXECUTE, @@ -86,7 +110,13 @@ export class MenuBootstrap extends Bootstrap { this.keyBindingService.addKeyBindingHandler({ id: 'sql-editor-execute-script', binding: KEY_BINDING_SQL_EDITOR_EXECUTE_SCRIPT, - isBindingApplicable: (contexts, action) => action === ACTION_SQL_EDITOR_EXECUTE_SCRIPT, + isBindingApplicable: (contexts, action) => { + const sqlEditorData = contexts.tryGet(DATA_CONTEXT_SQL_EDITOR_DATA); + return ( + action === ACTION_SQL_EDITOR_EXECUTE_SCRIPT + && sqlEditorData?.dataSource?.hasFeature(ESqlDataSourceFeatures.executable) === true + ); + }, handler: this.sqlEditorActionHandler.bind(this), }); diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts index 2ccfdde897..3e805dbb90 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts @@ -40,9 +40,10 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { } get features(): ESqlDataSourceFeatures[] { - return [ESqlDataSourceFeatures.script]; + return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable]; } + protected saved: boolean; readonly history: ISqlDataSourceHistory; readonly onUpdate: ISyncExecutor; readonly onSetScript: ISyncExecutor; @@ -57,6 +58,7 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { this.message = undefined; this.outdated = true; this.editing = true; + this.saved = true; this.history = new SqlDataSourceHistory(); this.onUpdate = new SyncExecutor(); this.onSetScript = new SyncExecutor(); @@ -83,17 +85,24 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { } setScript(script: string, source?: string): void { + this.saved = false; this.onSetScript.execute({ script, source }); } abstract canRename(name: string | null): boolean; + setName(name: string | null): void { this.onUpdate.execute(); } + setExecutionContext(executionContext?: IConnectionExecutionContextInfo | undefined): void { this.onUpdate.execute(); } + isSaved(): boolean { + return this.saved; + } + isError(): boolean { return isContainsException(this.exception); } @@ -146,6 +155,10 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { this.onUpdate.execute(); } + save(): Promise | void { } load(): Promise | void { } + reset(): Promise | void { + this.saved = true; + } dispose(): void | Promise { } } \ No newline at end of file diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ESqlDataSourceFeatures.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ESqlDataSourceFeatures.ts index 4721be2524..55e5342464 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ESqlDataSourceFeatures.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ESqlDataSourceFeatures.ts @@ -8,6 +8,8 @@ export enum ESqlDataSourceFeatures { script, + query, + executable, setName, setProject } \ No newline at end of file diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts index d7aeada71c..34b2299ee1 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/ISqlDataSource.ts @@ -40,6 +40,7 @@ export interface ISqlDataSource extends ILoadableState { readonly features: ESqlDataSourceFeatures[]; readonly history: ISqlDataSourceHistory; + isSaved(): boolean; isReadonly(): boolean; isEditing(): boolean; isOutdated(): boolean; @@ -52,6 +53,8 @@ export interface ISqlDataSource extends ILoadableState { setScript(script: string, source?: string): void; setEditing(state: boolean): void; setExecutionContext(executionContext?: IConnectionExecutionContextInfo): void; + save(): Promise | void; load(): Promise | void; + reset(): Promise | void; dispose(): Promise | void; } \ No newline at end of file diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSource.ts index f95ebbd456..c42aa23030 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSource.ts @@ -30,7 +30,12 @@ export class LocalStorageSqlDataSource extends BaseSqlDataSource { } get features(): ESqlDataSourceFeatures[] { - return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.setName]; + return [ + ESqlDataSourceFeatures.script, + ESqlDataSourceFeatures.query, + ESqlDataSourceFeatures.executable, + ESqlDataSourceFeatures.setName, + ]; } private readonly state: ILocalStorageSqlDataSourceState; @@ -47,6 +52,10 @@ export class LocalStorageSqlDataSource extends BaseSqlDataSource { }); } + isSaved(): boolean { + return true; + } + isReadonly(): boolean { return false; } diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/MemorySqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/MemorySqlDataSource.ts index 98f1f06c86..54985bb179 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/MemorySqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/MemorySqlDataSource.ts @@ -29,7 +29,12 @@ export class MemorySqlDataSource extends BaseSqlDataSource { } get features(): ESqlDataSourceFeatures[] { - return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.setName]; + return [ + ESqlDataSourceFeatures.script, + ESqlDataSourceFeatures.query, + ESqlDataSourceFeatures.executable, + ESqlDataSourceFeatures.setName, + ]; } private _name: string | null; @@ -55,6 +60,10 @@ export class MemorySqlDataSource extends BaseSqlDataSource { }); } + isSaved(): boolean { + return true; + } + isReadonly(): boolean { return false; } diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_ACTIONS_MENU_STYLES.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_ACTIONS_MENU_STYLES.ts index fb79b6d885..354de2db6e 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_ACTIONS_MENU_STYLES.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_ACTIONS_MENU_STYLES.ts @@ -44,9 +44,7 @@ export const SQL_EDITOR_ACTIONS_MENU_STYLES = css` } & menu-bar-item-label { - display: block; - text-transform: uppercase; - font-weight: 700; + display: none; } & IconOrImage + menu-bar-item-label, & Loader + menu-bar-item-label { diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_TOOLS_MENU.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_TOOLS_MENU.ts new file mode 100644 index 0000000000..ea1763b47b --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SQL_EDITOR_TOOLS_MENU.ts @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { createMenu } from '@cloudbeaver/core-view'; + +export const SQL_EDITOR_TOOLS_MENU = createMenu('sql-editor-tools', ''); \ No newline at end of file diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx index 5a03ed5517..5466b80e07 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditor.tsx @@ -20,6 +20,7 @@ import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures' import { ISqlEditorModeProps, SqlEditorModeService } from '../SqlEditorModeService'; import { DATA_CONTEXT_SQL_EDITOR_DATA } from './DATA_CONTEXT_SQL_EDITOR_DATA'; import type { ISqlEditorProps } from './ISqlEditorProps'; +import { SqlEditorActionsMenu } from './SqlEditorActionsMenu'; import { SqlEditorTools } from './SqlEditorTools'; import { useSqlEditor } from './useSqlEditor'; @@ -53,7 +54,7 @@ const styles = css` user-select: none; &:empty { - display: none; + width: initial; } } @@ -124,10 +125,16 @@ export const SqlEditor = observer(function SqlEditor({ state, c const displayedEditors = getComputed(() => sqlEditorModeService.tabsContainer.getDisplayed({ state, data }).length); useEffect(() => { - split.fixate('maximize', displayedEditors === 0); + if (displayedEditors === 0) { + split.fixate('maximize', true); + } else if (split.state.disable) { + split.fixate('resize', false); + split.state.setSize(-1); + } }); - const isScript = data.dataSource?.hasFeature(ESqlDataSourceFeatures.script); + const isQuery = data.dataSource?.hasFeature(ESqlDataSourceFeatures.query); + const isExecutable = data.dataSource?.hasFeature(ESqlDataSourceFeatures.executable); return styled(styles, BASE_TAB_STYLES, VERTICAL_ROTATED_TAB_STYLES, tabStyles)( (function SqlEditor({ state, c - {isScript && ( + {isExecutable && ( <> - - + {isQuery && ( + <> + + + + )} - {data.dialect?.supportsExplainExecutionPlan && ( + {isQuery && data.dialect?.supportsExplainExecutionPlan && ( - )} + - {!isActiveSegmentMode && !data.readonly && ( + {!isActiveSegmentMode && ( - + diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorToolsMenu.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorToolsMenu.tsx new file mode 100644 index 0000000000..0289d63a08 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorToolsMenu.tsx @@ -0,0 +1,33 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { observer } from 'mobx-react-lite'; + +import type { ComponentStyle } from '@cloudbeaver/core-theming'; +import { MenuBar } from '@cloudbeaver/core-ui'; +import { IDataContext, useMenu } from '@cloudbeaver/core-view'; + +import { DATA_CONTEXT_SQL_EDITOR_STATE } from '../DATA_CONTEXT_SQL_EDITOR_STATE'; +import type { ISqlEditorTabState } from '../ISqlEditorTabState'; +import { SQL_EDITOR_ACTIONS_MENU_STYLES } from './SQL_EDITOR_ACTIONS_MENU_STYLES'; +import { SQL_EDITOR_TOOLS_MENU } from './SQL_EDITOR_TOOLS_MENU'; + +interface Props { + state: ISqlEditorTabState; + context?: IDataContext; + style?: ComponentStyle; +} + +export const SqlEditorToolsMenu = observer(function SqlEditorToolsMenu({ state, context, style }) { + const menu = useMenu({ menu: SQL_EDITOR_TOOLS_MENU, context }); + menu.context.set(DATA_CONTEXT_SQL_EDITOR_STATE, state); + + return ( + + ); +}); \ No newline at end of file diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts index a84a487075..aff1bb1f7b 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts @@ -268,6 +268,12 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, async executeQuery(): Promise { + const isQuery = this.dataSource?.hasFeature(ESqlDataSourceFeatures.query); + const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable); + + if (!isQuery || !isExecutable) { + return; + } const query = this.getSubQuery(); await this.executeQueryAction( @@ -296,6 +302,12 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, async executeQueryNewTab(): Promise { + const isQuery = this.dataSource?.hasFeature(ESqlDataSourceFeatures.query); + const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable); + + if (!isQuery || !isExecutable) { + return; + } const query = this.getSubQuery(); await this.executeQueryAction( @@ -309,7 +321,10 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, async showExecutionPlan(): Promise { - if (!this.dialect?.supportsExplainExecutionPlan) { + const isQuery = this.dataSource?.hasFeature(ESqlDataSourceFeatures.query); + const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable); + + if (!isQuery || !isExecutable || !this.dialect?.supportsExplainExecutionPlan) { return; } @@ -329,7 +344,9 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData { }, async executeScript(): Promise { - if (this.isDisabled || this.isScriptEmpty) { + const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable); + + if (!isExecutable || this.isDisabled || this.isScriptEmpty) { return; } diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx index 9201c95f22..f3ad558be4 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx @@ -43,7 +43,7 @@ const styles = css` display: flex; overflow: auto; } - TabList:not([|script]) tab-outer:only-child { + TabList:not([|executable]) tab-outer:only-child { display: none; } TextPlaceholder { @@ -91,11 +91,22 @@ export const SqlResultTabs = observer(function SqlDataResult({ state, onT } } + function handleCanClose(tab: ITabData): boolean { + const resultTab = state.resultTabs.find(tabState => tabState.tabId === tab.tabId); + + if (resultTab) { + const group = state.resultGroups.find(group => group.groupId === resultTab.groupId)!; + + return dataSource?.databaseModels.some(model => model.id === group.modelId) !== true; + } + return true; + } + if (!state.tabs.length) { return styled(style)({translate(dataSource?.emptyPlaceholder ?? 'sql_editor_placeholder')}); } - const script = dataSource?.hasFeature(ESqlDataSourceFeatures.script); + const executable = dataSource?.hasFeature(ESqlDataSourceFeatures.executable); const tabList = orderedTabs.map(tab => tab.id); return styled(style)( @@ -103,11 +114,12 @@ export const SqlResultTabs = observer(function SqlDataResult({ state, onT - + {orderedTabs.map(result => (