diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index b51bb1cd2f..d5ab8ee8e8 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -44,6 +44,7 @@ type SQLContextInfo { id: ID! projectId: ID! connectionId: ID! + autoCommit: Boolean defaultCatalog: String defaultSchema: String @@ -401,4 +402,26 @@ extend type Mutation { asyncSqlRowDataCountResult(taskId: ID!): Int! + @since(version: "24.0.1") + asyncSqlSetAutoCommit( + projectId: ID!, + connectionId: ID!, + contextId: ID!, + autoCommit: Boolean! + ): AsyncTaskInfo! + + @since(version: "24.0.1") + asyncSqlCommitTransaction( + projectId: ID!, + connectionId: ID!, + contextId: ID! + ): AsyncTaskInfo! + + @since(version: "24.0.1") + asyncSqlRollbackTransaction( + projectId: ID!, + connectionId: ID!, + contextId: ID! + ): AsyncTaskInfo! + } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java index 3dcb4b5f64..752357ba66 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java @@ -172,4 +172,22 @@ String generateGroupByQuery(@NotNull WebSQLContextInfo contextInfo, @Nullable @WebAction Long getRowDataCountResult(@NotNull WebSession webSession, @NotNull String taskId) throws DBWebException; + + @WebAction + WebAsyncTaskInfo asyncSqlSetAutoCommit( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo contextInfo, + boolean autoCommit + ) throws DBWebException; + + @WebAction + WebAsyncTaskInfo asyncSqlRollbackTransaction( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo contextInfo + ) throws DBWebException; + + @WebAction + WebAsyncTaskInfo asyncSqlCommitTransaction( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo sqlContext); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java index 2a554eccd3..4856ed809b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java @@ -19,20 +19,27 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebAction; import io.cloudbeaver.WebProjectImpl; +import io.cloudbeaver.model.WebAsyncTaskInfo; +import io.cloudbeaver.model.session.WebAsyncTaskProcessor; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.session.WebSessionProvider; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBUtils; import org.jkiss.dbeaver.model.data.DBDAttributeBinding; -import org.jkiss.dbeaver.model.exec.DBCException; -import org.jkiss.dbeaver.model.exec.DBCExecutionContextDefaults; -import org.jkiss.dbeaver.model.exec.DBExecUtils; +import org.jkiss.dbeaver.model.exec.*; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.qm.QMTransactionState; +import org.jkiss.dbeaver.model.qm.QMUtils; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.struct.DBSDataContainer; import org.jkiss.dbeaver.model.struct.rdb.DBSCatalog; import org.jkiss.dbeaver.model.struct.rdb.DBSSchema; +import org.jkiss.dbeaver.utils.RuntimeUtils; import org.jkiss.utils.CommonUtils; +import java.lang.reflect.InvocationTargetException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; @@ -170,4 +177,103 @@ void dispose() { public WebSession getWebSession() { return processor.getWebSession(); } + + + /////////////////////////////////////////////////////// + // Transactions + + public WebAsyncTaskInfo setAutoCommit(boolean autoCommit) { + DBCExecutionContext context = processor.getExecutionContext(); + DBCTransactionManager txnManager = DBUtils.getTransactionManager(context); + WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor<>() { + @Override + public void run(DBRProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + if (txnManager != null) { + monitor.beginTask("Change connection auto-commit to " + autoCommit, 1); + try { + monitor.subTask("Change context '" + context.getContextName() + "' auto-commit state"); + txnManager.setAutoCommit(monitor, autoCommit); + result = true; + } catch (DBException e) { + throw new InvocationTargetException(e); + } finally { + monitor.done(); + } + } + + } + }; + return getWebSession().createAndRunAsyncTask("Set auto-commit", runnable); + + } + + public WebAsyncTaskInfo commitTransaction() { + DBCExecutionContext context = processor.getExecutionContext(); + DBCTransactionManager txnManager = DBUtils.getTransactionManager(context); + WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor<>() { + @Override + public void run(DBRProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + if (txnManager != null) { + QMTransactionState txnInfo = QMUtils.getTransactionState(context); + try (DBCSession session = context.openSession(monitor, DBCExecutionPurpose.UTIL, "Commit transaction")) { + txnManager.commit(session); + } catch (DBCException e) { + throw new InvocationTargetException(e); + } + result = """ + Transaction has been committed + Query count: %s + Duration: %s + """.formatted( + txnInfo.getUpdateCount(), + RuntimeUtils.formatExecutionTime(System.currentTimeMillis() - txnInfo.getTransactionStartTime()) + ); + } + } + }; + return getWebSession().createAndRunAsyncTask("Commit transaction", runnable); + } + + + public WebAsyncTaskInfo rollbackTransaction() { + DBCExecutionContext context = processor.getExecutionContext(); + DBCTransactionManager txnManager = DBUtils.getTransactionManager(context); + WebAsyncTaskProcessor runnable = new WebAsyncTaskProcessor<>() { + @Override + public void run(DBRProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + if (txnManager != null) { + QMTransactionState txnInfo = QMUtils.getTransactionState(context); + try (DBCSession session = context.openSession(monitor, DBCExecutionPurpose.UTIL, "Rollback transaction")) { + txnManager.rollback(session, null); + } catch (DBCException e) { + throw new InvocationTargetException(e); + } + result = """ + Transaction has been rolled back + Query count: %s + Duration: %s + """.formatted( + txnInfo.getUpdateCount(), + RuntimeUtils.formatExecutionTime(System.currentTimeMillis() - txnInfo.getTransactionStartTime()) + ); + } + } + }; + + return getWebSession().createAndRunAsyncTask("Rollback transaction", runnable); + } + + @Property + public Boolean isAutoCommit() throws DBWebException { + DBCExecutionContext context = processor.getExecutionContext(); + DBCTransactionManager txnManager = DBUtils.getTransactionManager(context); + if (txnManager == null) { + return null; + } + try { + return txnManager.isAutoCommit(); + } catch (DBException e) { + throw new DBWebException("Error getting auto-commit parameter from context", e); + } + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java index 7d4f225192..08e199f930 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java @@ -358,9 +358,15 @@ public WebSQLExecuteInfo updateResultsDataBatch( try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.USER, "Update data in container")) { DBCTransactionManager txnManager = DBUtils.getTransactionManager(executionContext); boolean revertToAutoCommit = false; - if (txnManager != null && txnManager.isSupportsTransactions() && txnManager.isAutoCommit()) { - txnManager.setAutoCommit(monitor, false); - revertToAutoCommit = true; + boolean isAutoCommitEnabled = true; + DBCSavepoint savepoint = null; + if (txnManager != null) { + isAutoCommitEnabled = txnManager.isAutoCommit(); + if (txnManager.isSupportsTransactions() && isAutoCommitEnabled) { + txnManager.setAutoCommit(monitor, false); + savepoint = txnManager.setSavepoint(monitor, null); + revertToAutoCommit = true; + } } try { Map options = Collections.emptyMap(); @@ -375,17 +381,27 @@ public WebSQLExecuteInfo updateResultsDataBatch( newResultSetRows.add(new WebSQLQueryResultSetRow(rowValues, null)); } - if (txnManager != null && txnManager.isSupportsTransactions()) { + if (txnManager != null && txnManager.isSupportsTransactions() && isAutoCommitEnabled) { txnManager.commit(session); } } catch (Exception e) { if (txnManager != null && txnManager.isSupportsTransactions()) { - txnManager.rollback(session, null); + txnManager.rollback(session, savepoint); } throw new DBCException("Error persisting data changes", e); } finally { - if (revertToAutoCommit) { - txnManager.setAutoCommit(monitor, true); + if (txnManager != null) { + if (revertToAutoCommit) { + txnManager.setAutoCommit(monitor, true); + } + try { + if (savepoint != null) { + txnManager.releaseSavepoint(monitor, savepoint); + } + } catch (Throwable e) { + // Maybe savepoints not supported + log.debug("Can't release savepoint", e); + } } } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java index cc266b8265..662fe6bd7e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java @@ -207,8 +207,23 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { getService(env).getRowDataCountResult( getWebSession(env), env.getArgument("taskId") - ) - ); + )) + .dataFetcher("asyncSqlSetAutoCommit", env -> + getService(env).asyncSqlSetAutoCommit( + getWebSession(env), + getSQLContext(env), + env.getArgument("autoCommit") + )) + .dataFetcher("asyncSqlCommitTransaction", env -> + getService(env).asyncSqlCommitTransaction( + getWebSession(env), + getSQLContext(env) + )) + .dataFetcher("asyncSqlRollbackTransaction", env -> + getService(env).asyncSqlRollbackTransaction( + getWebSession(env), + getSQLContext(env) + )); } @NotNull diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index 8728b647c6..e07d0e24cd 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -48,7 +48,6 @@ import org.jkiss.dbeaver.model.sql.registry.SQLGeneratorConfigurationRegistry; import org.jkiss.dbeaver.model.sql.registry.SQLGeneratorDescriptor; import org.jkiss.dbeaver.model.struct.DBSDataContainer; -import org.jkiss.dbeaver.model.struct.DBSEntity; import org.jkiss.dbeaver.model.struct.DBSObject; import org.jkiss.dbeaver.model.struct.DBSWrapper; import org.jkiss.dbeaver.utils.RuntimeUtils; @@ -579,4 +578,19 @@ public Long getRowDataCountResult(@NotNull WebSession webSession, @NotNull Strin return null; } + @Override + public WebAsyncTaskInfo asyncSqlSetAutoCommit(@NotNull WebSession webSession, @NotNull WebSQLContextInfo contextInfo, boolean autoCommit) throws DBWebException { + return contextInfo.setAutoCommit(autoCommit); + } + + @Override + public WebAsyncTaskInfo asyncSqlRollbackTransaction(@NotNull WebSession webSession, @NotNull WebSQLContextInfo contextInfo) throws DBWebException { + return contextInfo.rollbackTransaction(); + } + + @Override + public WebAsyncTaskInfo asyncSqlCommitTransaction(@NotNull WebSession webSession, @NotNull WebSQLContextInfo contextInfo) { + return contextInfo.commitTransaction(); + } + } diff --git a/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContext.ts b/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContext.ts index 31b804d9c0..b06e38d3d8 100644 --- a/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContext.ts +++ b/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContext.ts @@ -8,10 +8,16 @@ import { computed, makeObservable, observable } from 'mobx'; import type { ITask, TaskScheduler } from '@cloudbeaver/core-executor'; +import type { AsyncTaskInfo, AsyncTaskInfoService, GraphQLService } from '@cloudbeaver/core-sdk'; import type { ConnectionExecutionContextResource, IConnectionExecutionContextInfo } from './ConnectionExecutionContextResource'; import type { IConnectionExecutionContext } from './IConnectionExecutionContext'; +export interface IConnectionExecutionContextUpdateTaskInfo { + name?: string; + result?: string | boolean; +} + export class ConnectionExecutionContext implements IConnectionExecutionContext { get context(): IConnectionExecutionContextInfo | undefined { return this.connectionExecutionContextResource.get(this.contextId); @@ -25,12 +31,22 @@ export class ConnectionExecutionContext implements IConnectionExecutionContext { return this.currentTask?.cancellable || false; } + get autoCommit() { + if (!this.context) { + return; + } + + return this.context.autoCommit; + } + private currentTask: ITask | null; constructor( + private readonly contextId: string, private readonly scheduler: TaskScheduler, private readonly connectionExecutionContextResource: ConnectionExecutionContextResource, - private readonly contextId: string, + private readonly asyncTaskInfoService: AsyncTaskInfoService, + private readonly graphQLService: GraphQLService, ) { this.currentTask = null; makeObservable(this, { @@ -38,6 +54,7 @@ export class ConnectionExecutionContext implements IConnectionExecutionContext { context: computed, executing: computed, cancellable: computed, + autoCommit: computed, }); } @@ -76,4 +93,86 @@ export class ConnectionExecutionContext implements IConnectionExecutionContext { return await this.connectionExecutionContextResource.update(this.contextId, defaultCatalog, defaultSchema); } + + async setAutoCommit(auto: boolean): Promise { + const result = await this.withContext(async context => { + const task = this.asyncTaskInfoService.create(async () => { + const { taskInfo } = await this.graphQLService.sdk.asyncSqlSetAutoCommit({ + projectId: context.projectId, + connectionId: context.connectionId, + contextId: context.id, + autoCommit: auto, + }); + + return taskInfo; + }); + + return await this.run( + async () => await this.asyncTaskInfoService.run(task), + () => this.asyncTaskInfoService.cancel(task.id), + () => this.asyncTaskInfoService.remove(task.id), + ); + }); + + return mapAsyncTaskInfo(result); + } + + async commit(): Promise { + const result = await this.withContext(async context => { + const task = this.asyncTaskInfoService.create(async () => { + const { taskInfo } = await this.graphQLService.sdk.asyncSqlCommitTransaction({ + projectId: context.projectId, + connectionId: context.connectionId, + contextId: context.id, + }); + + return taskInfo; + }); + + return await this.run( + async () => await this.asyncTaskInfoService.run(task), + () => this.asyncTaskInfoService.cancel(task.id), + () => this.asyncTaskInfoService.remove(task.id), + ); + }); + + return mapAsyncTaskInfo(result); + } + + async rollback(): Promise { + const result = await this.withContext(async context => { + const task = this.asyncTaskInfoService.create(async () => { + const { taskInfo } = await this.graphQLService.sdk.asyncSqlRollbackTransaction({ + projectId: context.projectId, + connectionId: context.connectionId, + contextId: context.id, + }); + + return taskInfo; + }); + + return await this.run( + async () => await this.asyncTaskInfoService.run(task), + () => this.asyncTaskInfoService.cancel(task.id), + () => this.asyncTaskInfoService.remove(task.id), + ); + }); + + return mapAsyncTaskInfo(result); + } + + private withContext(callback: (context: IConnectionExecutionContextInfo) => Promise): Promise { + if (!this.context) { + throw new Error('Execution Context not found'); + } + + return callback(this.context); + } +} + +function mapAsyncTaskInfo(info: AsyncTaskInfo): IConnectionExecutionContextUpdateTaskInfo { + return { + name: info.name, + result: info.taskResult, + }; } diff --git a/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextService.ts b/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextService.ts index 8747a60e2b..6da75ec1ea 100644 --- a/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextService.ts +++ b/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextService.ts @@ -8,6 +8,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { TaskScheduler } from '@cloudbeaver/core-executor'; import { CachedMapAllKey, ResourceKeyUtils } from '@cloudbeaver/core-resource'; +import { AsyncTaskInfoService, GraphQLService } from '@cloudbeaver/core-sdk'; import { MetadataMap } from '@cloudbeaver/core-utils'; import type { IConnectionInfoParams } from '../CONNECTION_INFO_PARAM_SCHEMA'; @@ -19,8 +20,21 @@ export class ConnectionExecutionContextService { private readonly contexts: MetadataMap; protected scheduler: TaskScheduler; - constructor(readonly connectionExecutionContextResource: ConnectionExecutionContextResource) { - this.contexts = new MetadataMap(contextId => new ConnectionExecutionContext(this.scheduler, this.connectionExecutionContextResource, contextId)); + constructor( + readonly connectionExecutionContextResource: ConnectionExecutionContextResource, + private readonly asyncTaskInfoService: AsyncTaskInfoService, + private readonly GraphQLService: GraphQLService, + ) { + this.contexts = new MetadataMap( + contextId => + new ConnectionExecutionContext( + contextId, + this.scheduler, + this.connectionExecutionContextResource, + this.asyncTaskInfoService, + this.GraphQLService, + ), + ); this.scheduler = new TaskScheduler((a, b) => a === b); this.connectionExecutionContextResource.onItemDelete.addHandler(key => ResourceKeyUtils.forEach(key, contextId => this.contexts.delete(contextId)), diff --git a/webapp/packages/core-connections/src/ConnectionsManagerService.ts b/webapp/packages/core-connections/src/ConnectionsManagerService.ts index 82f695bd68..5146828808 100644 --- a/webapp/packages/core-connections/src/ConnectionsManagerService.ts +++ b/webapp/packages/core-connections/src/ConnectionsManagerService.ts @@ -152,6 +152,7 @@ export class ConnectionsManagerService { if (!connection.connected) { return; } + await this.connectionInfo.close(createConnectionParam(connection)); } @@ -159,13 +160,29 @@ export class ConnectionsManagerService { if (this.disconnecting) { return; } + + const connectionParams = this.projectConnections.map(connection => createConnectionParam(connection)); + const contexts = await this.onDisconnect.execute({ + connections: connectionParams, + state: 'before', + }); + + if (ExecutorInterrupter.isInterrupted(contexts)) { + return; + } + this.disconnecting = true; const { controller, notification } = this.notificationService.processNotification(() => ProcessSnackbar, {}, { title: 'Disconnecting...' }); try { for (const connection of this.projectConnections) { await this._closeConnectionAsync(connection); + this.onDisconnect.execute({ + connections: [createConnectionParam(connection)], + state: 'after', + }); } + notification.close(); } catch (e: any) { controller.reject(e); diff --git a/webapp/packages/core-connections/src/extensions/IExecutionContextProvider.ts b/webapp/packages/core-connections/src/extensions/IExecutionContextProvider.ts new file mode 100644 index 0000000000..599597d0f9 --- /dev/null +++ b/webapp/packages/core-connections/src/extensions/IExecutionContextProvider.ts @@ -0,0 +1,22 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { createExtension, IExtension, isExtension } from '@cloudbeaver/core-extensions'; + +import type { IConnectionExecutionContextInfo } from '../ConnectionExecutionContext/ConnectionExecutionContextResource'; + +const EXECUTION_CONTEXT_PROVIDER_SYMBOL = Symbol('@extension/ExecutionContextProvider'); + +export type IExecutionContextProvider = (context: T) => IConnectionExecutionContextInfo | undefined; + +export function executionContextProvider(provider: IExecutionContextProvider) { + return createExtension(provider, EXECUTION_CONTEXT_PROVIDER_SYMBOL); +} + +export function isExecutionContextProvider(obj: IExtension): obj is IExecutionContextProvider & IExtension { + return isExtension(obj, EXECUTION_CONTEXT_PROVIDER_SYMBOL); +} diff --git a/webapp/packages/core-connections/src/index.ts b/webapp/packages/core-connections/src/index.ts index ede58b7a42..202ebbf4a9 100644 --- a/webapp/packages/core-connections/src/index.ts +++ b/webapp/packages/core-connections/src/index.ts @@ -9,6 +9,7 @@ export * from './extensions/IObjectCatalogProvider'; export * from './extensions/IObjectCatalogSetter'; export * from './extensions/IObjectSchemaProvider'; export * from './extensions/IObjectSchemaSetter'; +export * from './extensions/IExecutionContextProvider'; export * from './NavTree/ConnectionNavNodeService'; export * from './NavTree/NavNodeExtensionsService'; export * from './NavTree/getConnectionFolderIdFromNodeId'; diff --git a/webapp/packages/core-sdk/src/queries/fragments/ExecutionContextInfo.gql b/webapp/packages/core-sdk/src/queries/fragments/ExecutionContextInfo.gql index eb1925b4d8..9f23894015 100644 --- a/webapp/packages/core-sdk/src/queries/fragments/ExecutionContextInfo.gql +++ b/webapp/packages/core-sdk/src/queries/fragments/ExecutionContextInfo.gql @@ -1,7 +1,8 @@ fragment ExecutionContextInfo on SQLContextInfo { - id - projectId - connectionId - defaultCatalog - defaultSchema -} \ No newline at end of file + id + projectId + connectionId + autoCommit + defaultCatalog + defaultSchema +} diff --git a/webapp/packages/core-sdk/src/queries/transactions/asyncSqlCommitTransaction.gql b/webapp/packages/core-sdk/src/queries/transactions/asyncSqlCommitTransaction.gql new file mode 100644 index 0000000000..8253d797a6 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/transactions/asyncSqlCommitTransaction.gql @@ -0,0 +1,5 @@ +mutation asyncSqlCommitTransaction($projectId: ID!, $connectionId: ID!, $contextId: ID!) { + taskInfo: asyncSqlCommitTransaction(projectId: $projectId, connectionId: $connectionId, contextId: $contextId) { + ...AsyncTaskInfo + } +} diff --git a/webapp/packages/core-sdk/src/queries/transactions/asyncSqlRollbackTransaction.gql b/webapp/packages/core-sdk/src/queries/transactions/asyncSqlRollbackTransaction.gql new file mode 100644 index 0000000000..2b13f458cf --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/transactions/asyncSqlRollbackTransaction.gql @@ -0,0 +1,5 @@ +mutation asyncSqlRollbackTransaction($projectId: ID!, $connectionId: ID!, $contextId: ID!) { + taskInfo: asyncSqlRollbackTransaction(projectId: $projectId, connectionId: $connectionId, contextId: $contextId) { + ...AsyncTaskInfo + } +} diff --git a/webapp/packages/core-sdk/src/queries/transactions/asyncSqlSetAutoCommit.gql b/webapp/packages/core-sdk/src/queries/transactions/asyncSqlSetAutoCommit.gql new file mode 100644 index 0000000000..9b2a6de697 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/transactions/asyncSqlSetAutoCommit.gql @@ -0,0 +1,5 @@ +mutation asyncSqlSetAutoCommit($projectId: ID!, $connectionId: ID!, $contextId: ID!, $autoCommit: Boolean!) { + taskInfo: asyncSqlSetAutoCommit(projectId: $projectId, connectionId: $connectionId, contextId: $contextId, autoCommit: $autoCommit) { + ...AsyncTaskInfo + } +} diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts index 2f0d71d4c1..e61ff1e75f 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerService.ts @@ -15,12 +15,14 @@ import { IConnectionInfoParams, IConnectionProvider, IConnectionSetter, + IExecutionContextProvider, IObjectCatalogProvider, IObjectCatalogSetter, IObjectSchemaProvider, IObjectSchemaSetter, isConnectionProvider, isConnectionSetter, + isExecutionContextProvider, isObjectCatalogProvider, isObjectCatalogSetter, isObjectSchemaProvider, @@ -58,6 +60,7 @@ interface IActiveItem { getCurrentConnectionId?: IConnectionProvider; getCurrentSchemaId?: IObjectSchemaProvider; getCurrentCatalogId?: IObjectCatalogProvider; + getCurrentExecutionContext?: IExecutionContextProvider; changeConnectionId?: IConnectionSetter; changeProjectId?: IProjectSetter; changeCatalogId?: IObjectCatalogSetter; @@ -121,6 +124,14 @@ export class ConnectionSchemaManagerService { return this.activeObjectCatalogId; } + get activeExecutionContext() { + if (!this.activeItem?.getCurrentExecutionContext) { + return; + } + + return this.activeItem.getCurrentExecutionContext(this.activeItem.context); + } + get currentObjectSchemaId(): string | undefined { if (this.pendingSchemaId !== null) { return this.pendingSchemaId; @@ -442,6 +453,9 @@ export class ConnectionSchemaManagerService { .on(isObjectSchemaProvider, extension => { item.getCurrentSchemaId = extension; }) + .on(isExecutionContextProvider, extension => { + item.getCurrentExecutionContext = extension; + }) .on(isProjectSetter, extension => { item.changeProjectId = extension; diff --git a/webapp/packages/plugin-datasource-transaction-manager/.gitignore b/webapp/packages/plugin-datasource-transaction-manager/.gitignore new file mode 100644 index 0000000000..15bc16c7c3 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/.gitignore @@ -0,0 +1,17 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/plugin-datasource-transaction-manager/package.json b/webapp/packages/plugin-datasource-transaction-manager/package.json new file mode 100644 index 0000000000..cc10783b7a --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/package.json @@ -0,0 +1,37 @@ +{ + "name": "@cloudbeaver/plugin-datasource-transaction-manager", + "sideEffects": [ + "src/**/*.css", + "src/**/*.scss", + "public/**/*" + ], + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "main": "dist/index.js", + "scripts": { + "build": "tsc -b", + "lint": "eslint ./src/ --ext .ts,.tsx", + "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", + "validate-dependencies": "core-cli-validate-dependencies", + "update-ts-references": "rimraf --glob dist && typescript-resolve-references" + }, + "dependencies": { + "@cloudbeaver/core-connections": "~0.1.0", + "@cloudbeaver/core-di": "~0.1.0", + "@cloudbeaver/core-events": "~0.1.0", + "@cloudbeaver/core-executor": "~0.1.0", + "@cloudbeaver/core-localization": "~0.1.0", + "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-ui": "~0.1.0", + "@cloudbeaver/core-utils": "~0.1.0", + "@cloudbeaver/core-view": "~0.1.0", + "@cloudbeaver/plugin-datasource-context-switch": "~0.1.0", + "@cloudbeaver/plugin-top-app-bar": "~0.1.0", + "mobx": "^6.12.0" + }, + "peerDependencies": {}, + "devDependencies": { + "typescript": "^5.3.2" + } +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit.svg new file mode 100644 index 0000000000..e640e089ef --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_m.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_m.svg new file mode 100644 index 0000000000..f07ddff927 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_m.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto.svg new file mode 100644 index 0000000000..cfa19cd5a2 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto_m.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto_m.svg new file mode 100644 index 0000000000..812bca94d1 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto_m.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto_sm.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto_sm.svg new file mode 100644 index 0000000000..fbc45eba62 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_auto_sm.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual.svg new file mode 100644 index 0000000000..62920cce76 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual_m.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual_m.svg new file mode 100644 index 0000000000..82ca26326a --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual_m.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual_sm.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual_sm.svg new file mode 100644 index 0000000000..f23fd9fbe5 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_mode_manual_sm.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_sm.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_sm.svg new file mode 100644 index 0000000000..6ef412d43f --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/commit_sm.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback.svg new file mode 100644 index 0000000000..6932be5834 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback.svg @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback_m.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback_m.svg new file mode 100644 index 0000000000..c469264a9a --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback_m.svg @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback_sm.svg b/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback_sm.svg new file mode 100644 index 0000000000..f206366047 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/public/icons/rollback_sm.svg @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/LocaleService.ts b/webapp/packages/plugin-datasource-transaction-manager/src/LocaleService.ts new file mode 100644 index 0000000000..edeb82d255 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/LocaleService.ts @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +@injectable() +export class LocaleService extends Bootstrap { + constructor(private readonly localizationService: LocalizationService) { + super(); + } + + register(): void | Promise { + this.localizationService.addProvider(this.provider.bind(this)); + } + + load(): void | Promise {} + + private async provider(locale: string) { + switch (locale) { + case 'ru': + return (await import('./locales/ru')).default; + case 'it': + return (await import('./locales/it')).default; + case 'zh': + return (await import('./locales/zh')).default; + default: + return (await import('./locales/en')).default; + } + } +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerBootstrap.ts b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerBootstrap.ts new file mode 100644 index 0000000000..8dae08b56c --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerBootstrap.ts @@ -0,0 +1,212 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { ConfirmationDialog } from '@cloudbeaver/core-blocks'; +import { + ConnectionExecutionContext, + ConnectionExecutionContextResource, + ConnectionExecutionContextService, + ConnectionInfoResource, + ConnectionsManagerService, + createConnectionParam, + IConnectionExecutionContextUpdateTaskInfo, + IConnectionExecutorData, + isConnectionInfoParamEqual, +} from '@cloudbeaver/core-connections'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import { OptionsPanelService } from '@cloudbeaver/core-ui'; +import { isNotNullDefined } from '@cloudbeaver/core-utils'; +import { ActionService, MenuService } from '@cloudbeaver/core-view'; +import { ConnectionSchemaManagerService } from '@cloudbeaver/plugin-datasource-context-switch'; +import { MENU_APP_ACTIONS } from '@cloudbeaver/plugin-top-app-bar'; + +import { ACTION_DATASOURCE_TRANSACTION_COMMIT } from './actions/ACTION_DATASOURCE_TRANSACTION_COMMIT'; +import { ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE } from './actions/ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE'; +import { ACTION_DATASOURCE_TRANSACTION_ROLLBACK } from './actions/ACTION_DATASOURCE_TRANSACTION_ROLLBACK'; + +@injectable() +export class TransactionManagerBootstrap extends Bootstrap { + constructor( + private readonly menuService: MenuService, + private readonly actionService: ActionService, + private readonly connectionSchemaManagerService: ConnectionSchemaManagerService, + private readonly connectionExecutionContextService: ConnectionExecutionContextService, + private readonly connectionExecutionContextResource: ConnectionExecutionContextResource, + private readonly connectionInfoResource: ConnectionInfoResource, + private readonly connectionsManagerService: ConnectionsManagerService, + private readonly optionsPanelService: OptionsPanelService, + private readonly notificationService: NotificationService, + private readonly commonDialogService: CommonDialogService, + private readonly localizationService: LocalizationService, + ) { + super(); + } + + register() { + this.connectionsManagerService.onDisconnect.addHandler(this.disconnectHandler.bind(this)); + + this.menuService.addCreator({ + menus: [MENU_APP_ACTIONS], + isApplicable: () => { + const transaction = this.getContextTransaction(); + + return ( + !this.optionsPanelService.active && + this.connectionSchemaManagerService.currentConnection?.connected === true && + !!transaction?.context && + isNotNullDefined(transaction.autoCommit) + ); + }, + getItems: (_, items) => [ + ...items, + ACTION_DATASOURCE_TRANSACTION_COMMIT, + ACTION_DATASOURCE_TRANSACTION_ROLLBACK, + ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE, + ], + }); + + this.actionService.addHandler({ + id: 'commit-mode-base', + isActionApplicable: (_, action) => + [ACTION_DATASOURCE_TRANSACTION_COMMIT, ACTION_DATASOURCE_TRANSACTION_ROLLBACK, ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE].includes( + action, + ), + isLabelVisible: (_, action) => action === ACTION_DATASOURCE_TRANSACTION_COMMIT || action === ACTION_DATASOURCE_TRANSACTION_ROLLBACK, + getActionInfo: (_, action) => { + const transaction = this.getContextTransaction(); + + if (!transaction) { + return action.info; + } + + if (action === ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE) { + const auto = transaction.autoCommit; + const icon = `/icons/commit_mode_${auto ? 'auto' : 'manual'}_m.svg`; + const label = `plugin_datasource_transaction_manager_commit_mode_switch_to_${auto ? 'manual' : 'auto'}`; + + return { ...action.info, icon, label, tooltip: label }; + } + + return action.info; + }, + isDisabled: () => { + const transaction = this.getContextTransaction(); + return transaction?.executing === true; + }, + isHidden: (_, action) => { + const transaction = this.getContextTransaction(); + + if (!transaction) { + return true; + } + + if (action === ACTION_DATASOURCE_TRANSACTION_COMMIT || action === ACTION_DATASOURCE_TRANSACTION_ROLLBACK) { + return transaction.autoCommit === true; + } + + return false; + }, + handler: async (_, action) => { + const transaction = this.getContextTransaction(); + + if (!transaction) { + return; + } + + switch (action) { + case ACTION_DATASOURCE_TRANSACTION_COMMIT: { + await this.commit(transaction); + break; + } + case ACTION_DATASOURCE_TRANSACTION_ROLLBACK: { + try { + const result = await transaction.rollback(); + this.showTransactionResult(transaction, result); + } catch (exception: any) { + this.notificationService.logException(exception, 'plugin_datasource_transaction_manager_rollback_fail'); + } + + break; + } + case ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE: + try { + await transaction.setAutoCommit(!transaction.autoCommit); + await this.connectionExecutionContextResource.refresh(); + } catch (exception: any) { + this.notificationService.logException(exception, 'plugin_datasource_transaction_manager_commit_mode_fail'); + } + + break; + } + }, + }); + } + + private showTransactionResult(transaction: ConnectionExecutionContext, info: IConnectionExecutionContextUpdateTaskInfo) { + if (!transaction.context) { + return; + } + + const connectionParam = createConnectionParam(transaction.context.projectId, transaction.context.connectionId); + const connection = this.connectionInfoResource.get(connectionParam); + const message = typeof info.result === 'string' ? info.result : ''; + + this.notificationService.logInfo({ title: connection?.name ?? info.name ?? '', message }); + } + + private getContextTransaction() { + const context = this.connectionSchemaManagerService.activeExecutionContext; + + if (!context) { + return; + } + + return this.connectionExecutionContextService.get(context.id); + } + + private async disconnectHandler(data: IConnectionExecutorData, contexts: IExecutionContextProvider) { + if (data.state === 'before') { + for (const connectionKey of data.connections) { + const context = this.connectionExecutionContextResource.values.find(connection => isConnectionInfoParamEqual(connection, connectionKey)); + + if (context) { + const transaction = this.connectionExecutionContextService.get(context.id); + + if (transaction?.autoCommit === false) { + const connectionData = this.connectionInfoResource.get(connectionKey); + const state = await this.commonDialogService.open(ConfirmationDialog, { + title: `${this.localizationService.translate('plugin_datasource_transaction_manager_commit')} (${connectionData?.name ?? context.id})`, + message: 'plugin_datasource_transaction_manager_commit_confirmation_message', + confirmActionText: 'plugin_datasource_transaction_manager_commit', + extraStatus: 'no', + }); + + if (state === DialogueStateResult.Resolved) { + await this.commit(transaction, () => ExecutorInterrupter.interrupt(contexts)); + } else if (state === DialogueStateResult.Rejected) { + ExecutorInterrupter.interrupt(contexts); + } + } + } + } + } + } + + private async commit(transaction: ConnectionExecutionContext, onError?: (exception: any) => void) { + try { + const result = await transaction.commit(); + this.showTransactionResult(transaction, result); + } catch (exception: any) { + this.notificationService.logException(exception, 'plugin_datasource_transaction_manager_commit_fail'); + onError?.(exception); + } + } +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_COMMIT.ts b/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_COMMIT.ts new file mode 100644 index 0000000000..ed5c81d8d9 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_COMMIT.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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_DATASOURCE_TRANSACTION_COMMIT = createAction('datasource-transaction-commit', { + label: 'plugin_datasource_transaction_manager_commit', + tooltip: 'plugin_datasource_transaction_manager_commit', + icon: '/icons/commit_m.svg', +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE.ts b/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE.ts new file mode 100644 index 0000000000..66f485f534 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE = createAction('datasource-transaction-commit-mode-toggle', { + label: 'plugin_datasource_transaction_manager_commit_mode_switch_to_manual', + tooltip: 'plugin_datasource_transaction_manager_commit_mode_switch_to_manual', + icon: '/icons/commit_mode_auto_m.svg', +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_ROLLBACK.ts b/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_ROLLBACK.ts new file mode 100644 index 0000000000..e801026795 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/actions/ACTION_DATASOURCE_TRANSACTION_ROLLBACK.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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_DATASOURCE_TRANSACTION_ROLLBACK = createAction('datasource-transaction-rollback', { + label: 'plugin_datasource_transaction_manager_rollback', + tooltip: 'plugin_datasource_transaction_manager_rollback', + icon: '/icons/rollback_m.svg', +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/index.ts b/webapp/packages/plugin-datasource-transaction-manager/src/index.ts new file mode 100644 index 0000000000..08e0384d3c --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/index.ts @@ -0,0 +1 @@ +export { datasourceTransactionManagerPlugin } from './manifest'; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/locales/en.ts b/webapp/packages/plugin-datasource-transaction-manager/src/locales/en.ts new file mode 100644 index 0000000000..f113567706 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/locales/en.ts @@ -0,0 +1,10 @@ +export default [ + ['plugin_datasource_transaction_manager_commit', 'Commit'], + ['plugin_datasource_transaction_manager_rollback', 'Rollback'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_auto', 'Switch to auto-commit'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_manual', 'Switch to manual commit'], + ['plugin_datasource_transaction_manager_commit_fail', 'Failed to commit transaction'], + ['plugin_datasource_transaction_manager_rollback_fail', 'Failed to rollback transaction'], + ['plugin_datasource_transaction_manager_commit_mode_fail', 'Failed to change commit mode'], + ['plugin_datasource_transaction_manager_commit_confirmation_message', 'Do you want to commit changes?'], +]; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/locales/it.ts b/webapp/packages/plugin-datasource-transaction-manager/src/locales/it.ts new file mode 100644 index 0000000000..f113567706 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/locales/it.ts @@ -0,0 +1,10 @@ +export default [ + ['plugin_datasource_transaction_manager_commit', 'Commit'], + ['plugin_datasource_transaction_manager_rollback', 'Rollback'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_auto', 'Switch to auto-commit'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_manual', 'Switch to manual commit'], + ['plugin_datasource_transaction_manager_commit_fail', 'Failed to commit transaction'], + ['plugin_datasource_transaction_manager_rollback_fail', 'Failed to rollback transaction'], + ['plugin_datasource_transaction_manager_commit_mode_fail', 'Failed to change commit mode'], + ['plugin_datasource_transaction_manager_commit_confirmation_message', 'Do you want to commit changes?'], +]; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/locales/ru.ts b/webapp/packages/plugin-datasource-transaction-manager/src/locales/ru.ts new file mode 100644 index 0000000000..921b0e3d58 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/locales/ru.ts @@ -0,0 +1,10 @@ +export default [ + ['plugin_datasource_transaction_manager_commit', 'Commit'], + ['plugin_datasource_transaction_manager_rollback', 'Rollback'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_auto', 'Переключить в авто-коммит'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_manual', 'Переключить в ручной коммит'], + ['plugin_datasource_transaction_manager_commit_fail', 'Не удалось выполнить коммит'], + ['plugin_datasource_transaction_manager_rollback_fail', 'Не удалось выполнить откат'], + ['plugin_datasource_transaction_manager_commit_mode_fail', 'Не удалось переключить режим коммита'], + ['plugin_datasource_transaction_manager_commit_confirmation_message', 'Вы хотите зафиксировать изменения?'], +]; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/locales/zh.ts b/webapp/packages/plugin-datasource-transaction-manager/src/locales/zh.ts new file mode 100644 index 0000000000..f113567706 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/locales/zh.ts @@ -0,0 +1,10 @@ +export default [ + ['plugin_datasource_transaction_manager_commit', 'Commit'], + ['plugin_datasource_transaction_manager_rollback', 'Rollback'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_auto', 'Switch to auto-commit'], + ['plugin_datasource_transaction_manager_commit_mode_switch_to_manual', 'Switch to manual commit'], + ['plugin_datasource_transaction_manager_commit_fail', 'Failed to commit transaction'], + ['plugin_datasource_transaction_manager_rollback_fail', 'Failed to rollback transaction'], + ['plugin_datasource_transaction_manager_commit_mode_fail', 'Failed to change commit mode'], + ['plugin_datasource_transaction_manager_commit_confirmation_message', 'Do you want to commit changes?'], +]; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/manifest.ts b/webapp/packages/plugin-datasource-transaction-manager/src/manifest.ts new file mode 100644 index 0000000000..b46508e305 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/manifest.ts @@ -0,0 +1,19 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { PluginManifest } from '@cloudbeaver/core-di'; + +import { LocaleService } from './LocaleService'; +import { TransactionManagerBootstrap } from './TransactionManagerBootstrap'; + +export const datasourceTransactionManagerPlugin: PluginManifest = { + info: { + name: 'Datasource transaction manager plugin', + }, + + providers: [TransactionManagerBootstrap, LocaleService], +}; diff --git a/webapp/packages/plugin-datasource-transaction-manager/tsconfig.json b/webapp/packages/plugin-datasource-transaction-manager/tsconfig.json new file mode 100644 index 0000000000..f336f7ca0b --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/tsconfig.json @@ -0,0 +1,55 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + }, + "references": [ + { + "path": "../core-connections/tsconfig.json" + }, + { + "path": "../core-di/tsconfig.json" + }, + { + "path": "../core-events/tsconfig.json" + }, + { + "path": "../core-executor/tsconfig.json" + }, + { + "path": "../core-localization/tsconfig.json" + }, + { + "path": "../core-sdk/tsconfig.json" + }, + { + "path": "../core-ui/tsconfig.json" + }, + { + "path": "../core-utils/tsconfig.json" + }, + { + "path": "../core-view/tsconfig.json" + }, + { + "path": "../plugin-datasource-context-switch/tsconfig.json" + }, + { + "path": "../plugin-top-app-bar/tsconfig.json" + } + ], + "include": [ + "__custom_mocks__/**/*", + "src/**/*", + "src/**/*.json", + "src/**/*.css", + "src/**/*.scss" + ], + "exclude": [ + "**/node_modules", + "lib/**/*", + "dist/**/*" + ] +} diff --git a/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts b/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts index cf2696bc03..720faaec96 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts @@ -10,12 +10,15 @@ import { action, makeObservable, runInAction } from 'mobx'; import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Connection, + ConnectionExecutionContextResource, ConnectionInfoActiveProjectKey, ConnectionInfoResource, ConnectionNavNodeService, connectionProvider, createConnectionParam, + executionContextProvider, IConnectionInfoParams, + isConnectionInfoParamEqual, objectCatalogProvider, objectSchemaProvider, } from '@cloudbeaver/core-connections'; @@ -55,6 +58,7 @@ export class ObjectViewerTabService { private readonly connectionInfoResource: ConnectionInfoResource, private readonly connectionNavNodeService: ConnectionNavNodeService, private readonly navTreeResource: NavTreeResource, + private readonly connectionExecutionContextResource: ConnectionExecutionContextResource, ) { this.tabHandler = this.navigationTabsService.registerTabHandler({ key: objectViewerTabHandlerKey, @@ -71,6 +75,7 @@ export class ObjectViewerTabService { connectionProvider(this.getConnection.bind(this)), objectCatalogProvider(this.getDBObjectCatalog.bind(this)), objectSchemaProvider(this.getDBObjectSchema.bind(this)), + executionContextProvider(this.getExecutionContext.bind(this)), ], }); @@ -329,6 +334,16 @@ export class ObjectViewerTabService { return nodeInfo.schemaId; } + private getExecutionContext(context: ITab) { + const connectionKey = context.handlerState.connectionKey; + + if (!connectionKey) { + return; + } + + return this.connectionExecutionContextResource.values.find(connection => isConnectionInfoParamEqual(connection, connectionKey)); + } + private selectObjectTab(tab: ITab) { if (tab.handlerState.error) { return; 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 b1e9c41be9..a4d7857fd1 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts @@ -18,6 +18,7 @@ import { ConnectionsManagerService, ContainerResource, createConnectionParam, + executionContextProvider, ICatalogData, IConnectionExecutorData, IConnectionInfoParams, @@ -95,6 +96,7 @@ export class SqlEditorTabService extends Bootstrap { connectionProvider(this.getConnectionId.bind(this)), objectCatalogProvider(this.getObjectCatalogId.bind(this)), objectSchemaProvider(this.getObjectSchemaId.bind(this)), + executionContextProvider(this.getExecutionContext.bind(this)), projectSetter(this.setProjectId.bind(this)), connectionSetter((connectionId, tab) => this.setConnectionId(tab, connectionId)), objectCatalogSetter(this.setObjectCatalogId.bind(this)), @@ -339,6 +341,11 @@ export class SqlEditorTabService extends Bootstrap { return context?.defaultSchema; } + private getExecutionContext(tab: ITab) { + const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId); + return dataSource?.executionContext; + } + private setProjectId(projectId: string | null, tab: ITab): boolean { const dataSource = this.sqlDataSourceService.get(tab.handlerState.editorId); diff --git a/webapp/packages/product-default/package.json b/webapp/packages/product-default/package.json index e13634bb09..d3ab34b6bf 100644 --- a/webapp/packages/product-default/package.json +++ b/webapp/packages/product-default/package.json @@ -46,6 +46,7 @@ "@cloudbeaver/plugin-data-viewer": "~0.1.0", "@cloudbeaver/plugin-data-viewer-result-set-grouping": "~0.1.0", "@cloudbeaver/plugin-datasource-context-switch": "~0.1.0", + "@cloudbeaver/plugin-datasource-transaction-manager": "~0.1.0", "@cloudbeaver/plugin-ddl-viewer": "~0.1.0", "@cloudbeaver/plugin-devtools": "~0.1.0", "@cloudbeaver/plugin-gis-viewer": "~0.1.0", diff --git a/webapp/packages/product-default/src/index.ts b/webapp/packages/product-default/src/index.ts index 70751df971..b4b13d7e6b 100644 --- a/webapp/packages/product-default/src/index.ts +++ b/webapp/packages/product-default/src/index.ts @@ -17,6 +17,7 @@ import { dataSpreadsheetNewManifest } from '@cloudbeaver/plugin-data-spreadsheet import { dataViewerManifest } from '@cloudbeaver/plugin-data-viewer'; import { dvResultSetGroupingPlugin } from '@cloudbeaver/plugin-data-viewer-result-set-grouping'; import { datasourceContextSwitchPluginManifest } from '@cloudbeaver/plugin-datasource-context-switch'; +import { datasourceTransactionManagerPlugin } from '@cloudbeaver/plugin-datasource-transaction-manager'; import ddlViewer from '@cloudbeaver/plugin-ddl-viewer'; import devTools from '@cloudbeaver/plugin-devtools'; import gisViewer from '@cloudbeaver/plugin-gis-viewer'; @@ -106,6 +107,7 @@ const PLUGINS: PluginManifest[] = [ root, sessionExpirationPlugin, toolsPanel, + datasourceTransactionManagerPlugin, projects, browserPlugin, navigationTreeFilters, diff --git a/webapp/packages/product-default/tsconfig.json b/webapp/packages/product-default/tsconfig.json index ead0ffaad6..0f5134c4b2 100644 --- a/webapp/packages/product-default/tsconfig.json +++ b/webapp/packages/product-default/tsconfig.json @@ -69,6 +69,9 @@ { "path": "../plugin-datasource-context-switch/tsconfig.json" }, + { + "path": "../plugin-datasource-transaction-manager/tsconfig.json" + }, { "path": "../plugin-ddl-viewer/tsconfig.json" },