diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java index 3605fab65b..72e538dcf6 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java @@ -70,8 +70,10 @@ public abstract class BaseWebApplication extends BaseApplicationImpl implements } @Override - public RMController createResourceController(@NotNull SMCredentialsProvider credentialsProvider, - @NotNull DBPWorkspace workspace) throws DBException { + public RMController createResourceController( + @NotNull SMCredentialsProvider credentialsProvider, + @NotNull DBPWorkspace workspace + ) throws DBException { throw new IllegalStateException("Resource controller is not supported by " + getClass().getSimpleName()); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java index 319f5debbb..0b06478792 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/WebApplication.java @@ -53,7 +53,8 @@ public interface WebApplication extends DBPApplication { WebProjectImpl createProjectImpl( @NotNull WebSession webSession, @NotNull RMProject project, - @NotNull DataSourceFilter dataSourceFilter); + @NotNull DataSourceFilter dataSourceFilter + ); SMController createSecurityController(@NotNull SMCredentialsProvider credentialsProvider) throws DBException; @@ -61,8 +62,10 @@ public interface WebApplication extends DBPApplication { DBSSecretController getSecretController(@NotNull SMCredentialsProvider credentialsProvider) throws DBException; - RMController createResourceController(@NotNull SMCredentialsProvider credentialsProvider, - @NotNull DBPWorkspace workspace) throws DBException; + RMController createResourceController( + @NotNull SMCredentialsProvider credentialsProvider, + @NotNull DBPWorkspace workspace + ) throws DBException; DBFileController createFileController(@NotNull SMCredentialsProvider credentialsProvider); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/RMUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/RMUtils.java index 29bf9d4aff..d2e38b973e 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/RMUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/RMUtils.java @@ -64,7 +64,7 @@ public class RMUtils { * @return path to the project based on the name of projects and prefixes that it contains */ @NotNull - public static Path getProjectPathByName(@NotNull String projectId) throws DBException { + public static Path getProjectPathById(@NotNull String projectId) throws DBException { int divPos = projectId.indexOf("_"); if (divPos <= 0) { throw new DBException("Bad project ID"); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java index ad59077fb6..61825cfc6d 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java @@ -71,7 +71,6 @@ public class LocalResourceController implements RMController { private static final String FILE_REGEX = "(?U)[\\w.$()@/\\\\ -]+"; private static final String PROJECT_REGEX = "(?U)[\\w.$()@ -]+"; // slash not allowed in project name - public static final String DEFAULT_CHANGE_ID = "0"; private final DBPWorkspace workspace; @@ -82,8 +81,8 @@ public class LocalResourceController implements RMController { private final Path sharedProjectsPath; private final String globalProjectName; private Supplier smControllerSupplier; - private final RMFileLockController lockController; - private final List fileHandlers; + protected final RMFileLockController lockController; + protected final List fileHandlers; private final Map projectRegistries = new LinkedHashMap<>(); @@ -121,7 +120,7 @@ public class LocalResourceController implements RMController { return userId == null ? null : this.userProjectsPath.resolve(userId); } - private BaseWebProjectImpl getWebProject(String projectId, boolean refresh) throws DBException { + protected BaseWebProjectImpl getWebProject(String projectId, boolean refresh) throws DBException { synchronized (projectRegistries) { BaseWebProjectImpl project = projectRegistries.get(projectId); if (project == null || refresh) { @@ -765,7 +764,7 @@ public class LocalResourceController implements RMController { try { Files.write(targetPath, data); } catch (IOException e) { - throw new DBException("Error reading resource '" + resourcePath + "'", e); + throw new DBException("Error writing resource '" + resourcePath + "'", e); } return null; }); @@ -777,7 +776,7 @@ public class LocalResourceController implements RMController { return DEFAULT_CHANGE_ID; } - private void createFolder(Path targetPath) throws DBException { + protected void createFolder(Path targetPath) throws DBException { if (!Files.exists(targetPath)) { try { Files.createDirectories(targetPath); @@ -924,34 +923,31 @@ public class LocalResourceController implements RMController { } private T doProjectOperation(String projectId, RMFileOperation operation) throws DBException { - var projectPath = getProjectPath(projectId); for (RMFileOperationHandler fileHandler : fileHandlers) { - fileHandler.projectOpened(projectPath); + fileHandler.projectOpened(projectId); } return operation.doOperation(); } private T doFileReadOperation(String projectId, Path file, RMFileOperation operation) throws DBException { - var projectPath = getProjectPath(projectId); for (RMFileOperationHandler fileHandler : fileHandlers) { - fileHandler.beforeFileRead(projectPath, file); + fileHandler.beforeFileRead(projectId, file); } return operation.doOperation(); } private T doFileWriteOperation(String projectId, Path file, RMFileOperation operation) throws DBException { - var projectPath = getProjectPath(projectId); for (RMFileOperationHandler fileHandler : fileHandlers) { - fileHandler.beforeFileChange(projectPath, file); + fileHandler.beforeFileChange(projectId, file); } var result = operation.doOperation(); for (RMFileOperationHandler fileHandler : fileHandlers) { - fileHandler.afterFileChange(projectPath, file, credentialsProvider.getActiveUserCredentials()); + fileHandler.afterFileChange(projectId, file, credentialsProvider.getActiveUserCredentials()); } return result; } - private Path getProjectPath(String projectId) throws DBException { + protected Path getProjectPath(String projectId) throws DBException { RMProjectName project = parseProjectName(projectId); RMProjectType type = project.getType(); String projectName = project.getName(); @@ -1179,9 +1175,16 @@ public class LocalResourceController implements RMController { return RMProjectType.GLOBAL.getPrefix().equals(rmProjectName.getPrefix()); } - public static boolean isPrivateProject(String projectId, String userId) { + public static boolean isPrivateProject(String projectId) { + RMProjectName rmProjectName = parseProjectNameUnsafe(projectId); + return RMProjectType.USER.getPrefix().equals(rmProjectName.getPrefix()); + } + + public static boolean isProjectOwner(String projectId, String userId) { RMProjectName rmProjectName = parseProjectNameUnsafe(projectId); return RMProjectType.USER.getPrefix().equals(rmProjectName.getPrefix()) && rmProjectName.name.equals(userId); } + + } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandler.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandler.java index dfbffff91f..9f01681214 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandler.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/RMFileOperationHandler.java @@ -25,23 +25,23 @@ import java.nio.file.Path; public interface RMFileOperationHandler { - void projectOpened(@NotNull Path projectPath) throws DBException; + void projectOpened(@NotNull String projectId) throws DBException; /** * Executed before reading file */ - void beforeFileRead(@NotNull Path projectPath, @NotNull Path filePath) throws DBException; + void beforeFileRead(@NotNull String projectId, @NotNull Path filePath) throws DBException; /** * Executed before file modification */ - void beforeFileChange(@NotNull Path projectPath, @NotNull Path filePath) throws DBException; + void beforeFileChange(@NotNull String projectId, @NotNull Path filePath) throws DBException; /** * Executed after file modification */ void afterFileChange( - @NotNull Path projectPath, + @NotNull String projectId, @NotNull Path filePath, @Nullable SMCredentials smCredentials ) throws DBException; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java index 7207edcb4f..22f38d5284 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/lock/RMFileLockController.java @@ -18,6 +18,7 @@ package io.cloudbeaver.model.rm.lock; import com.google.gson.Gson; import io.cloudbeaver.model.app.WebApplication; +import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; @@ -71,7 +72,8 @@ public class RMFileLockController { * @param operationName - executed operation name * @return - lock */ - public RMLock lockProject(String projectId, String operationName) throws DBException { + @NotNull + public RMLock lockProject(@NotNull String projectId,@NotNull String operationName) throws DBException { synchronized (RMFileLockController.class) { try { createLockFolderIfNeeded(); @@ -91,6 +93,24 @@ public class RMFileLockController { } } + /** + * if the project is already locked, the operation will be executed as a child of the first lock, + * otherwise it creates its own lock. + * + * @param projectId - project to be locked + * @param operationName - executed operation name + * @return - lock + */ + @Nullable + public RMLock lockIfNotLocked(@NotNull String projectId, @NotNull String operationName) throws DBException { + synchronized (RMFileLockController.class) { + if (isProjectLocked(projectId)) { + return null; + } + return lockProject(projectId, operationName); + } + } + /** * Check that project locked */ diff --git a/webapp/packages/core-cli/bin/dependency-graph.js b/webapp/packages/core-cli/bin/dependency-graph.js old mode 100644 new mode 100755 diff --git a/webapp/packages/core-root/src/ServerConfigResource.ts b/webapp/packages/core-root/src/ServerConfigResource.ts index d479fbfb94..34325e38a7 100644 --- a/webapp/packages/core-root/src/ServerConfigResource.ts +++ b/webapp/packages/core-root/src/ServerConfigResource.ts @@ -17,6 +17,8 @@ import { DataSynchronizationQueue } from './DataSynchronization/DataSynchronizat import { DataSynchronizationService } from './DataSynchronization/DataSynchronizationService'; import { ServerConfigEventHandler } from './ServerConfigEventHandler'; +export const FEATURE_GIT_ID = 'git'; + export type ServerConfig = Omit; @injectable() diff --git a/webapp/packages/core-view/src/Action/KeyBinding/Bindings/KEY_BINDING_SAVE.ts b/webapp/packages/core-view/src/Action/KeyBinding/Bindings/KEY_BINDING_SAVE.ts new file mode 100644 index 0000000000..9e6a31ab50 --- /dev/null +++ b/webapp/packages/core-view/src/Action/KeyBinding/Bindings/KEY_BINDING_SAVE.ts @@ -0,0 +1,16 @@ +/* + * 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 { createKeyBinding } from '../createKeyBinding'; + +export const KEY_BINDING_SAVE = createKeyBinding({ + id: 'save', + keysWin: ['ctrl+s'], + keysMac: ['cmd+s'], + preventDefault: true, +}); diff --git a/webapp/packages/core-view/src/index.ts b/webapp/packages/core-view/src/index.ts index 3eb8afa1e8..62b84b564a 100644 --- a/webapp/packages/core-view/src/index.ts +++ b/webapp/packages/core-view/src/index.ts @@ -18,6 +18,7 @@ export * from './Action/Actions/ACTION_ZOOM_OUT'; export * from './Action/KeyBinding/Bindings/KEY_BINDING_OPEN_IN_TAB'; export * from './Action/KeyBinding/Bindings/KEY_BINDING_REDO'; export * from './Action/KeyBinding/Bindings/KEY_BINDING_UNDO'; +export * from './Action/KeyBinding/Bindings/KEY_BINDING_SAVE'; export * from './Action/KeyBinding/KeyBindingService'; export * from './Action/KeyBinding/createKeyBinding'; export * from './Action/ActionService'; diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ACTION_SAVE_AS_SCRIPT.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ACTION_SAVE_AS_SCRIPT.ts new file mode 100644 index 0000000000..f7f5696b2b --- /dev/null +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ACTION_SAVE_AS_SCRIPT.ts @@ -0,0 +1,15 @@ +/* + * 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 '@cloudbeaver/core-view'; + +export const ACTION_SAVE_AS_SCRIPT = createAction('save-as-script', { + label: 'plugin_sql_editor_navigation_tab_resource_save_script_title', + tooltip: 'plugin_sql_editor_navigation_tab_resource_save_script_title', + icon: '/icons/sql_script_sm.svg', +}); 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 98810934d9..9aee60e3e1 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 @@ -14,7 +14,7 @@ import { isResourceOfType, ProjectInfoResource, ProjectsService } from '@cloudbe import { NAV_NODE_TYPE_RM_RESOURCE, ResourceManagerResource, RESOURCES_NODE_PATH } from '@cloudbeaver/core-resource-manager'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; import { createPath, getPathName } from '@cloudbeaver/core-utils'; -import { ACTION_SAVE, ActionService, DATA_CONTEXT_MENU, MenuService } from '@cloudbeaver/core-view'; +import { ACTION_SAVE, ActionService, DATA_CONTEXT_MENU, KEY_BINDING_SAVE, KeyBindingService, MenuService } from '@cloudbeaver/core-view'; import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; import { getResourceKeyFromNodeId, NavResourceNodeService } from '@cloudbeaver/plugin-navigation-tree-rm'; import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager'; @@ -28,12 +28,14 @@ import { MemorySqlDataSource, SQL_EDITOR_TOOLS_MENU, SqlDataSourceService, + SqlEditorService, SqlEditorSettingsService, } from '@cloudbeaver/plugin-sql-editor'; import { isSQLEditorTab, SqlEditorNavigatorService } from '@cloudbeaver/plugin-sql-editor-navigation-tab'; import { ResourceSqlDataSource } from './ResourceSqlDataSource'; import { SqlEditorTabResourceService } from './SqlEditorTabResourceService'; +import { ACTION_SAVE_AS_SCRIPT } from './ACTION_SAVE_AS_SCRIPT'; @injectable() export class PluginBootstrap extends Bootstrap { @@ -53,8 +55,10 @@ export class PluginBootstrap extends Bootstrap { private readonly menuService: MenuService, private readonly sqlDataSourceService: SqlDataSourceService, private readonly sqlEditorSettingsService: SqlEditorSettingsService, + private readonly sqlEditorService: SqlEditorService, private readonly resourceManagerResource: ResourceManagerResource, private readonly resourceManagerScriptsService: ResourceManagerScriptsService, + private readonly keyBindingService: KeyBindingService, ) { super(); } @@ -66,7 +70,7 @@ export class PluginBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'scripts-base-handler', isActionApplicable: (context, action): boolean => { - if (action === ACTION_SAVE) { + if (action === ACTION_SAVE_AS_SCRIPT || action === ACTION_SAVE) { const state = context.tryGet(DATA_CONTEXT_SQL_EDITOR_STATE); if (!state) { @@ -79,7 +83,20 @@ export class PluginBootstrap extends Bootstrap { const dataSource = this.sqlDataSourceService.get(state.editorId); - return dataSource instanceof MemorySqlDataSource || dataSource instanceof LocalStorageSqlDataSource; + if (action === ACTION_SAVE_AS_SCRIPT) { + return dataSource instanceof MemorySqlDataSource || dataSource instanceof LocalStorageSqlDataSource; + } + + if (action === ACTION_SAVE) { + return dataSource instanceof ResourceSqlDataSource; + } + } + + return false; + }, + isHidden: (context, action) => { + if (action === ACTION_SAVE) { + return this.sqlEditorService.autoSave; } return false; @@ -93,7 +110,7 @@ export class PluginBootstrap extends Bootstrap { return; } - if (action === ACTION_SAVE) { + if (action === ACTION_SAVE_AS_SCRIPT) { let projectId = dataSource.executionContext?.projectId ?? null; await this.projectInfoResource.load(CachedMapAllKey); const name = getSqlEditorName(state, dataSource); @@ -157,14 +174,40 @@ export class PluginBootstrap extends Bootstrap { } } } + + if (action === ACTION_SAVE) { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const source = this.sqlDataSourceService.get(state.editorId) as ResourceSqlDataSource | undefined; + + 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) as ResourceSqlDataSource | undefined; + + if (!source) { + return true; + } + + return source.isLoading() || source.isSaved(); + } + + return false; }, getActionInfo: (context, action) => { - if (action === ACTION_SAVE) { + if (action === ACTION_SAVE_AS_SCRIPT || action === ACTION_SAVE) { return { ...action.info, label: '', }; } + return action.info; }, }); @@ -176,6 +219,7 @@ export class PluginBootstrap extends Bootstrap { if (!state) { return false; } + const dataSource = this.sqlDataSourceService.get(state.editorId); return ( @@ -184,11 +228,27 @@ export class PluginBootstrap extends Bootstrap { !!dataSource?.hasFeature(ESqlDataSourceFeatures.script) ); }, - getItems: (context, items) => [...items, ACTION_SAVE], + getItems: (context, items) => [...items, ACTION_SAVE_AS_SCRIPT, ACTION_SAVE], + }); + + this.keyBindingService.addKeyBindingHandler({ + id: 'script-save', + binding: KEY_BINDING_SAVE, + isBindingApplicable: (context, action) => action === ACTION_SAVE, + handler: async context => { + const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); + const source = this.sqlDataSourceService.get(state.editorId) as ResourceSqlDataSource | undefined; + + if (!source) { + return; + } + + await source.save(); + }, }); } - load(): void | Promise {} + load(): void | Promise { } private canOpenHandler(data: INodeNavigationData, contexts: IExecutionContextProvider): void { const nodeInfo = contexts.getContext(this.navNodeManagerService.navigationNavNodeContext); 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 85291d6de2..a790197938 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 @@ -19,7 +19,7 @@ import { getRmResourceKey, ResourceManagerResource } from '@cloudbeaver/core-res import { isResourceAlias, ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; import { createPath, debounce, getPathName, getPathParent, isArraysEqual, isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils'; import { SCRIPTS_TYPE_ID } from '@cloudbeaver/plugin-resource-manager-scripts'; -import { BaseSqlDataSource, ESqlDataSourceFeatures } from '@cloudbeaver/plugin-sql-editor'; +import { BaseSqlDataSource, ESqlDataSourceFeatures, SqlEditorService } from '@cloudbeaver/plugin-sql-editor'; import type { IResourceSqlDataSourceState } from './IResourceSqlDataSourceState'; @@ -106,6 +106,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { private readonly projectInfoResource: ProjectInfoResource, private readonly connectionInfoResource: ConnectionInfoResource, private readonly resourceManagerResource: ResourceManagerResource, + private readonly sqlEditorService: SqlEditorService, state: IResourceSqlDataSourceState, ) { super(); @@ -116,6 +117,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { this.resourceUseKeyId = null; this.scheduler = new TaskScheduler(() => true); this.debouncedWrite = debounce(this.debouncedWrite.bind(this), VALUE_SYNC_DELAY); + this.debouncedSaveProperties = debounce(this.debouncedSaveProperties.bind(this), VALUE_SYNC_DELAY); this.syncResource = this.syncResource.bind(this); resourceManagerResource.onDataOutdated.addHandler(this.syncResource); @@ -200,7 +202,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { this._script = script; super.setScript(script); - if (previous !== script) { + if (this.sqlEditorService.autoSave && previous !== script) { this.debouncedWrite(); } } @@ -264,7 +266,9 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { this.state.executionContext = toJS(executionContext); super.setExecutionContext(executionContext); - this.saveProperties(); + if (this.sqlEditorService.autoSave) { + this.debouncedSaveProperties(); + } } } @@ -351,6 +355,7 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { async save(): Promise { await this.write(); + await this.saveProperties(); } private async saveProperties() { @@ -385,4 +390,8 @@ export class ResourceSqlDataSource extends BaseSqlDataSource { private debouncedWrite() { this.write(); } + + private debouncedSaveProperties() { + this.saveProperties(); + } } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts index 5bc7f65def..6fe1fa7e56 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts @@ -21,13 +21,12 @@ import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; import { NavResourceNodeService } from '@cloudbeaver/plugin-navigation-tree-rm'; import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager'; import { ResourceManagerScriptsService } from '@cloudbeaver/plugin-resource-manager-scripts'; -import { getSqlEditorName, SqlDataSourceService } from '@cloudbeaver/plugin-sql-editor'; +import { getSqlEditorName, SqlDataSourceService, SqlEditorService } from '@cloudbeaver/plugin-sql-editor'; import { SqlEditorTabService } from '@cloudbeaver/plugin-sql-editor-navigation-tab'; import type { IResourceSqlDataSourceState } from './IResourceSqlDataSourceState'; import { ResourceSqlDataSource } from './ResourceSqlDataSource'; import { SqlEditorTabResourceService } from './SqlEditorTabResourceService'; - const RESOURCE_TAB_STATE = 'sql_editor_resource_tab_state'; const SYNC_DELAY = 5 * 60 * 1000; @@ -50,6 +49,7 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { private readonly projectInfoResource: ProjectInfoResource, private readonly windowEventsService: WindowEventsService, private readonly sqlEditorTabResourceService: SqlEditorTabResourceService, + private readonly sqlEditorService: SqlEditorService, localStorageSaveService: LocalStorageSaveService, ) { super(); @@ -95,6 +95,7 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { this.projectInfoResource, this.connectionInfoResource, this.resourceManagerResource, + this.sqlEditorService, this.createState(editorId), ); @@ -169,7 +170,7 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { }); } - load(): void | Promise {} + load(): void | Promise { } private createState(editorId: string, resourceKey?: string): IResourceSqlDataSourceState { let state = this.dataSourceStateState.get(editorId); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/en.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/en.ts index c8f3675c19..d32911cf65 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/en.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/en.ts @@ -1,4 +1,5 @@ export default [ + ['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Save as script'], ['plugin_sql_editor_navigation_tab_script_state_renaming', 'Renaming script...'], ['plugin_sql_editor_navigation_tab_script_state_reading', 'Reading script...'], ['plugin_sql_editor_navigation_tab_script_state_saving', 'Saving script...'], diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/it.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/it.ts index c8f3675c19..d32911cf65 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/it.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/it.ts @@ -1,4 +1,5 @@ export default [ + ['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Save as script'], ['plugin_sql_editor_navigation_tab_script_state_renaming', 'Renaming script...'], ['plugin_sql_editor_navigation_tab_script_state_reading', 'Reading script...'], ['plugin_sql_editor_navigation_tab_script_state_saving', 'Saving script...'], diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/ru.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/ru.ts index d079876823..8e20e5cff1 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/ru.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/ru.ts @@ -1,4 +1,5 @@ export default [ + ['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Сохранить как скрипт'], ['plugin_sql_editor_navigation_tab_script_state_renaming', 'Переименовывание скрипта...'], ['plugin_sql_editor_navigation_tab_script_state_reading', 'Чтение скрипта...'], ['plugin_sql_editor_navigation_tab_script_state_saving', 'Сохранение скрипта...'], diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/zh.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/zh.ts index a92d3bd1af..39c633324c 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/zh.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/locales/zh.ts @@ -1,4 +1,5 @@ export default [ + ['plugin_sql_editor_navigation_tab_resource_save_script_title', 'Save as script'], ['plugin_sql_editor_navigation_tab_script_state_renaming', 'Renaming script...'], ['plugin_sql_editor_navigation_tab_script_state_reading', 'Reading script...'], ['plugin_sql_editor_navigation_tab_script_state_saving', 'Saving script...'], diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts index 1410fc375f..be04a6ec5d 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/BaseSqlDataSource.ts @@ -76,12 +76,13 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { this.history.onNavigate.addHandler(value => this.setScript(value, SOURCE_HISTORY)); - makeObservable(this, { + makeObservable(this, { databaseModels: observable.ref, exception: observable.ref, outdated: observable.ref, message: observable.ref, editing: observable.ref, + saved: observable.ref, }); } @@ -97,6 +98,7 @@ export abstract class BaseSqlDataSource implements ISqlDataSource { } setExecutionContext(executionContext?: IConnectionExecutionContextInfo | undefined): void { + this.saved = false; this.onUpdate.execute(); } diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts index 70793a6744..ef8a1b53d4 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts @@ -23,11 +23,13 @@ import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import { CachedMapAllKey, GraphQLService, SqlCompletionProposal, SqlScriptInfoFragment } from '@cloudbeaver/core-sdk'; +import { FEATURE_GIT_ID, ServerConfigResource } from '@cloudbeaver/core-root'; import { getSqlEditorName } from './getSqlEditorName'; import type { ISqlEditorTabState } from './ISqlEditorTabState'; import { ESqlDataSourceFeatures } from './SqlDataSource/ESqlDataSourceFeatures'; import { SqlDataSourceService } from './SqlDataSource/SqlDataSourceService'; +import { SqlEditorSettingsService } from './SqlEditorSettingsService'; export type SQLProposal = SqlCompletionProposal; @@ -39,6 +41,10 @@ export interface IQueryChangeData { @injectable() export class SqlEditorService { + get autoSave() { + return this.sqlEditorSettingsService.settings.getValue('autoSave') && !this.serverConfigResource.isFeatureEnabled(FEATURE_GIT_ID, true); + } + readonly onQueryChange: ISyncExecutor; constructor( @@ -49,6 +55,8 @@ export class SqlEditorService { private readonly connectionExecutionContextResource: ConnectionExecutionContextResource, private readonly connectionInfoResource: ConnectionInfoResource, private readonly sqlDataSourceService: SqlDataSourceService, + private readonly sqlEditorSettingsService: SqlEditorSettingsService, + private readonly serverConfigResource: ServerConfigResource, ) { this.onQueryChange = new SyncExecutor(); } diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts index 32c4a99820..f19b82a86c 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts @@ -11,6 +11,7 @@ import { PluginManagerService, PluginSettings } from '@cloudbeaver/core-plugin'; const defaultSettings = { maxFileSize: 10 * 1024, // kilobyte disabled: false, + autoSave: true, }; export type SqlEditorSettings = typeof defaultSettings; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorView.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorView.ts index 8494880bcb..58f8bb152e 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorView.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorView.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ import { injectable } from '@cloudbeaver/core-di'; -import { ACTION_REDO, ACTION_UNDO, IActiveView, View } from '@cloudbeaver/core-view'; +import { ACTION_REDO, ACTION_SAVE, ACTION_UNDO, IActiveView, View } from '@cloudbeaver/core-view'; import { ITab, NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; import { ACTION_SQL_EDITOR_EXECUTE } from './actions/ACTION_SQL_EDITOR_EXECUTE'; @@ -27,6 +27,7 @@ export class SqlEditorView extends View { ACTION_UNDO, ACTION_REDO, ACTION_SQL_EDITOR_SHOW_EXECUTION_PLAN, + ACTION_SAVE, ); }