diff --git a/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts b/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts index 88afc0a944..3e9b0fc0e2 100644 --- a/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts +++ b/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts @@ -8,6 +8,7 @@ import { computed, observable } from 'mobx'; +import { AppAuthService } from '@cloudbeaver/core-authentication'; import { ConnectionInfoResource, ConnectionsManagerService, @@ -94,7 +95,10 @@ export class ConnectionSchemaManagerService { return; } - return this.connectionsManagerService.connectionObjectContainers.data.get(this.currentConnectionId); + return this.connectionsManagerService.connectionObjectContainers.get({ + connectionId: this.currentConnectionId, + catalogId: this.currentObjectCatalogId, + }); } get isConnectionChangeable(): boolean { @@ -120,7 +124,8 @@ export class ConnectionSchemaManagerService { private connectionInfo: ConnectionInfoResource, private connectionsManagerService: ConnectionsManagerService, private dbDriverResource: DBDriverResource, - private notificationService: NotificationService + private notificationService: NotificationService, + private appAuthService: AppAuthService ) { } @@ -130,6 +135,8 @@ export class ConnectionSchemaManagerService { this.navigationTabsService.onTabClose .subscribe(this.onTabClose.bind(this)); + + this.appAuthService.auth.addHandler(() => this.reset()); } /** @@ -164,6 +171,11 @@ export class ConnectionSchemaManagerService { await this.activeItem.changeSchemaId(schemaId, this.activeItem.context); } + private reset() { + this.activeItem = null; + this.activeItemHistory = []; + } + private async updateContainer(connectionId?: string, catalogId?: string): Promise { if (!connectionId) { connectionId = this.currentConnectionId; @@ -213,7 +225,10 @@ export class ConnectionSchemaManagerService { this.setActiveItem(item); } - private onTabClose(tab: ITab) { + private onTabClose(tab: ITab | undefined) { + if (!tab) { + return; + } this.removeActiveItem(tab.id); } diff --git a/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelectorController.ts b/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelectorController.ts index b1d5a4c822..c0ac63684f 100644 --- a/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelectorController.ts +++ b/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelectorController.ts @@ -48,7 +48,9 @@ export class ConnectionSelectorController { } get isObjectContainerSelectorVisible() { - return this.getObjectContainerItems().length !== 0 && !this.objectContainerSelectionDisabled; + return this.currentConnection?.connected + && this.getObjectContainerItems().length !== 0 + && !this.objectContainerSelectionDisabled; } @computed get objectContainerSelectionDisabled(): boolean { diff --git a/webapp/packages/core-app/src/shared/NavigationTabs/ITab.ts b/webapp/packages/core-app/src/shared/NavigationTabs/ITab.ts index 47429dbac7..3574c4d8c9 100644 --- a/webapp/packages/core-app/src/shared/NavigationTabs/ITab.ts +++ b/webapp/packages/core-app/src/shared/NavigationTabs/ITab.ts @@ -14,5 +14,6 @@ export interface ITabOptions { export interface ITab extends ITabOptions { id: string; + userId: string; restored: boolean; } diff --git a/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts b/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts index 5a3eaf7a09..87cf0f687d 100644 --- a/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts +++ b/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts @@ -12,9 +12,9 @@ import { } from 'mobx'; import { Subject } from 'rxjs'; +import { AppAuthService, UserInfoResource } from '@cloudbeaver/core-authentication'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { SessionService } from '@cloudbeaver/core-root'; import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; import { IActiveView } from '@cloudbeaver/core-view'; @@ -34,29 +34,42 @@ const NAVIGATION_TABS_BASE_KEY = 'navigation_tabs'; export class NavigationTabsService { @observable handlers = new Map(); @observable tabsMap = new Map(); - @observable state: TabsState = { - tabs: [], - history: [], - currentId: '', - }; + @observable state = new Map(); @computed get currentTabId(): string { - return this.state.currentId; + return this.userTabsState.currentId; } @computed get tabIdList(): string[] { - return this.state.tabs; + return Array.from(this.tabsMap.values()) + .filter(tab => tab.restored && tab.userId === this.userInfoResource.getId()) + .map(tab => tab.id); + } + + get userTabsState(): TabsState { + const userId = this.userInfoResource.getId(); + + if (!this.state.has(userId)) { + this.state.set(userId, { + tabs: [], + history: [], + currentId: '', + }); + } + + return this.state.get(userId)!; } private tabSelectSubject = new Subject(); - private tabCloseSubject = new Subject(); + private tabCloseSubject = new Subject(); readonly onTabSelect = this.tabSelectSubject.asObservable(); readonly onTabClose = this.tabCloseSubject.asObservable(); constructor( private notificationService: NotificationService, private autoSaveService: LocalStorageSaveService, - private sessionService: SessionService + private userInfoResource: UserInfoResource, + appAuthService: AppAuthService ) { this.autoSaveService.withAutoSave( this.tabsMap, @@ -67,6 +80,7 @@ export class NavigationTabsService { if ( typeof value.id === 'string' && typeof value.handlerId === 'string' + && typeof value.userId === 'string' ) { value.restored = false; map[key] = value; @@ -76,12 +90,36 @@ export class NavigationTabsService { } ); - this.autoSaveService.withAutoSave(this.state, NAVIGATION_TABS_BASE_KEY); + this.autoSaveService.withAutoSave( + this.state, + NAVIGATION_TABS_BASE_KEY, + (json): IKeyValueMap => { + const map: IKeyValueMap = {}; + for (const [key, value] of Object.entries(json as IKeyValueMap)) { + if ( + typeof value.currentId === 'string' + && Array.isArray(value.history) + && Array.isArray(value.tabs) + ) { + map[key] = value; + } + } + return map; + } + ); + + appAuthService.auth + .addHandler(() => this.unloadTabs()) + .addPostHandler(async state => { + if (state || appAuthService.authenticated) { + await this.restoreTabs(); + } + }); } @action openTab(tab: ITab, isSelected?: boolean): void { this.tabsMap.set(tab.id, tab); - this.state.tabs.push(tab.id); + this.userTabsState.tabs.push(tab.id); if (isSelected) { this.selectTab(tab.id); @@ -90,17 +128,20 @@ export class NavigationTabsService { @action async selectTab(tabId: string, skipHandlers?: boolean): Promise { if (tabId === '') { - this.state.currentId = ''; + this.userTabsState.currentId = ''; + } + if (!this.userTabsState.tabs.includes(tabId)) { + return; } const tab = this.tabsMap.get(tabId); if (!tab) { return; } - if (this.state.currentId !== tabId) { - this.state.history = this.state.history.filter(id => id !== tabId); - this.state.history.unshift(tabId); - this.state.currentId = tabId; + if (this.userTabsState.currentId !== tabId) { + this.userTabsState.history = this.userTabsState.history.filter(id => id !== tabId); + this.userTabsState.history.unshift(tabId); + this.userTabsState.currentId = tabId; } if (!skipHandlers) { @@ -111,18 +152,22 @@ export class NavigationTabsService { } @action async closeTab(tabId: string, skipHandlers?: boolean): Promise { + if (!this.userTabsState.tabs.includes(tabId)) { + return; + } + const tab = this.tabsMap.get(tabId); if (tab && !skipHandlers) { await this.callHandlerCallback(tab, handler => handler.onClose); } this.tabCloseSubject.next(tab); - this.state.history = this.state.history.filter(id => id !== tabId); + this.userTabsState.history = this.userTabsState.history.filter(id => id !== tabId); this.tabsMap.delete(tabId); - this.state.tabs = this.state.tabs.filter(id => id !== tabId); + this.userTabsState.tabs = this.userTabsState.tabs.filter(id => id !== tabId); - if (this.state.currentId === tabId) { - this.selectTab(this.state.history.shift() ?? '', skipHandlers); + if (this.userTabsState.currentId === tabId) { + this.selectTab(this.userTabsState.history.shift() ?? '', skipHandlers); } } @@ -182,7 +227,7 @@ export class NavigationTabsService { findTab(predicate: (tab: ITab) => boolean): ITab | null; findTab(predicate: (tab: ITab) => boolean): ITab | null { for (const tab of this.tabsMap.values()) { - if (predicate(tab)) { + if (tab.restored && tab.userId === this.userInfoResource.getId() && predicate(tab)) { return tab; } } @@ -193,23 +238,28 @@ export class NavigationTabsService { findTabs(predicate: (tab: ITab) => tab is ITab): Generator>; * findTabs(predicate: (tab: ITab) => boolean): Generator { for (const tab of this.tabsMap.values()) { - if (predicate(tab)) { + if (tab.restored && tab.userId === this.userInfoResource.getId() && predicate(tab)) { yield tab; } } } - // must be executed with low priority, because this call runs many requests to backend and blocks others - async restoreTabs(): Promise { - const removedTabs: string[] = []; - const session = await this.sessionService.session.load(); - - for (const tabId of this.state.tabs) { - if (session?.cacheExpired) { - removedTabs.push(tabId); - continue; + @action private async unloadTabs() { + for (const tab of this.tabsMap.values()) { + if (tab.userId !== this.userInfoResource.getId()) { + if (tab.restored) { + await this.callHandlerCallback(tab, handler => handler.onClose); + tab.restored = false; + } } + } + } + // must be executed with low priority, because this call runs many requests to backend and blocks others + private async restoreTabs(): Promise { + const removedTabs: string[] = []; + + for (const tabId of this.userTabsState.tabs) { const tab = this.tabsMap.get(tabId); if (!tab) { removedTabs.push(tabId); @@ -227,12 +277,12 @@ export class NavigationTabsService { this.closeTab(tabId, true); } - if (this.tabsMap.has(this.state.currentId)) { - this.selectTab(this.state.currentId); + if (this.tabsMap.has(this.userTabsState.currentId)) { + this.selectTab(this.userTabsState.currentId); } } - navigationTabContext = (): ITabNavigationContext => new TabNavigationContext(this); + navigationTabContext = (): ITabNavigationContext => new TabNavigationContext(this, this.userInfoResource); private async callHandlerCallback(tab: ITab, selector: (handler: TabHandler) => TabHandlerEvent | undefined) { const handler = this.handlers.get(tab.handlerId); diff --git a/webapp/packages/core-app/src/shared/NavigationTabs/TabNavigationContext.ts b/webapp/packages/core-app/src/shared/NavigationTabs/TabNavigationContext.ts index 1736316a8c..67196575c0 100644 --- a/webapp/packages/core-app/src/shared/NavigationTabs/TabNavigationContext.ts +++ b/webapp/packages/core-app/src/shared/NavigationTabs/TabNavigationContext.ts @@ -8,6 +8,7 @@ import { observable } from 'mobx'; +import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { uuid } from '@cloudbeaver/core-utils'; import { ITab, ITabOptions } from './ITab'; @@ -22,15 +23,15 @@ export interface ITabNavigationContext { } export class TabNavigationContext implements ITabNavigationContext { - get isNewlyCreated() { + get isNewlyCreated(): boolean { return this._isNewlyCreated; } - get handlerPriority() { + get handlerPriority(): number { return this._handlerPriority; } - get tab() { + get tab(): ITab | null { return this._tab; } @@ -38,11 +39,15 @@ export class TabNavigationContext implements ITabNavigationContext { private _handlerPriority = 0; private _tab: ITab | null = null; - constructor(private navigationTabsService: NavigationTabsService) { } + constructor( + private navigationTabsService: NavigationTabsService, + private userInfoResource: UserInfoResource + ) { } openNewTab(options: ITabOptions): ITab { this._tab = observable({ id: uuid(), + userId: this.userInfoResource.getId(), restored: true, ...options, }); @@ -51,7 +56,7 @@ export class TabNavigationContext implements ITabNavigationContext { return this._tab; } - registerTab(tab: ITab) { + registerTab(tab: ITab): void { this._tab = tab; } } diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts index e29fb20924..2ddc7adf01 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts @@ -6,6 +6,7 @@ * you may not use this file except in compliance with the License. */ +import { AppAuthService } from '@cloudbeaver/core-authentication'; import { ConnectionAuthService, Connection, ConnectionInfoResource } from '@cloudbeaver/core-connections'; @@ -13,7 +14,7 @@ import { injectable, Bootstrap } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { IExecutor, Executor, IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { - PermissionsService, EPermission, SessionResource, ServerService + PermissionsService, EPermission, ServerService } from '@cloudbeaver/core-root'; import { GraphQLService, resourceKeyList, ResourceKey, ResourceKeyUtils @@ -69,6 +70,7 @@ export interface INavNodeId { export interface INodeNavigationContext { type: NavigationType; + connection: Connection | undefined; nodeId: string; parentId: string; folderId: string; @@ -88,7 +90,6 @@ export interface INodeNavigationData { @injectable() export class NavNodeManagerService extends Bootstrap { readonly navigator: IExecutor; - private activeNavigationNodes: string[]; constructor( private graphQLService: GraphQLService, @@ -98,26 +99,23 @@ export class NavNodeManagerService extends Bootstrap { readonly navNodeInfoResource: NavNodeInfoResource, private connectionAuthService: ConnectionAuthService, private notificationService: NotificationService, - private sessionResource: SessionResource, - private serverService: ServerService + private serverService: ServerService, + private appAuthService: AppAuthService ) { super(); - this.activeNavigationNodes = []; this.navigator = new Executor( { type: NavigationType.open, nodeId: ROOT_NODE_PATH, parentId: ROOT_NODE_PATH, - } + }, + (active, current) => active.nodeId === current.nodeId ) - .addHandler(this.navigateHandler.bind(this)) - .addPostHandler(({ nodeId }) => { - this.activeNavigationNodes = this.activeNavigationNodes.filter(id => id !== nodeId); - }); + .addHandler(this.navigateHandler.bind(this)); } register(): void { - this.sessionResource.onDataUpdate.addHandler(this.refreshRoot.bind(this)); + this.appAuthService.auth.addHandler(this.refreshRoot.bind(this)); this.connectionInfo.onItemAdd.addHandler(this.connectionUpdateHandler.bind(this)); this.connectionInfo.onItemDelete.addHandler(this.connectionRemoveHandler.bind(this)); this.connectionInfo.onConnectionCreate.addHandler(this.connectionCreateHandler.bind(this)); @@ -261,8 +259,17 @@ export class NavNodeManagerService extends Bootstrap { let folderId = ''; let name: string | undefined; let icon: string | undefined; + let connection: Connection | undefined; - if (NodeManagerUtils.isDatabaseObject(nodeId)) { + const nodeInfo = this.getNodeContainerInfo(nodeId); + + if (nodeInfo.connectionId) { + // connection node id differs from connection id + const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(nodeInfo.connectionId); + connection = this.connectionInfo.get(connectionId); + } + + if (NodeManagerUtils.isDatabaseObject(nodeId) && connection?.connected) { const node = await this.loadNode({ nodeId, parentId }); name = node.name; @@ -309,6 +316,7 @@ export class NavNodeManagerService extends Bootstrap { return { type: data.type, + connection, nodeId, parentId, folderId, @@ -386,20 +394,12 @@ export class NavNodeManagerService extends Bootstrap { contexts: IExecutionContextProvider // eslint-disable-next-line @typescript-eslint/no-invalid-void-type ): Promise { - if (this.activeNavigationNodes.includes(data.nodeId)) { - return false; - } - - this.activeNavigationNodes.push(data.nodeId); - const nodeInfo = await contexts.getContext(this.navigationNavNodeContext); - if (NodeManagerUtils.isDatabaseObject(nodeInfo.nodeId)) { + if (NodeManagerUtils.isDatabaseObject(nodeInfo.nodeId) && nodeInfo.connection) { let connection: Connection | undefined; try { - connection = await this.connectionAuthService.auth( - NodeManagerUtils.nodeIdToConnectionId(nodeInfo.nodeId) - ); + connection = await this.connectionAuthService.auth(nodeInfo.connection.id); } catch (exception) { this.notificationService.logException(exception); throw exception; diff --git a/webapp/packages/core-authentication/src/AppAuthService.ts b/webapp/packages/core-authentication/src/AppAuthService.ts new file mode 100644 index 0000000000..6ad91f76fa --- /dev/null +++ b/webapp/packages/core-authentication/src/AppAuthService.ts @@ -0,0 +1,62 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { injectable, Bootstrap } from '@cloudbeaver/core-di'; +import { Executor, IExecutor } from '@cloudbeaver/core-executor'; +import { ServerService, SessionService } from '@cloudbeaver/core-root'; + +import { UserInfoResource } from './UserInfoResource'; + +@injectable() +export class AppAuthService extends Bootstrap { + get authenticated(): boolean { + const config = this.serverService.config.data; + const user = this.userInfoResource.data; + + return !!config?.anonymousAccessEnabled + || !!config?.configurationMode + || user !== null; + } + + readonly auth: IExecutor; + constructor( + private serverService: ServerService, + private sessionService: SessionService, + private userInfoResource: UserInfoResource, + ) { + super(); + this.auth = new Executor(); + this.sessionService.session.onDataUpdate.addHandler(async () => { await this.authUser(); }); + } + + async isAuthNeeded(): Promise { + const config = await this.serverService.config.load(); + if (!config) { + throw new Error('Can\'t configure Authentication'); + } + + const user = await this.userInfoResource.load(); + + return !!config.authenticationEnabled + && !config.configurationMode + && !config.anonymousAccessEnabled + && user === null; + } + + async authUser(): Promise { + const userInfo = await this.userInfoResource.load(); + + const state = userInfo !== null; + await this.auth.execute(state); + return state; + } + + register(): void { } + + load(): void { } +} diff --git a/webapp/packages/core-authentication/src/UserInfoResource.ts b/webapp/packages/core-authentication/src/UserInfoResource.ts index 113ab348aa..133ccd6691 100644 --- a/webapp/packages/core-authentication/src/UserInfoResource.ts +++ b/webapp/packages/core-authentication/src/UserInfoResource.ts @@ -26,7 +26,11 @@ export class UserInfoResource extends CachedDataResource this.markOutdated()); - this.sessionResource.onDataUpdate.addHandler(() => { this.load(); }); + this.sessionResource.onDataUpdate.addHandler(async () => { await this.load(); }); + } + + getId(): string { + return this.data?.userId || 'anonymous'; } isLoaded(): boolean { @@ -59,6 +63,7 @@ export class UserInfoResource extends CachedDataResource { + await this.sessionResource.load(); const { user } = await this.graphQLService.sdk.getSessionUser(); this.loaded = true; diff --git a/webapp/packages/core-authentication/src/index.ts b/webapp/packages/core-authentication/src/index.ts index 5d67bb70cb..312394d3ea 100644 --- a/webapp/packages/core-authentication/src/index.ts +++ b/webapp/packages/core-authentication/src/index.ts @@ -1,3 +1,4 @@ +export * from './AppAuthService'; export * from './AuthInfoService'; export * from './AuthProviderService'; export * from './AuthProvidersResource'; diff --git a/webapp/packages/core-bootstrap/src/AppBootstrap.ts b/webapp/packages/core-bootstrap/src/AppBootstrap.ts index e03829a520..edde3ff4f2 100644 --- a/webapp/packages/core-bootstrap/src/AppBootstrap.ts +++ b/webapp/packages/core-bootstrap/src/AppBootstrap.ts @@ -52,6 +52,6 @@ export class AppBootstrap { async doAfterPluginsInit() { // todo this should be moved to the NavigationTabs component creation phase but now it leads to bugs - await this.navigationTabsService.restoreTabs(); + // await this.navigationTabsService.restoreTabs(); } } diff --git a/webapp/packages/core-bootstrap/src/manifest.ts b/webapp/packages/core-bootstrap/src/manifest.ts index 5c203d2b02..4191b0481e 100644 --- a/webapp/packages/core-bootstrap/src/manifest.ts +++ b/webapp/packages/core-bootstrap/src/manifest.ts @@ -38,6 +38,7 @@ import { SessionExpiredDialogService } from '@cloudbeaver/core-app'; import { + AppAuthService, AuthInfoService, AuthProviderService, AuthProvidersResource, @@ -80,7 +81,8 @@ import { ServerSettingsService, ServerConfigResource, PermissionsResource, - SessionResource, SessionExpireService + SessionResource, + SessionExpireService } from '@cloudbeaver/core-root'; import { RouterService, ScreenService } from '@cloudbeaver/core-routing'; import { EnvironmentService, GraphQLService } from '@cloudbeaver/core-sdk'; @@ -108,6 +110,7 @@ export const coreManifest: PluginManifest = { ProductSettingsService, ProductManagerService, PluginManagerService, + AppAuthService, AuthInfoService, AuthProviderService, AuthProvidersResource, diff --git a/webapp/packages/core-connections/src/ConnectionInfoResource.ts b/webapp/packages/core-connections/src/ConnectionInfoResource.ts index 293459c35b..da8ed079eb 100644 --- a/webapp/packages/core-connections/src/ConnectionInfoResource.ts +++ b/webapp/packages/core-connections/src/ConnectionInfoResource.ts @@ -8,6 +8,7 @@ import { action } from 'mobx'; +import { AppAuthService } from '@cloudbeaver/core-authentication'; import { injectable } from '@cloudbeaver/core-di'; import { Executor, IExecutor } from '@cloudbeaver/core-executor'; import { SessionResource } from '@cloudbeaver/core-root'; @@ -27,6 +28,7 @@ export class ConnectionInfoResource extends CachedMapResource !this.sessionUpdate); this.onItemDelete.addHandler(() => !this.sessionUpdate); - sessionResource.onDataUpdate.addHandler(() => this.refreshSession(true)); + sessionResource.onDataOutdated.addHandler(() => this.markOutdated()); + appAuthService.auth.addHandler(() => this.refreshSession(true)); } @action async refreshSession(sessionUpdate?: boolean): Promise { diff --git a/webapp/packages/core-connections/src/ConnectionsManagerService.ts b/webapp/packages/core-connections/src/ConnectionsManagerService.ts index 2da9d90c6f..e832737891 100644 --- a/webapp/packages/core-connections/src/ConnectionsManagerService.ts +++ b/webapp/packages/core-connections/src/ConnectionsManagerService.ts @@ -12,6 +12,7 @@ import { ProcessSnackbar } from '@cloudbeaver/core-blocks'; import { injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; +import { Executor, IExecutionContextProvider, IExecutor } from '@cloudbeaver/core-executor'; import { ConnectionAuthService } from './ConnectionAuthService'; import { ConnectionInfoResource, Connection } from './ConnectionInfoResource'; @@ -23,6 +24,7 @@ export class ConnectionsManagerService { onOpenConnection = new Subject(); onCloseConnection = new Subject(); private disconnecting: boolean; + private connectionExecutor: IExecutor; constructor( readonly connectionInfo: ConnectionInfoResource, @@ -32,28 +34,14 @@ export class ConnectionsManagerService { private commonDialogService: CommonDialogService ) { this.disconnecting = false; + this.connectionExecutor = new Executor(null, (active, current) => active === current); + this.connectionExecutor.addHandler(this.connectionDialog.bind(this)); } - async requireConnection(connectionId?: string) { - if (!connectionId) { - if (!this.hasAnyConnection()) { - return null; - } - connectionId = Array.from(this.connectionInfo.data.values())[0].id; - } + async requireConnection(connectionId: string | null = null): Promise { + const context = await this.connectionExecutor.execute(connectionId); - try { - const connection = await this.connectionAuthService.auth(connectionId); - - if (!connection.connected) { - return null; - } - - return connection; - } catch (exception) { - this.notificationService.logException(exception); - throw exception; - } + return (await context.getContext(this.connectionContext)).connection; } async addOpenedConnection(connection: Connection) { @@ -65,7 +53,7 @@ export class ConnectionsManagerService { objectCatalogId: string, objectSchemaId?: string ): ObjectContainer | undefined { - const objectContainers = this.connectionObjectContainers.data.get(connectionId); + const objectContainers = this.connectionObjectContainers.get({ connectionId, catalogId: objectCatalogId }); if (!objectContainers) { return; } @@ -101,6 +89,34 @@ export class ConnectionsManagerService { return !!Array.from(this.connectionInfo.data.values()).length; } + private connectionContext() { + return { + connection: null as (Connection | null), + }; + } + + private async connectionDialog(connectionId: string | null, context: IExecutionContextProvider) { + const connection = await context.getContext(this.connectionContext); + if (!connectionId) { + if (!this.hasAnyConnection()) { + return; + } + connectionId = Array.from(this.connectionInfo.data.values())[0].id; + } + + try { + const tempConnection = await this.connectionAuthService.auth(connectionId); + + if (!tempConnection.connected) { + return; + } + connection.connection = tempConnection; + } catch (exception) { + this.notificationService.logException(exception); + throw exception; + } + } + private async _closeConnectionAsync(connection: Connection) { if (!connection.connected) { return; @@ -145,7 +161,7 @@ export class ConnectionsManagerService { async loadObjectContainer(connectionId: string, catalogId?: string): Promise { await this.connectionObjectContainers.load({ connectionId, catalogId }); - return this.connectionObjectContainers.data.get(connectionId)!; + return this.connectionObjectContainers.get({ connectionId, catalogId })!; } private async afterConnectionClose(id: string) { diff --git a/webapp/packages/core-connections/src/ContainerResource.ts b/webapp/packages/core-connections/src/ContainerResource.ts index a16bc21678..fd21b24628 100644 --- a/webapp/packages/core-connections/src/ContainerResource.ts +++ b/webapp/packages/core-connections/src/ContainerResource.ts @@ -10,32 +10,104 @@ import { injectable } from '@cloudbeaver/core-di'; import { GraphQLService, CachedDataResource, - DatabaseObjectInfo + DatabaseObjectInfo, ICachedResourceMetadata, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; +import { MetadataMap } from '@cloudbeaver/core-utils'; + +import { ConnectionInfoResource } from './ConnectionInfoResource'; export type ObjectContainer = Pick; +type DataValue = MetadataMap>; + +const defaultCatalog = 'default'; interface ObjectContainerParams { connectionId: string; catalogId?: string; } +interface ObjectContainerMetadata extends ICachedResourceMetadata { + outdatedData: string[]; + loadingData: string[]; +} + @injectable() -export class ContainerResource extends CachedDataResource, ObjectContainerParams> { - constructor(private graphQLService: GraphQLService) { - super(new Map()); +export class ContainerResource extends CachedDataResource< +DataValue, +ObjectContainerParams, +string +> { + metadata: MetadataMap; + + constructor( + private graphQLService: GraphQLService, + private connectionInfoResource: ConnectionInfoResource + ) { + super(new MetadataMap(() => ({ }))); + this.metadata = new MetadataMap(() => ({ outdated: true, loading: false, outdatedData: [], loadingData: [] })); + this.connectionInfoResource.onItemDelete.addHandler( + key => ResourceKeyUtils.forEach(key, key => this.data.delete(key)) + ); + this.connectionInfoResource.onItemAdd.addHandler( + key => ResourceKeyUtils.forEach(key, key => { + if (!this.connectionInfoResource.get(key)?.connected) { + this.data.delete(key); + } + }) + ); } - isLoaded({ connectionId }: ObjectContainerParams): boolean { - return this.data.has(connectionId); + get({ connectionId, catalogId = defaultCatalog }: ObjectContainerParams): ObjectContainer[] | undefined { + return this.data.get(connectionId)[catalogId]; } - protected async loader({ connectionId, catalogId }: ObjectContainerParams): Promise> { + isLoaded({ connectionId, catalogId = defaultCatalog }: ObjectContainerParams): boolean { + return catalogId in this.data.get(connectionId); + } + + isOutdated({ connectionId, catalogId = defaultCatalog }: ObjectContainerParams): boolean { + const metadata = this.metadata.get(connectionId); + return metadata.outdatedData.includes(catalogId); + } + + isDataLoading({ connectionId, catalogId = defaultCatalog }: ObjectContainerParams): boolean { + const metadata = this.metadata.get(connectionId); + return metadata.loadingData.includes(catalogId); + } + + markDataLoading({ connectionId, catalogId = defaultCatalog }: ObjectContainerParams): void { + const metadata = this.metadata.get(connectionId); + if (!metadata.loadingData.includes(catalogId)) { + metadata.loadingData.push(catalogId); + } + } + + markDataLoaded({ connectionId, catalogId = defaultCatalog }: ObjectContainerParams): void { + const metadata = this.metadata.get(connectionId); + metadata.loadingData = metadata.loadingData.filter(id => id !== catalogId); + } + + markOutdated(param: ObjectContainerParams): void { + const { connectionId, catalogId = defaultCatalog } = param; + const metadata = this.metadata.get(connectionId); + if (!metadata.outdatedData.includes(catalogId)) { + metadata.outdatedData.push(catalogId); + } + this.onDataOutdated.execute(param); + } + + markUpdated({ connectionId, catalogId = defaultCatalog }: ObjectContainerParams): void { + const metadata = this.metadata.get(connectionId); + metadata.outdatedData = metadata.outdatedData.filter(id => id !== catalogId); + } + + protected async loader({ connectionId, catalogId }: ObjectContainerParams): Promise { const { navGetStructContainers } = await this.graphQLService.sdk.navGetStructContainers({ connectionId, catalogId, }); - this.data.set(connectionId, [...navGetStructContainers.schemaList, ...navGetStructContainers.catalogList]); + const value = this.data.get(connectionId); + value[catalogId || defaultCatalog] = [...navGetStructContainers.schemaList, ...navGetStructContainers.catalogList]; return this.data; } diff --git a/webapp/packages/core-connections/src/useConnectionInfo.ts b/webapp/packages/core-connections/src/useConnectionInfo.ts index b69a44f2d3..bae2261d0b 100644 --- a/webapp/packages/core-connections/src/useConnectionInfo.ts +++ b/webapp/packages/core-connections/src/useConnectionInfo.ts @@ -11,8 +11,10 @@ import { useCallback } from 'react'; import { useService } from '@cloudbeaver/core-di'; import { ConnectionInfoResource } from './ConnectionInfoResource'; +import { ConnectionsManagerService } from './ConnectionsManagerService'; export function useConnectionInfo(connectionId: string) { + const manager = useService(ConnectionsManagerService); const service = useService(ConnectionInfoResource); const connectionInfo = service.get(connectionId); @@ -20,6 +22,7 @@ export function useConnectionInfo(connectionId: string) { const refresh = useCallback(() => service.refresh(connectionId), [service, connectionId]); const isLoading = useCallback(() => service.isDataLoading(connectionId), [service, connectionId]); const isLoaded = useCallback(() => service.isLoaded(connectionId), [service, connectionId]); + const connect = useCallback(() => manager.requireConnection(connectionId), [manager, connectionId]); return { connectionInfo, @@ -27,5 +30,6 @@ export function useConnectionInfo(connectionId: string) { isLoaded, load, refresh, + connect, }; } diff --git a/webapp/packages/core-executor/src/Executor.ts b/webapp/packages/core-executor/src/Executor.ts index 371682926d..264aa2f232 100644 --- a/webapp/packages/core-executor/src/Executor.ts +++ b/webapp/packages/core-executor/src/Executor.ts @@ -11,7 +11,7 @@ import { IExecutor } from './IExecutor'; import { IExecutorHandler } from './IExecutorHandler'; import { BlockedExecution, TaskScheduler } from './TaskScheduler/TaskScheduler'; -export class Executor implements IExecutor { +export class Executor implements IExecutor { private handlers: Array> = []; private postHandlers: Array> = []; private scheduler: TaskScheduler; diff --git a/webapp/packages/core-executor/src/IExecutor.ts b/webapp/packages/core-executor/src/IExecutor.ts index 04a0c7397a..e8ca3ab4e7 100644 --- a/webapp/packages/core-executor/src/IExecutor.ts +++ b/webapp/packages/core-executor/src/IExecutor.ts @@ -9,7 +9,7 @@ import { IExecutionContextProvider } from './ExecutionContext'; import { IExecutorHandler } from './IExecutorHandler'; -export interface IExecutor { +export interface IExecutor { execute: (data: T) => Promise>; addHandler: (handler: IExecutorHandler) => this; removeHandler: (handler: IExecutorHandler) => void; diff --git a/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts b/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts index 3d15643b67..759f4365df 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts @@ -14,7 +14,8 @@ import { CachedResource } from './CachedResource'; export abstract class CachedDataResource< TData, TParam, -> extends CachedResource { + TKey = TParam, +> extends CachedResource { async refresh(param: TParam): Promise { await this.loadData(param, true); return this.data; diff --git a/webapp/packages/core-sdk/src/Resource/CachedResource.ts b/webapp/packages/core-sdk/src/Resource/CachedResource.ts index a9236f21c3..83a0cb340f 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedResource.ts @@ -21,6 +21,7 @@ export interface ICachedResourceMetadata { export abstract class CachedResource< TData, TParam, + TKey = TParam > { @observable data: TData; @@ -28,7 +29,7 @@ export abstract class CachedResource< readonly onDataOutdated: IExecutor; readonly onDataUpdate: IExecutor; - protected metadata: MetadataMap; + protected metadata: MetadataMap; @observable protected loading = false; @@ -48,36 +49,36 @@ export abstract class CachedResource< abstract isLoaded(param: TParam): boolean; - isOutdated(param: TParam): boolean { - return this.metadata.get(param).outdated; - } - isLoading(): boolean { return this.loading; } + isOutdated(param: TParam): boolean { + return this.metadata.get(param as unknown as TKey).outdated; + } + isDataLoading(param: TParam): boolean { - return this.metadata.get(param).loading; + return this.metadata.get(param as unknown as TKey).loading; } markDataLoading(param: TParam): void { - const metadata = this.metadata.get(param); + const metadata = this.metadata.get(param as unknown as TKey); metadata.loading = true; } markDataLoaded(param: TParam): void { - const metadata = this.metadata.get(param); + const metadata = this.metadata.get(param as unknown as TKey); metadata.loading = false; } markOutdated(param: TParam): void { - const metadata = this.metadata.get(param); + const metadata = this.metadata.get(param as unknown as TKey); metadata.outdated = true; this.onDataOutdated.execute(param); } markUpdated(param: TParam): void { - const metadata = this.metadata.get(param); + const metadata = this.metadata.get(param as unknown as TKey); metadata.outdated = false; } diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index 3a6d98cf67..2b7a1bf3e9 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -8,10 +8,9 @@ import { AdministrationScreenService } from '@cloudbeaver/core-administration'; import { AppScreenService } from '@cloudbeaver/core-app'; -import { UserInfoResource } from '@cloudbeaver/core-authentication'; +import { AppAuthService, UserInfoResource } from '@cloudbeaver/core-authentication'; import { injectable, Bootstrap } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { ServerService, SessionService } from '@cloudbeaver/core-root'; import { ScreenService } from '@cloudbeaver/core-routing'; import { AuthDialogService } from './Dialog/AuthDialogService'; @@ -22,8 +21,7 @@ export class AuthenticationService extends Bootstrap { constructor( private screenService: ScreenService, private appScreenService: AppScreenService, - private serverService: ServerService, - private sessionService: SessionService, + private appAuthService: AppAuthService, private authDialogService: AuthDialogService, private userInfoResource: UserInfoResource, private notificationService: NotificationService, @@ -71,12 +69,7 @@ export class AuthenticationService extends Bootstrap { } private async requireAuthentication() { - const config = await this.serverService.config.load(); - if (!config) { - throw new Error('Can\'t configure Authentication'); - } - - if (config.anonymousAccessEnabled || !config.authenticationEnabled || config.configurationMode) { + if (!await this.appAuthService.isAuthNeeded()) { return; } @@ -84,7 +77,11 @@ export class AuthenticationService extends Bootstrap { } register(): void { - this.sessionService.session.onDataUpdate.addHandler(() => this.requireAuthentication()); + this.appAuthService.auth.addHandler(async state => { + if (!state) { + await this.requireAuthentication(); + } + }); this.appScreenService.activation.addHandler(() => this.requireAuthentication()); this.administrationScreenService.ensurePermissions.addHandler(async () => await this.auth(false)); } diff --git a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts index 0d1af397c9..eeeb0a2c8d 100644 --- a/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/ContainerDataSource.ts @@ -99,6 +99,15 @@ export class ContainerDataSource extends DatabaseDataSource { + if (this.executionContext) { + await this.graphQLService.sdk.sqlContextDestroy({ + connectionId: this.executionContext.connectionId, + contextId: this.executionContext.contextId, + }); + } + } + private async ensureContextCreated(): Promise { if (!this.executionContext) { if (!this.options) { diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts index e7b030993f..5e493d1963 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts @@ -105,18 +105,10 @@ export class DataViewerTabService { } private async handleTabRestore(tab: ITab) { - // if (!this.nodesManagerService.isNodeHasData(tab.handlerState.objectId)) { - // return; - // } - await this.navNodeManagerService.loadNode({ - nodeId: tab.handlerState.objectId, - parentId: tab.handlerState.parentId, - }); - // await this.dbObjectService.load(tab.handlerState.objectId); return true; } private handleTabClose(tab: ITab) { - this.dataViewerTableService.removeTableModel(tab.handlerState.objectId); + this.dataViewerTableService.removeTableModel(tab.id); } } diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts index edb90c694c..9c53065a08 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerTableService.ts @@ -28,7 +28,20 @@ export class DataViewerTableService { return this.tableViewerStorageService.has(tableId); } - removeTableModel(tableId: string): void { + async removeTableModel(tableId: string): Promise { + const model = this.tableViewerStorageService.get(tableId); + if (model) { + await model.dispose(); + + if (model.deprecatedModel.executionContext) { + try { + await this.graphQLService.sdk.sqlContextDestroy({ + connectionId: model.deprecatedModel.executionContext.connectionId, + contextId: model.deprecatedModel.executionContext.contextId, + }); + } catch {} + } + } this.tableViewerStorageService.remove(tableId); } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts index ce20bf093d..179b5c8eee 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts @@ -118,4 +118,6 @@ implements IDatabaseDataSource { abstract request(prevResults: TResult[]): TResult[] | Promise; abstract save(prevResults: TResult[], data: DataUpdate): Promise | TResult[]; + + abstract dispose(): Promise; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts index d53f78d83d..d79cbec8da 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts @@ -49,4 +49,5 @@ export interface IDatabaseDataSource Promise | TResult[]; cancel: () => Promise; + dispose: () => Promise; } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/DataModelWrapper.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/DataModelWrapper.ts index e2c133bb2e..7e8b3488dd 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/DataModelWrapper.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/DataModelWrapper.ts @@ -125,6 +125,10 @@ export class DataModelWrapper extends DatabaseDataModel { } }; + async dispose(): Promise { + await this.source.dispose(); + } + private showError(exception: any) { this.exception = null; this.hasDetails = false; diff --git a/webapp/packages/plugin-object-viewer/src/IObjectViewerTabState.ts b/webapp/packages/plugin-object-viewer/src/IObjectViewerTabState.ts index 5333288be8..e34ff8d490 100644 --- a/webapp/packages/plugin-object-viewer/src/IObjectViewerTabState.ts +++ b/webapp/packages/plugin-object-viewer/src/IObjectViewerTabState.ts @@ -7,6 +7,7 @@ */ export interface IObjectViewerTabState { + connectionId: string | undefined; objectId: string; parentId: string; parents: string[]; diff --git a/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx b/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx index f3cf0ef7e3..9d24801fc4 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx @@ -7,12 +7,14 @@ */ import { observer } from 'mobx-react'; +import { useCallback } from 'react'; import styled, { css } from 'reshadow'; -import { useChildren, TabHandlerPanelProps } from '@cloudbeaver/core-app'; +import { useChildren, TabHandlerPanelProps, NodeManagerUtils, NavigationTabsService } from '@cloudbeaver/core-app'; import { - Loader, TabsBox, TabPanel, TextPlaceholder + Loader, TabsBox, TabPanel, TextPlaceholder, Button } from '@cloudbeaver/core-blocks'; +import { useConnectionInfo } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { useStyles, composes } from '@cloudbeaver/core-theming'; @@ -39,8 +41,13 @@ const styles = composes( const stylesArray = [styles]; export const ObjectViewerPanel = observer(function ObjectViewerPanel({ - tab, handler, + tab, }: TabHandlerPanelProps) { + const { + connectionInfo, + connect, + } = useConnectionInfo(tab.handlerState.connectionId || ''); + const navigation = useService(NavigationTabsService); const style = useStyles(styles); const { children, isOutdated, isLoading, isLoaded, @@ -48,6 +55,21 @@ export const ObjectViewerPanel = observer(function ObjectViewerPanel({ const dbObjectPagesService = useService(DBObjectPageService); const pages = dbObjectPagesService.orderedPages; + const handleConnect = useCallback(async () => { + await connect(); + navigation.selectTab(tab.id); + }, [navigation, connect, tab]); + + if (connectionInfo && !connectionInfo.connected) { + return ( + + Connection required +
+ +
+ ); + } + if (!isLoaded() || (!isOutdated() && isLoading())) { return ; } diff --git a/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts b/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts index d4b616b263..9729ddbf95 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts @@ -43,7 +43,8 @@ export class ObjectViewerTabService { private dbObjectService: DBObjectService, private dbObjectPageService: DBObjectPageService, private notificationService: NotificationService, - private navigationTabsService: NavigationTabsService + private navigationTabsService: NavigationTabsService, + private connectionInfo: ConnectionInfoResource ) { this.tabHandler = this.navigationTabsService .registerTabHandler({ @@ -93,6 +94,7 @@ export class ObjectViewerTabService { tabInfo.openNewTab({ handlerId: objectViewerTabHandlerKey, handlerState: { + connectionId: nodeInfo.connection?.id, objectId: nodeInfo.nodeId, parentId: nodeInfo.parentId, parents: await nodeInfo.getParents(), @@ -140,7 +142,7 @@ export class ObjectViewerTabService { }; private async updateConnectionInfoTabs(key: ResourceKey) { - ResourceKeyUtils.forEach(key, async key => { + await ResourceKeyUtils.forEachAsync(key, async key => { const navNodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key); const connected = this.connectionInfoResource.get(key)?.connected; @@ -157,7 +159,7 @@ export class ObjectViewerTabService { } private async updateTabs(key: ResourceKey) { - ResourceKeyUtils.forEach(key, async key => { + await ResourceKeyUtils.forEachAsync(key, async key => { const tab = this.navigationTabsService.findTab( isObjectViewerTab(tab => tab.handlerState.objectId === key) ); @@ -169,7 +171,7 @@ export class ObjectViewerTabService { } private async removeTabs(key: ResourceKey) { - ResourceKeyUtils.forEach(key, async key => { + await ResourceKeyUtils.forEachAsync(key, async key => { const tab = this.navigationTabsService.findTab( isObjectViewerTab(tab => tab.handlerState.objectId === key) ); @@ -181,14 +183,7 @@ export class ObjectViewerTabService { } private getConnection(context: ITab) { - const nodeInfo = this.navNodeManagerService - .getNodeContainerInfo(context.handlerState.objectId); - - if (!nodeInfo.connectionId) { - return; - } - // connection node id differs from connection id - return NodeManagerUtils.connectionNodeIdToConnectionId(nodeInfo.connectionId); + return context.handlerState.connectionId; } private getDBObjectCatalog(context: ITab) { @@ -213,16 +208,32 @@ export class ObjectViewerTabService { private async selectObjectTab(tab: ITab) { try { + if (tab.handlerState.connectionId) { + const connection = await this.connectionInfo.load(tab.handlerState.connectionId); + + if (!connection.connected) { + return; + } + } + + for (const nodeId of tab.handlerState.parents) { + await this.navNodeManagerService.loadTree(nodeId); + } + + // TODO: must be loaded by info folder? + const node = await this.navNodeManagerService.loadNode({ + nodeId: tab.handlerState.objectId, + parentId: tab.handlerState.parentId, + }); + const currentPage = this.dbObjectPageService.getPage(tab.handlerState.pageId); if (currentPage) { await this.dbObjectPageService.selectPage(tab, currentPage); } - - // TODO: must be loaded by info folder? - await this.navNodeManagerService.loadNode({ - nodeId: tab.handlerState.objectId, - parentId: tab.handlerState.parentId, - }); + if (node) { + tab.handlerState.tabIcon = node.icon; + tab.handlerState.tabTitle = node.name; + } await this.dbObjectService.load(tab.handlerState.objectId); const children = await this.navNodeManagerService.loadTree(tab.handlerState.objectId); @@ -243,6 +254,7 @@ export class ObjectViewerTabService { if ( typeof tab.handlerState?.folderId === 'string' && typeof tab.handlerState.parentId === 'string' + && ['string', 'undefined'].includes(typeof tab.handlerState.connectionId) && Array.isArray(tab.handlerState.parents) && typeof tab.handlerState.objectId === 'string' && typeof tab.handlerState.pagesState === 'object' @@ -250,21 +262,15 @@ export class ObjectViewerTabService { && (!tab.handlerState.tabTitle || typeof tab.handlerState.tabTitle === 'string') ) { tab.handlerState.pagesState = observable.map(tab.handlerState.pagesState); + if (tab.handlerState.connectionId) { + const connection = this.connectionInfo.get(tab.handlerState.connectionId); - for (const nodeId of tab.handlerState.parents) { - await this.navNodeManagerService.loadTree(nodeId); + if (!connection) { + return false; + } } - const node = await this.navNodeManagerService.loadNode({ - nodeId: tab.handlerState.objectId, - parentId: tab.handlerState.parentId, - }); - if (node) { - tab.handlerState.tabIcon = node.icon; - tab.handlerState.tabTitle = node.name; - - return this.dbObjectPageService.restorePages(tab); - } + return this.dbObjectPageService.restorePages(tab); } return false; } diff --git a/webapp/packages/plugin-sql-editor/src/ISqlEditorGroupMetadata.ts b/webapp/packages/plugin-sql-editor/src/ISqlEditorGroupMetadata.ts index 4a2cab83dd..d5bc3e603a 100644 --- a/webapp/packages/plugin-sql-editor/src/ISqlEditorGroupMetadata.ts +++ b/webapp/packages/plugin-sql-editor/src/ISqlEditorGroupMetadata.ts @@ -13,11 +13,12 @@ import { SqlExecutionState } from './SqlExecutionState'; import { SQLQueryExecutionProcess } from './SqlResultTabs/SQLQueryExecutionProcess'; export interface ISqlEditorGroupMetadata { + resultDataProcess: SQLQueryExecutionProcess; start: ( context: SqlExecutionState, sqlQueryParams: ISqlQueryParams, filter: SqlDataFilter, dataFormat: ResultDataFormat, ) => Promise; - resultDataProcess: SQLQueryExecutionProcess; + dispose: (params: ISqlQueryParams) => Promise; } diff --git a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts index 1b7c60198f..25195cd5d3 100644 --- a/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts +++ b/webapp/packages/plugin-sql-editor/src/ISqlEditorTabState.ts @@ -34,7 +34,7 @@ export interface IQueryTabGroup { sqlQueryParams: ISqlQueryParams; } -export interface ISqlEditorTabState extends IExecutionContext { +export interface ISqlEditorTabState extends Partial { order: number; query: string; currentResultTabId?: string; diff --git a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts index 6ff5ca9b25..068c2f6bc1 100644 --- a/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts +++ b/webapp/packages/plugin-sql-editor/src/QueryDataSource.ts @@ -114,4 +114,12 @@ export class QueryDataSource extends DatabaseDataSource { + if (!this.options) { + return; + } + + await this.metadata.dispose(this.options.group.sqlQueryParams); + } } diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorController.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorController.tsx index d495d30858..c15be84aca 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorController.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorController.tsx @@ -18,8 +18,8 @@ import { SqlDialectInfo } from '@cloudbeaver/core-sdk'; import { ISqlEditorTabState } from '../ISqlEditorTabState'; import { SqlDialectInfoService } from '../SqlDialectInfoService'; +import { SqlEditorService } from '../SqlEditorService'; import { SqlResultTabsService } from '../SqlResultTabs/SqlResultTabsService'; -import { SqlEditorService } from './SqlEditorService'; // allows to bypass rollup-commonjs error " 'showHint' is not exported by codemirror.js " const showHint = CodeMirror.showHint; @@ -27,6 +27,10 @@ const showHint = CodeMirror.showHint; @injectable() export class SqlEditorController implements IInitializableController { @computed get dialect(): SqlDialectInfo | undefined { + if (!this.tab.handlerState.connectionId) { + return undefined; + } + return this.sqlDialectInfoService.getDialectInfo(this.tab.handlerState.connectionId); } @@ -125,6 +129,14 @@ export class SqlEditorController implements IInitializableController { private getHandleAutocomplete(): AsyncHintFunction { const handleAutocomplete: AsyncHintFunction = (editor, callback) => { + if (!this.tab.handlerState.connectionId) { + console.error('getHandleAutocomplete connectionId is not provided'); + return; + } + if (!this.tab.handlerState.contextId) { + console.error('getHandleAutocomplete contextId is not provided'); + return; + } const cursor = editor.getCursor('from'); const cursorPosition = getAbsolutePosition(editor, cursor); const [from, to] = getWordRange(editor, cursor); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorService.ts deleted file mode 100644 index 36cd044cb3..0000000000 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorService.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 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 { injectable } from '@cloudbeaver/core-di'; -import { GraphQLService, QuerySqlCompletionProposalsQuery } from '@cloudbeaver/core-sdk'; - -@injectable() -export class SqlEditorService { - constructor(private gql: GraphQLService) { - } - - async getAutocomplete( - connectionId: string, - contextId: string, - query: string, - cursor: number - ): Promise { - const result = await this.gql.sdk.querySqlCompletionProposals({ - connectionId, - contextId, - query, - position: cursor, - }); - - return result.sqlCompletionProposals; - } -} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts index f3194775d7..fc7220aca4 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts @@ -16,7 +16,7 @@ import { } from '@cloudbeaver/core-app'; import { ConnectionsManagerService, isConnectionProvider } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; -import { ContextMenuService, IContextMenuItem, IMenuContext } from '@cloudbeaver/core-dialogs'; +import { ContextMenuService, IMenuContext } from '@cloudbeaver/core-dialogs'; import { ExtensionUtils } from '@cloudbeaver/core-extensions'; import { ActiveViewService } from '@cloudbeaver/core-view'; @@ -43,8 +43,7 @@ export class SqlEditorBootstrap { isDisabled: () => !this.connectionsManagerService.hasAnyConnection(), } ); - - const openSqlEditor: IContextMenuItem = { + this.contextMenuService.addMenuItem(this.contextMenuService.getRootMenuToken(), { id: 'open-sql-editor', isPresent(context) { return context.contextType === NavigationTreeContextMenuService.nodeContextType @@ -57,8 +56,7 @@ export class SqlEditorBootstrap { const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(node.id); this.sqlEditorNavigatorService.openNewEditor(connectionId); }, - }; - this.contextMenuService.addMenuItem(this.contextMenuService.getRootMenuToken(), openSqlEditor); + }); } private openSQLEditor() { diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorGroupMetadataService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorGroupMetadataService.ts index e5b042f856..123fc6e56d 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorGroupMetadataService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorGroupMetadataService.ts @@ -34,6 +34,7 @@ export class SqlEditorGroupMetadataService { const serviceContext = this; return { + resultDataProcess: new SQLQueryExecutionProcess(this.graphQLService, this.notificationService), async start(context, sqlQueryParams, filter, dataFormat) { this.resultDataProcess = new SQLQueryExecutionProcess( serviceContext.graphQLService, @@ -44,7 +45,9 @@ export class SqlEditorGroupMetadataService { return this.resultDataProcess; }, - resultDataProcess: new SQLQueryExecutionProcess(this.graphQLService, this.notificationService), + async dispose(params) { + // TODO: will be implemented + }, }; } } diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorNavigatorService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorNavigatorService.ts index 84c7c08da2..b7121e5c64 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorNavigatorService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorNavigatorService.ts @@ -12,6 +12,7 @@ import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { IExecutor, Executor, IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { ISqlEditorTabState } from './ISqlEditorTabState'; import { SqlEditorTabService, isSQLEditorTab } from './SqlEditorTabService'; import { SqlExecutionState } from './SqlExecutionState'; @@ -86,10 +87,10 @@ export class SqlEditorNavigatorService { private async handleConnectionClose(connectionId: string) { try { - for (const tab of this.navigationTabsService.findTabs( - isSQLEditorTab(tab => tab.handlerState.connectionId.includes(connectionId)) + for (const tab of this.navigationTabsService.findTabs( + isSQLEditorTab(tab => !!tab.handlerState.connectionId?.includes(connectionId)) )) { - await this.navigationTabsService.closeTab(tab.id); + tab.handlerState.contextId = undefined; } return; } catch (exception) { @@ -110,17 +111,12 @@ export class SqlEditorNavigatorService { data.catalogId, data.schemaId ); - const connectionInfo = this.connectionInfoResource.get(data.connectionId || ''); if (tabOptions) { const tab = tabInfo.openNewTab(tabOptions); // FIXME: should be in SqlEditorTabService this.sqlEditorTabService.tabExecutionState.set(tab.id, new SqlExecutionState()); - } else { - this.notificationService.logError({ - title: `Failed to create editor for ${connectionInfo?.name || data.connectionId} connection`, - }); } return; } @@ -131,30 +127,9 @@ export class SqlEditorNavigatorService { } if (data.type === SQLEditorNavigationAction.select) { - tab.handlerState.currentResultTabId = data.resultId; + this.sqlEditorTabService.selectResultTab(tab, data.resultId); } else if (data.type === SQLEditorNavigationAction.close) { - const resultTabGroupId = tab.handlerState.resultTabs - .find(resultTab => resultTab.resultTabId === data.resultId)?.groupId; - - tab.handlerState.resultTabs.splice( - tab.handlerState.resultTabs.findIndex(result => result.resultTabId === data.resultId), - 1 - ); - - const isGroupEmpty = !tab.handlerState.resultTabs.some(resultTab => resultTab.groupId === resultTabGroupId); - - if (isGroupEmpty) { - tab.handlerState.queryTabGroups.splice( - tab.handlerState.queryTabGroups.findIndex(queryTabGroup => queryTabGroup.groupId === resultTabGroupId), 1); - } - - if (tab.handlerState.currentResultTabId === data.resultId) { - if (tab.handlerState.resultTabs.length > 0) { - tab.handlerState.currentResultTabId = tab.handlerState.resultTabs[0].resultTabId; - } else { - tab.handlerState.currentResultTabId = ''; - } - } + await this.sqlEditorTabService.closeResultTab(tab, data.resultId); } this.navigationTabsService.selectTab(tab.id); } catch (exception) { diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts new file mode 100644 index 0000000000..f3f7d20949 --- /dev/null +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts @@ -0,0 +1,142 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 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 { ConnectionInfoResource, ConnectionsManagerService } from '@cloudbeaver/core-connections'; +import { injectable } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { GraphQLService, QuerySqlCompletionProposalsQuery } from '@cloudbeaver/core-sdk'; +import { IExecutionContext } from '@cloudbeaver/plugin-data-viewer'; + +import { ISqlEditorTabState } from './ISqlEditorTabState'; +import { SqlDialectInfoService } from './SqlDialectInfoService'; + +@injectable() +export class SqlEditorService { + constructor( + private gql: GraphQLService, + private connectionInfoResource: ConnectionInfoResource, + private sqlDialectInfoService: SqlDialectInfoService, + private connectionsManagerService: ConnectionsManagerService, + private notificationService: NotificationService, + ) { + } + + async getAutocomplete( + connectionId: string, + contextId: string, + query: string, + cursor: number + ): Promise { + const result = await this.gql.sdk.querySqlCompletionProposals({ + connectionId, + contextId, + query, + position: cursor, + }); + + return result.sqlCompletionProposals; + } + + async initEditorConnection(state: ISqlEditorTabState): Promise { + if (!state.connectionId) { + console.error('executeEditorQuery connectionId is not provided'); + return; + } + + const context = await this.initContext(state.connectionId, state.objectCatalogId, state.objectSchemaId); + + if (!context) { + return; + } + + return context; + } + + async initContext( + connectionId?: string, + catalogId?: string, + schemaId?: string + ): Promise { + const connection = await this.connectionsManagerService.requireConnection(connectionId); + if (!connection) { + return null; + } + + try { + await this.sqlDialectInfoService.loadSqlDialectInfo(connection.id); + + return await this.createSqlContext(connection.id, catalogId, schemaId); + } catch (exception) { + this.notificationService.logException( + exception, + `Failed to create context for ${connection.name} connection`, + ); + return null; + } + } + + async destroySqlContext(connectionId?: string, contextId?: string): Promise { + if (!connectionId) { + return; + } + + const connection = this.connectionInfoResource.get(connectionId); + if (!connection?.connected || !contextId) { + return; + } + try { + await this.gql.sdk.sqlContextDestroy({ connectionId, contextId }); + } catch (exception) { + this.notificationService.logException(exception, `Failed to destroy SQL-context ${contextId}`, '', true); + } + } + + /** + * Returns context id, context catalog and schema + * When try create context without catalog or schema the context is created with default catalog and schema + * and response contains its ids. + * If in the response there are no catalog or schema it means that database has no catalogs or schemas at all. + */ + async createSqlContext( + connectionId: string, + defaultCatalog?: string, + defaultSchema?: string + ): Promise { + const response = await this.gql.sdk.sqlContextCreate({ + connectionId, + defaultCatalog, + defaultSchema, + }); + return { + contextId: response.context.id, + connectionId, + objectCatalogId: response.context.defaultCatalog, + objectSchemaId: response.context.defaultSchema, + }; + } + + /** + * Update catalog and schema for the exiting sql context in the certain connection + */ + async updateSqlContext( + connectionId: string, + contextId?: string, + defaultCatalog?: string, + defaultSchema?: string + ): Promise { + if (!contextId) { + throw new Error('updateSqlContext contextId not provided'); + } + await this.gql.sdk.sqlContextSetDefaults({ + connectionId, + contextId, + defaultCatalog, + defaultSchema, + }); + } +} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorTab.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditorTab.tsx index ecfec0241d..17913e3a3f 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorTab.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorTab.tsx @@ -18,11 +18,14 @@ import { useStyles } from '@cloudbeaver/core-theming'; import { ISqlEditorTabState } from './ISqlEditorTabState'; export const SqlEditorTab = observer(function SqlEditorTab({ - tab, handler, onSelect, onClose, style, + tab, onSelect, onClose, style, }: TabHandlerTabProps) { const connectionInfo = useService(ConnectionInfoResource); - const connection = connectionInfo.get(tab.handlerState.connectionId); - const name = `sql-${tab.handlerState.order}${connection ? ` (${connection.name})` : ''}`; + let name = `sql-${tab.handlerState.order}`; + if (tab.handlerState.connectionId) { + const connection = connectionInfo.get(tab.handlerState.connectionId); + name = `sql-${tab.handlerState.order}${connection ? ` (${connection.name})` : ''}`; + } const handleSelect = ({ tabId }: ITabData) => onSelect(tabId); const handleClose = onClose ? ({ tabId }: ITabData) => onClose(tabId) : undefined; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorTabService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorTabService.ts index d3b3c615a2..22b3e317dc 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorTabService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorTabService.ts @@ -19,36 +19,30 @@ import { ITabOptions } from '@cloudbeaver/core-app'; import { - ConnectionInfoResource, connectionProvider, connectionSetter, - ConnectionsManagerService } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { GraphQLService } from '@cloudbeaver/core-sdk'; -import { IExecutionContext } from '@cloudbeaver/plugin-data-viewer'; import { ISqlEditorTabState } from './ISqlEditorTabState'; -import { SqlDialectInfoService } from './SqlDialectInfoService'; import { SqlEditorPanel } from './SqlEditorPanel'; +import { SqlEditorService } from './SqlEditorService'; import { SqlEditorTab } from './SqlEditorTab'; import { sqlEditorTabHandlerKey } from './sqlEditorTabHandlerKey'; import { SqlExecutionState } from './SqlExecutionState'; @injectable() -export class SqlEditorTabService { +export class SqlEditorTabService extends Bootstrap { @observable readonly tabExecutionState: Map; readonly tabHandler: TabHandler; constructor( private navigationTabsService: NavigationTabsService, - private connectionInfoResource: ConnectionInfoResource, private notificationService: NotificationService, - private gql: GraphQLService, - private sqlDialectInfoService: SqlDialectInfoService, - private connectionsManagerService: ConnectionsManagerService + private sqlEditorService: SqlEditorService, ) { + super(); this.tabExecutionState = new Map(); this.tabHandler = this.navigationTabsService @@ -69,20 +63,21 @@ export class SqlEditorTabService { }); } + register(): void {} + load(): void {} + async createNewEditor( connectionId?: string, catalogId?: string, schemaId?: string ): Promise | null> { - const order = this.getFreeEditorId(); - const connection = await this.connectionsManagerService.requireConnection(connectionId); - if (!connection) { + const context = await this.sqlEditorService.initContext(connectionId, catalogId, schemaId); + + if (!context) { return null; } - await this.sqlDialectInfoService.loadSqlDialectInfo(connection.id); - - const context = await this.createSqlContext(connection.id, catalogId, schemaId); + const order = this.getFreeEditorId(); return { handlerId: sqlEditorTabHandlerKey, @@ -90,7 +85,7 @@ export class SqlEditorTabService { query: '', order, contextId: context.contextId, - connectionId: connection.id, + connectionId: context.connectionId, objectCatalogId: context.objectCatalogId, objectSchemaId: context.objectSchemaId, queryTabGroups: [], @@ -99,6 +94,39 @@ export class SqlEditorTabService { }; } + selectResultTab(tab: ITab, resultId: string): void { + tab.handlerState.currentResultTabId = resultId; + } + + async closeResultTab(tab: ITab, resultId: string): Promise { + const resultTabGroupId = tab.handlerState.resultTabs + .find(resultTab => resultTab.resultTabId === resultId)?.groupId; + + tab.handlerState.resultTabs.splice( + tab.handlerState.resultTabs.findIndex(result => result.resultTabId === resultId), + 1 + ); + + const isGroupEmpty = !tab.handlerState.resultTabs.some(resultTab => resultTab.groupId === resultTabGroupId); + + if (isGroupEmpty) { + const group = tab.handlerState.queryTabGroups.splice( + tab.handlerState.queryTabGroups.findIndex(queryTabGroup => queryTabGroup.groupId === resultTabGroupId), + 1 + )[0]; + + await this.sqlEditorService.destroySqlContext(group.sqlQueryParams.connectionId, group.sqlQueryParams.contextId); + } + + if (tab.handlerState.currentResultTabId === resultId) { + if (tab.handlerState.resultTabs.length > 0) { + tab.handlerState.currentResultTabId = tab.handlerState.resultTabs[0].resultTabId; + } else { + tab.handlerState.currentResultTabId = ''; + } + } + } + private getFreeEditorId() { const editorTabs = this.navigationTabsService.findTabs(isSQLEditorTab); const ordered = Array.from(editorTabs).map(tab => tab.handlerState.order); @@ -107,10 +135,10 @@ export class SqlEditorTabService { private async handleTabRestore(tab: ITab): Promise { if (typeof tab.handlerState.query !== 'string' - || typeof tab.handlerState.connectionId !== 'string' - || typeof tab.handlerState.contextId !== 'string' - || typeof tab.handlerState.objectCatalogId !== 'string' || typeof tab.handlerState.order !== 'number' + || !['string', 'undefined'].includes(typeof tab.handlerState.connectionId) + || !['string', 'undefined'].includes(typeof tab.handlerState.contextId) + || !['string', 'undefined'].includes(typeof tab.handlerState.objectCatalogId) || !['string', 'undefined'].includes(typeof tab.handlerState.currentResultTabId) || !Array.isArray(tab.handlerState.queryTabGroups) || !Array.isArray(tab.handlerState.resultTabs) @@ -118,20 +146,12 @@ export class SqlEditorTabService { return false; } - // // the connection for this editor was closed - if (!this.connectionInfoResource.get(tab.handlerState.connectionId)) { - return false; - } - tab.handlerState.currentResultTabId = ''; tab.handlerState.queryTabGroups = []; // clean old results tab.handlerState.resultTabs = []; // clean old results this.tabExecutionState.set(tab.id, new SqlExecutionState()); - // todo seems to be changed - await this.sqlDialectInfoService.loadSqlDialectInfo(tab.handlerState.connectionId); - return true; } @@ -149,21 +169,19 @@ export class SqlEditorTabService { private async setConnectionId(connectionId: string, tab: ITab) { try { - const connection = await this.connectionsManagerService.requireConnection(connectionId); + const context = await this.sqlEditorService.initContext(connectionId); - if (!connection) { + if (!context) { return false; } - // try to create new context first - const context = await this.createSqlContext(connectionId); + // when new context created - destroy old one silently - await this.destroySqlContext(tab.handlerState.connectionId, tab.handlerState.contextId); + await this.sqlEditorService.destroySqlContext(tab.handlerState.connectionId, tab.handlerState.contextId); tab.handlerState.connectionId = context.connectionId; tab.handlerState.contextId = context.contextId; tab.handlerState.objectCatalogId = context.objectCatalogId; tab.handlerState.objectSchemaId = context.objectSchemaId; - await this.sqlDialectInfoService.loadSqlDialectInfo(connectionId); return true; } catch (exception) { this.notificationService.logException(exception, 'Failed to change SQL-editor connection'); @@ -172,8 +190,11 @@ export class SqlEditorTabService { } private async setObjectCatalogId(containerId: string, tab: ITab) { + if (!tab.handlerState.connectionId) { + return false; + } try { - await this.updateSqlContext( + await this.sqlEditorService.updateSqlContext( tab.handlerState.connectionId, tab.handlerState.contextId, containerId @@ -187,8 +208,11 @@ export class SqlEditorTabService { } private async setObjectSchemaId(containerId: string, tab: ITab) { + if (!tab.handlerState.connectionId) { + return false; + } try { - await this.updateSqlContext( + await this.sqlEditorService.updateSqlContext( tab.handlerState.connectionId, tab.handlerState.contextId, tab.handlerState.objectCatalogId, @@ -204,61 +228,10 @@ export class SqlEditorTabService { private async handleTabClose(tab: ITab) { this.tabExecutionState.delete(tab.id); - await this.destroySqlContext(tab.handlerState.connectionId, tab.handlerState.contextId); - } - - private async destroySqlContext(connectionId: string, contextId: string) { - const connection = this.connectionInfoResource.get(connectionId); - if (!connection) { - // connection was closed before, nothing to destroy - return; + await this.sqlEditorService.destroySqlContext(tab.handlerState.connectionId, tab.handlerState.contextId); + for (const resultTab of tab.handlerState.resultTabs) { + await this.closeResultTab(tab, resultTab.resultTabId); } - try { - await this.gql.sdk.sqlContextDestroy({ connectionId, contextId }); - } catch (exception) { - this.notificationService.logException(exception, `Failed to destroy SQL-context ${contextId}`, '', true); - } - } - - /** - * Returns context id, context catalog and schema - * When try create context without catalog or schema the context is created with default catalog and schema - * and response contains its ids. - * If in the response there are no catalog or schema it means that database has no catalogs or schemas at all. - */ - private async createSqlContext( - connectionId: string, - defaultCatalog?: string, - defaultSchema?: string - ): Promise { - const response = await this.gql.sdk.sqlContextCreate({ - connectionId, - defaultCatalog, - defaultSchema, - }); - return { - contextId: response.context.id, - connectionId, - objectCatalogId: response.context.defaultCatalog, - objectSchemaId: response.context.defaultSchema, - }; - } - - /** - * Update catalog and schema for the exiting sql context in the certain connection - */ - private async updateSqlContext( - connectionId: string, - contextId: string, - defaultCatalog?: string, - defaultSchema?: string - ) { - await this.gql.sdk.sqlContextSetDefaults({ - connectionId, - contextId, - defaultCatalog, - defaultSchema, - }); } } diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts index 51b6f30744..048babd1ca 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabsService.ts @@ -17,6 +17,7 @@ import { } from '../ISqlEditorTabState'; import { SqlDialectInfoService } from '../SqlDialectInfoService'; import { SqlEditorGroupMetadataService } from '../SqlEditorGroupMetadataService'; +import { SqlEditorService } from '../SqlEditorService'; import { SqlExecutionState } from '../SqlExecutionState'; @injectable() @@ -25,7 +26,8 @@ export class SqlResultTabsService { constructor( private sqlEditorGroupMetadataService: SqlEditorGroupMetadataService, - private sqlDialectInfoService: SqlDialectInfoService + private sqlDialectInfoService: SqlDialectInfoService, + private sqlEditorService: SqlEditorService ) { this.tabExecutionContext = new MetadataMap(() => new SqlExecutionState()); } @@ -44,8 +46,25 @@ export class SqlResultTabsService { return; } + const context = await this.sqlEditorService.initEditorConnection(editorState); + + if (!context) { + console.error('executeEditorQuery connection not established'); + return; + } + + if (!context.contextId) { + console.error('executeEditorQuery contextId is not provided'); + return; + } + + if (!context.connectionId) { + console.error('executeEditorQuery connectionId is not provided'); + return; + } + + const dialectInfo = await this.sqlDialectInfoService.loadSqlDialectInfo(context.connectionId); const currentTab = editorState.resultTabs.find(tab => tab.resultTabId === editorState.currentResultTabId); - const dialectInfo = await this.sqlDialectInfoService.loadSqlDialectInfo(editorState.connectionId); if (dialectInfo?.scriptDelimiter && query.endsWith(dialectInfo?.scriptDelimiter)) { query = query.slice(0, query.length - dialectInfo.scriptDelimiter.length); @@ -54,10 +73,10 @@ export class SqlResultTabsService { let tabGroup: IQueryTabGroup; const sqlQueryParams: ISqlQueryParams = { - connectionId: editorState.connectionId, - objectCatalogId: editorState.objectCatalogId, - objectSchemaId: editorState.objectSchemaId, - contextId: editorState.contextId, + connectionId: context.connectionId, + objectCatalogId: context.objectCatalogId, + objectSchemaId: context.objectSchemaId, + contextId: context.contextId, query, }; diff --git a/webapp/packages/plugin-sql-editor/src/manifest.ts b/webapp/packages/plugin-sql-editor/src/manifest.ts index e93bf4f111..965b62d921 100644 --- a/webapp/packages/plugin-sql-editor/src/manifest.ts +++ b/webapp/packages/plugin-sql-editor/src/manifest.ts @@ -10,10 +10,10 @@ import { PluginManifest } from '@cloudbeaver/core-di'; import { LocaleService } from './LocaleService'; import { SqlDialectInfoService } from './SqlDialectInfoService'; -import { SqlEditorService } from './SqlEditor/SqlEditorService'; import { SqlEditorBootstrap } from './SqlEditorBootstrap'; import { SqlEditorGroupMetadataService } from './SqlEditorGroupMetadataService'; import { SqlEditorNavigatorService } from './SqlEditorNavigatorService'; +import { SqlEditorService } from './SqlEditorService'; import { SqlEditorTabService } from './SqlEditorTabService'; import { SqlResultService } from './SqlResultTabs/SqlResultService'; import { SqlResultTabsService } from './SqlResultTabs/SqlResultTabsService';