Merge remote-tracking branch 'origin/devel' into devel

This commit is contained in:
serge-rider
2020-11-15 20:12:03 +03:00
42 changed files with 729 additions and 339 deletions
@@ -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);
}
@@ -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';