diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java index 9b4988b371..335f28b5ab 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java @@ -221,7 +221,7 @@ public class WebServiceNavigator implements DBWServiceNavigator { contextDefaults != null && contextDefaults.supportsSchemaChange() && ( contextDefaults.getDefaultCatalog().getName().equals(node.getName()) - || catalog.equals(node.getName()) + || node.getName().equals(catalog) ) ) { try { diff --git a/webapp/packages/core-connections/src/ConnectionsManagerService.ts b/webapp/packages/core-connections/src/ConnectionsManagerService.ts index 578f42dbf1..583e2a7a66 100644 --- a/webapp/packages/core-connections/src/ConnectionsManagerService.ts +++ b/webapp/packages/core-connections/src/ConnectionsManagerService.ts @@ -10,16 +10,24 @@ import { ProcessSnackbar } from '@cloudbeaver/core-blocks'; import { injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import { Executor, IExecutor } from '@cloudbeaver/core-executor'; +import { Executor, ExecutorInterrupter, IExecutor } from '@cloudbeaver/core-executor'; import { CachedMapAllKey } from '@cloudbeaver/core-sdk'; +import { AuthenticationService } from '@cloudbeaver/plugin-authentication'; import { ConnectionInfoResource, Connection } from './ConnectionInfoResource'; import { ContainerResource, IStructContainers, ObjectContainer } from './ContainerResource'; import { EConnectionFeature } from './EConnectionFeature'; +export interface IConnectionExecutorData { + connections: string[]; + state: 'before' | 'after'; +} + @injectable() export class ConnectionsManagerService { readonly connectionExecutor: IExecutor; + readonly onDisconnect: IExecutor; + readonly onDelete: IExecutor; private disconnecting: boolean; @@ -27,11 +35,23 @@ export class ConnectionsManagerService { readonly connectionInfo: ConnectionInfoResource, readonly containerContainers: ContainerResource, private readonly notificationService: NotificationService, - private readonly commonDialogService: CommonDialogService + private readonly commonDialogService: CommonDialogService, + private readonly authenticationService: AuthenticationService, ) { this.disconnecting = false; + this.connectionExecutor = new Executor(null, (active, current) => active === current); + this.onDisconnect = new Executor(); + this.onDelete = new Executor(); + this.connectionExecutor.addHandler(() => connectionInfo.load(CachedMapAllKey)); + this.onDelete.before(this.onDisconnect); + this.authenticationService.onLogout.before(this.onDisconnect, state => ({ + connections: this.connectionInfo.values + .filter(connection => connection.connected) + .map(connection => connection.id), + state, + })); } async requireConnection(connectionId: string | null = null): Promise { @@ -93,7 +113,21 @@ export class ConnectionsManagerService { return; } + const contexts = await this.onDelete.execute({ + connections: [id], + state: 'before', + }); + + if (ExecutorInterrupter.isInterrupted(contexts)) { + return; + } + await this.connectionInfo.deleteConnection(id); + + this.onDelete.execute({ + connections: [id], + state: 'after', + }); } hasAnyConnection(connected?: boolean): boolean { @@ -140,11 +174,25 @@ export class ConnectionsManagerService { if (!connection || !connection.connected) { return; } + const contexts = await this.onDisconnect.execute({ + connections: [connection.id], + state: 'before', + }); + + if (ExecutorInterrupter.isInterrupted(contexts)) { + return; + } + const { controller, notification } = this.notificationService.processNotification(() => ProcessSnackbar, {}, { title: 'Disconnecting...' }); try { await this._closeConnectionAsync(connection); + notification.close(); + this.onDisconnect.execute({ + connections: [connection.id], + state: 'after', + }); } catch (exception: any) { controller.reject(exception); } diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index 81ba60c2b5..e4311e09d7 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -11,7 +11,7 @@ import { AppAuthService, AuthInfoService, AuthProviderContext, AuthProviderServi import { injectable, Bootstrap } from '@cloudbeaver/core-di'; import type { DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import type { IExecutorHandler } from '@cloudbeaver/core-executor'; +import { Executor, ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor'; import { ServerConfigResource, SessionDataResource } from '@cloudbeaver/core-root'; import { ScreenService } from '@cloudbeaver/core-routing'; import type { ObjectOrigin } from '@cloudbeaver/core-sdk'; @@ -19,8 +19,12 @@ import { openCenteredPopup } from '@cloudbeaver/core-utils'; import { AuthDialogService } from './Dialog/AuthDialogService'; +export type LogoutEventType = 'before' | 'after'; + @injectable() export class AuthenticationService extends Bootstrap { + readonly onLogout: Executor; + configureAuthProvider: (() => void) | null; configureIdentityProvider: (() => void) | null; @@ -40,6 +44,9 @@ export class AuthenticationService extends Bootstrap { private readonly serverConfigResource: ServerConfigResource, ) { super(); + + this.onLogout = new Executor(); + this.authPromise = null; this.configureAuthProvider = null; this.configureIdentityProvider = null; @@ -58,6 +65,12 @@ export class AuthenticationService extends Bootstrap { } async logout(): Promise { + const contexts = await this.onLogout.execute('before'); + + if (ExecutorInterrupter.isInterrupted(contexts)) { + return; + } + if (this.authInfoService.userAuthConfigurations.length > 0) { const userAuthConfiguration = this.authInfoService.userAuthConfigurations[0]; @@ -72,6 +85,8 @@ export class AuthenticationService extends Bootstrap { if (!this.administrationScreenService.isConfigurationMode) { this.screenService.navigateToRoot(); } + + await this.onLogout.execute('after'); } catch (exception: any) { this.notificationService.logException(exception, 'Can\'t logout'); } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts index ffccb953e0..58bcb8a421 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts @@ -12,15 +12,15 @@ import { ConnectionSchemaManagerService, isObjectCatalogProvider, isObjectSchemaProvider, DATA_CONTEXT_NAV_NODE, NavigationTabsService } from '@cloudbeaver/core-app'; -import { isConnectionProvider } from '@cloudbeaver/core-connections'; +import { ConnectionsManagerService, IConnectionExecutorData, isConnectionProvider } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult, RenameDialog } from '@cloudbeaver/core-dialogs'; -import type { IExecutorHandler } from '@cloudbeaver/core-executor'; +import { ExecutorInterrupter, IExecutionContextProvider, IExecutorHandler } from '@cloudbeaver/core-executor'; import { ExtensionUtils } from '@cloudbeaver/core-extensions'; import { ISessionAction, sessionActionContext, SessionActionService } from '@cloudbeaver/core-root'; import { ActionService, ACTION_RENAME, DATA_CONTEXT_MENU_NESTED, menuExtractActions, MenuService, ViewService } from '@cloudbeaver/core-view'; import { DATA_CONTEXT_CONNECTION } from '@cloudbeaver/plugin-connections'; -import { DATA_CONTEXT_SQL_EDITOR_STATE, LocalStorageSqlDataSource } from '@cloudbeaver/plugin-sql-editor'; +import { DATA_CONTEXT_SQL_EDITOR_STATE, LocalStorageSqlDataSource, SqlResultTabsService } from '@cloudbeaver/plugin-sql-editor'; import { ACTION_SQL_EDITOR_OPEN } from './ACTION_SQL_EDITOR_OPEN'; import { DATA_CONTEXT_SQL_EDITOR_TAB } from './DATA_CONTEXT_SQL_EDITOR_TAB'; @@ -41,12 +41,16 @@ export class SqlEditorBootstrap extends Bootstrap { private readonly menuService: MenuService, private readonly sessionActionService: SessionActionService, private readonly commonDialogService: CommonDialogService, - private readonly sqlEditorTabService: SqlEditorTabService + private readonly sqlEditorTabService: SqlEditorTabService, + private readonly sqlResultTabsService: SqlResultTabsService, + private readonly connectionsManagerService: ConnectionsManagerService, ) { super(); } register(): void { + this.connectionsManagerService.onDisconnect.addHandler(this.disconnectHandler.bind(this)); + this.mainMenuService.registerRootItem( { id: 'sql-editor', @@ -195,4 +199,24 @@ export class SqlEditorBootstrap extends Bootstrap { } } }; + + private async disconnectHandler( + data: IConnectionExecutorData, + contexts: IExecutionContextProvider + ) { + if (data.state === 'before') { + for (const tab of this.sqlEditorTabService.sqlEditorTabs) { + if (!data.connections.includes(tab.handlerState.executionContext?.connectionId ?? '')) { + continue; + } + + const canDisconnect = await this.sqlResultTabsService.canCloseResultTabs(tab.handlerState); + + if (!canDisconnect) { + ExecutorInterrupter.interrupt(contexts); + return; + } + } + } + } } diff --git a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts index 8f20f18eb0..2612bd9999 100644 --- a/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/MenuBootstrap.ts @@ -125,5 +125,5 @@ export class MenuBootstrap extends Bootstrap { } } - load(): void { } + load(): void | Promise { } } \ No newline at end of file