mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-13 07:47:05 +08:00
Merge remote-tracking branch 'origin/devel' into devel
This commit is contained in:
+18
-3
@@ -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<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -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 {
|
||||
|
||||
@@ -14,5 +14,6 @@ export interface ITabOptions<T = any> {
|
||||
|
||||
export interface ITab<T = any> extends ITabOptions<T> {
|
||||
id: string;
|
||||
userId: string;
|
||||
restored: boolean;
|
||||
}
|
||||
|
||||
@@ -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<string, TabHandler>();
|
||||
@observable tabsMap = new Map<string, ITab>();
|
||||
@observable state: TabsState = {
|
||||
tabs: [],
|
||||
history: [],
|
||||
currentId: '',
|
||||
};
|
||||
@observable state = new Map<string, TabsState>();
|
||||
|
||||
@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<ITab>();
|
||||
private tabCloseSubject = new Subject<ITab>();
|
||||
private tabCloseSubject = new Subject<ITab | undefined>();
|
||||
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<TabsState> => {
|
||||
const map: IKeyValueMap<TabsState> = {};
|
||||
for (const [key, value] of Object.entries(json as IKeyValueMap<TabsState>)) {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<S>(predicate: (tab: ITab) => tab is ITab<S>): Generator<ITab<S>>;
|
||||
* findTabs(predicate: (tab: ITab) => boolean): Generator<ITab> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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);
|
||||
|
||||
@@ -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<any> | 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<T = any>(options: ITabOptions<T>): ITab<T> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<INodeNavigationData>;
|
||||
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<INodeNavigationData>
|
||||
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
||||
): Promise<void | false> {
|
||||
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;
|
||||
|
||||
@@ -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<boolean>;
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
const userInfo = await this.userInfoResource.load();
|
||||
|
||||
const state = userInfo !== null;
|
||||
await this.auth.execute(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
register(): void { }
|
||||
|
||||
load(): void { }
|
||||
}
|
||||
@@ -26,7 +26,11 @@ export class UserInfoResource extends CachedDataResource<UserAuthInfo | null, vo
|
||||
super(null);
|
||||
this.loaded = false;
|
||||
this.sessionResource.onDataOutdated.addHandler(() => 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<UserAuthInfo | null, vo
|
||||
}
|
||||
|
||||
protected async loader(): Promise<UserAuthInfo | null> {
|
||||
await this.sessionResource.load();
|
||||
const { user } = await this.graphQLService.sdk.getSessionUser();
|
||||
this.loaded = true;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './AppAuthService';
|
||||
export * from './AuthInfoService';
|
||||
export * from './AuthProviderService';
|
||||
export * from './AuthProvidersResource';
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, Connection
|
||||
private sessionUpdate: boolean;
|
||||
constructor(
|
||||
private graphQLService: GraphQLService,
|
||||
appAuthService: AppAuthService,
|
||||
sessionResource: SessionResource
|
||||
) {
|
||||
super(new Map());
|
||||
@@ -38,7 +40,8 @@ export class ConnectionInfoResource extends CachedMapResource<string, Connection
|
||||
// it's prevents double nav tree refresh
|
||||
this.onItemAdd.addHandler(() => !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<void> {
|
||||
|
||||
@@ -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<Connection>();
|
||||
onCloseConnection = new Subject<string>();
|
||||
private disconnecting: boolean;
|
||||
private connectionExecutor: IExecutor<string | null>;
|
||||
|
||||
constructor(
|
||||
readonly connectionInfo: ConnectionInfoResource,
|
||||
@@ -32,28 +34,14 @@ export class ConnectionsManagerService {
|
||||
private commonDialogService: CommonDialogService
|
||||
) {
|
||||
this.disconnecting = false;
|
||||
this.connectionExecutor = new Executor<string | null>(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<Connection | null> {
|
||||
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<string | null>) {
|
||||
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<ObjectContainer[]> {
|
||||
await this.connectionObjectContainers.load({ connectionId, catalogId });
|
||||
return this.connectionObjectContainers.data.get(connectionId)!;
|
||||
return this.connectionObjectContainers.get({ connectionId, catalogId })!;
|
||||
}
|
||||
|
||||
private async afterConnectionClose(id: string) {
|
||||
|
||||
@@ -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<DatabaseObjectInfo, 'name' | 'description' | 'type' | 'features'>;
|
||||
type DataValue = MetadataMap<string, Record<string, ObjectContainer[]>>;
|
||||
|
||||
const defaultCatalog = 'default';
|
||||
|
||||
interface ObjectContainerParams {
|
||||
connectionId: string;
|
||||
catalogId?: string;
|
||||
}
|
||||
|
||||
interface ObjectContainerMetadata extends ICachedResourceMetadata {
|
||||
outdatedData: string[];
|
||||
loadingData: string[];
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ContainerResource extends CachedDataResource<Map<string, ObjectContainer[]>, ObjectContainerParams> {
|
||||
constructor(private graphQLService: GraphQLService) {
|
||||
super(new Map());
|
||||
export class ContainerResource extends CachedDataResource<
|
||||
DataValue,
|
||||
ObjectContainerParams,
|
||||
string
|
||||
> {
|
||||
metadata: MetadataMap<string, ObjectContainerMetadata>;
|
||||
|
||||
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<Map<string, ObjectContainer[]>> {
|
||||
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<DataValue> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { IExecutor } from './IExecutor';
|
||||
import { IExecutorHandler } from './IExecutorHandler';
|
||||
import { BlockedExecution, TaskScheduler } from './TaskScheduler/TaskScheduler';
|
||||
|
||||
export class Executor<T> implements IExecutor<T> {
|
||||
export class Executor<T = unknown> implements IExecutor<T> {
|
||||
private handlers: Array<IExecutorHandler<any>> = [];
|
||||
private postHandlers: Array<IExecutorHandler<any>> = [];
|
||||
private scheduler: TaskScheduler<T>;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import { IExecutionContextProvider } from './ExecutionContext';
|
||||
import { IExecutorHandler } from './IExecutorHandler';
|
||||
|
||||
export interface IExecutor<T> {
|
||||
export interface IExecutor<T = unknown> {
|
||||
execute: (data: T) => Promise<IExecutionContextProvider<T>>;
|
||||
addHandler: (handler: IExecutorHandler<T>) => this;
|
||||
removeHandler: (handler: IExecutorHandler<T>) => void;
|
||||
|
||||
@@ -14,7 +14,8 @@ import { CachedResource } from './CachedResource';
|
||||
export abstract class CachedDataResource<
|
||||
TData,
|
||||
TParam,
|
||||
> extends CachedResource<TData, TParam> {
|
||||
TKey = TParam,
|
||||
> extends CachedResource<TData, TParam, TKey> {
|
||||
async refresh(param: TParam): Promise<TData> {
|
||||
await this.loadData(param, true);
|
||||
return this.data;
|
||||
|
||||
@@ -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<TParam>;
|
||||
readonly onDataUpdate: IExecutor<TData>;
|
||||
|
||||
protected metadata: MetadataMap<TParam, ICachedResourceMetadata>;
|
||||
protected metadata: MetadataMap<TKey, ICachedResourceMetadata>;
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -99,6 +99,15 @@ export class ContainerDataSource extends DatabaseDataSource<IDataContainerOption
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
if (this.executionContext) {
|
||||
await this.graphQLService.sdk.sqlContextDestroy({
|
||||
connectionId: this.executionContext.connectionId,
|
||||
contextId: this.executionContext.contextId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureContextCreated(): Promise<IExecutionContext> {
|
||||
if (!this.executionContext) {
|
||||
if (!this.options) {
|
||||
|
||||
@@ -105,18 +105,10 @@ export class DataViewerTabService {
|
||||
}
|
||||
|
||||
private async handleTabRestore(tab: ITab<IObjectViewerTabState>) {
|
||||
// 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<IObjectViewerTabState>) {
|
||||
this.dataViewerTableService.removeTableModel(tab.handlerState.objectId);
|
||||
this.dataViewerTableService.removeTableModel(tab.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,7 +28,20 @@ export class DataViewerTableService {
|
||||
return this.tableViewerStorageService.has(tableId);
|
||||
}
|
||||
|
||||
removeTableModel(tableId: string): void {
|
||||
async removeTableModel(tableId: string): Promise<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -118,4 +118,6 @@ implements IDatabaseDataSource<TOptions, TResult> {
|
||||
|
||||
abstract request(prevResults: TResult[]): TResult[] | Promise<TResult[]>;
|
||||
abstract save(prevResults: TResult[], data: DataUpdate): Promise<TResult[]> | TResult[];
|
||||
|
||||
abstract dispose(): Promise<void>;
|
||||
}
|
||||
|
||||
@@ -49,4 +49,5 @@ export interface IDatabaseDataSource<TOptions, TResult extends IDatabaseDataResu
|
||||
data: DataUpdate
|
||||
) => Promise<TResult[]> | TResult[];
|
||||
cancel: () => Promise<boolean>;
|
||||
dispose: () => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -125,6 +125,10 @@ export class DataModelWrapper extends DatabaseDataModel<any> {
|
||||
}
|
||||
};
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
await this.source.dispose();
|
||||
}
|
||||
|
||||
private showError(exception: any) {
|
||||
this.exception = null;
|
||||
this.hasDetails = false;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
export interface IObjectViewerTabState {
|
||||
connectionId: string | undefined;
|
||||
objectId: string;
|
||||
parentId: string;
|
||||
parents: string[];
|
||||
|
||||
@@ -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<IObjectViewerTabState>) {
|
||||
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 (
|
||||
<TextPlaceholder>
|
||||
Connection required
|
||||
<br />
|
||||
<Button type="button" mod={['unelevated']} onClick={handleConnect}>Connect</Button>
|
||||
</TextPlaceholder>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoaded() || (!isOutdated() && isLoading())) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
@@ -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<IObjectViewerTabState>({
|
||||
@@ -93,6 +94,7 @@ export class ObjectViewerTabService {
|
||||
tabInfo.openNewTab<IObjectViewerTabState>({
|
||||
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<string>) {
|
||||
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<string>) {
|
||||
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<string>) {
|
||||
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<IObjectViewerTabState>) {
|
||||
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<IObjectViewerTabState>) {
|
||||
@@ -213,16 +208,32 @@ export class ObjectViewerTabService {
|
||||
|
||||
private async selectObjectTab(tab: ITab<IObjectViewerTabState>) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<SQLQueryExecutionProcess>;
|
||||
resultDataProcess: SQLQueryExecutionProcess;
|
||||
dispose: (params: ISqlQueryParams) => Promise<void>;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export interface IQueryTabGroup {
|
||||
sqlQueryParams: ISqlQueryParams;
|
||||
}
|
||||
|
||||
export interface ISqlEditorTabState extends IExecutionContext {
|
||||
export interface ISqlEditorTabState extends Partial<IExecutionContext> {
|
||||
order: number;
|
||||
query: string;
|
||||
currentResultTabId?: string;
|
||||
|
||||
@@ -114,4 +114,12 @@ export class QueryDataSource extends DatabaseDataSource<IDataContainerOptions, I
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
if (!this.options) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.metadata.dispose(this.options.group.sqlQueryParams);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<QuerySqlCompletionProposalsQuery['sqlCompletionProposals'] | null> {
|
||||
const result = await this.gql.sdk.querySqlCompletionProposals({
|
||||
connectionId,
|
||||
contextId,
|
||||
query,
|
||||
position: cursor,
|
||||
});
|
||||
|
||||
return result.sqlCompletionProposals;
|
||||
}
|
||||
}
|
||||
@@ -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<NavNode> = {
|
||||
this.contextMenuService.addMenuItem<NavNode>(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<NavNode>(this.contextMenuService.getRootMenuToken(), openSqlEditor);
|
||||
});
|
||||
}
|
||||
|
||||
private openSQLEditor() {
|
||||
|
||||
@@ -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
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ISqlEditorTabState>(
|
||||
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) {
|
||||
|
||||
@@ -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<QuerySqlCompletionProposalsQuery['sqlCompletionProposals'] | null> {
|
||||
const result = await this.gql.sdk.querySqlCompletionProposals({
|
||||
connectionId,
|
||||
contextId,
|
||||
query,
|
||||
position: cursor,
|
||||
});
|
||||
|
||||
return result.sqlCompletionProposals;
|
||||
}
|
||||
|
||||
async initEditorConnection(state: ISqlEditorTabState): Promise<IExecutionContext | undefined> {
|
||||
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<IExecutionContext | null> {
|
||||
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<void> {
|
||||
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<IExecutionContext> {
|
||||
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<void> {
|
||||
if (!contextId) {
|
||||
throw new Error('updateSqlContext contextId not provided');
|
||||
}
|
||||
await this.gql.sdk.sqlContextSetDefaults({
|
||||
connectionId,
|
||||
contextId,
|
||||
defaultCatalog,
|
||||
defaultSchema,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<ISqlEditorTabState>) {
|
||||
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<any>) => onSelect(tabId);
|
||||
const handleClose = onClose ? ({ tabId }: ITabData<any>) => onClose(tabId) : undefined;
|
||||
|
||||
@@ -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<string, SqlExecutionState>;
|
||||
readonly tabHandler: TabHandler<ISqlEditorTabState>;
|
||||
|
||||
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<ITabOptions<ISqlEditorTabState> | 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<ISqlEditorTabState>, resultId: string): void {
|
||||
tab.handlerState.currentResultTabId = resultId;
|
||||
}
|
||||
|
||||
async closeResultTab(tab: ITab<ISqlEditorTabState>, resultId: string): Promise<void> {
|
||||
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<ISqlEditorTabState>): Promise<boolean> {
|
||||
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<ISqlEditorTabState>) {
|
||||
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<ISqlEditorTabState>) {
|
||||
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<ISqlEditorTabState>) {
|
||||
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<ISqlEditorTabState>) {
|
||||
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<IExecutionContext> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user