Merge pull request #17 from dbeaver/chore-deps-update

refactor: navigation
This commit is contained in:
Alexey
2020-04-26 23:38:59 +03:00
committed by GitHub
107 changed files with 2247 additions and 1649 deletions
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { ThemeSelector } from '@dbeaver/core/src/theming';
import { ThemeSelector } from '@dbeaver/core/theming';
export const agGridStyles: ThemeSelector = async (theme) => {
const baseStyles = await import('./base.raw.scss');
@@ -20,7 +20,6 @@ export const agGridStyles: ThemeSelector = async (theme) => {
styles = await import('./themes/light.scss');
break;
}
console.log(baseStyles.default, styles.default);
return [baseStyles.default, styles.default];
};
@@ -0,0 +1,8 @@
{
"name": "@dbeaver/core/extensions",
"private": true,
"sideEffects": false,
"main": "../dist/extensions",
"module": "../dist/extensions",
"types": "../dist/types/extensions"
}
+4 -2
View File
@@ -10,7 +10,7 @@ import {
ConnectionDialogsService,
ConnectionsManagerService,
NodesManagerService,
NavigationTabsService, NavigationTreeContextMenuService, LogViewerMenuService,
NavigationTabsService, NavigationTreeContextMenuService, LogViewerMenuService, ConnectionSchemaManagerService,
} from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { ExceptionsCatcherService } from '@dbeaver/core/eventsLog';
@@ -34,7 +34,8 @@ export class AppBootstrap {
private logViewerMenuService: LogViewerMenuService,
private navigationTreeContextMenuService: NavigationTreeContextMenuService,
private nodesManagerService: NodesManagerService,
private navigationTabsService: NavigationTabsService) {
private navigationTabsService: NavigationTabsService,
private connectionSchemaManagerService: ConnectionSchemaManagerService) {
}
async init() {
@@ -43,6 +44,7 @@ export class AppBootstrap {
await this.sessionService.init();
await this.themeService.init();
this.connectionSchemaManagerService.registerCallbacks();
this.navigationTreeContextMenuService.registerMenuItems();
this.connectionDialogService.registerMenuItems();
this.logViewerMenuService.registerMenuItems();
@@ -6,231 +6,207 @@
* you may not use this file except in compliance with the License.
*/
import { computed } from 'mobx';
import { computed, observable } from 'mobx';
import { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import {
Connection,
ConnectionsManagerService,
ISchema,
} from '../../shared/ConnectionsManager/ConnectionsManagerService';
import { ConnectionsManagerService, ObjectContainer } from '../../shared/ConnectionsManager/ConnectionsManagerService';
import { isConnectionProvider, IConnectionProvider } from '../../shared/ConnectionsManager/extensions/IConnectionProvider';
import { isConnectionSetter, IConnectionSetter } from '../../shared/ConnectionsManager/extensions/IConnectionSetter';
import { ITab } from '../../shared/NavigationTabs/ITab';
import { NavigationTabsService } from '../../shared/NavigationTabs/NavigationTabsService';
import { NodeManagerUtils } from '../../shared/NodesManager/NodeManagerUtils';
import { NodesManagerService } from '../../shared/NodesManager/NodesManagerService';
import { ConnectionSchemaStore, IConnectionWithIcon } from './ConnectionSchemaStore';
import { IConnectionCatalogSchema, ITabHasConnectionChangeBehavior } from './IConnectionCatalogSchema';
import { IObjectCatalogProvider, isObjectCatalogProvider } from '../../shared/NodesManager/extensions/IObjectCatalogProvider';
import { IObjectCatalogSetter, isObjectCatalogSetter } from '../../shared/NodesManager/extensions/IObjectCatalogSetter';
import { IObjectSchemaProvider, isObjectSchemaProvider } from '../../shared/NodesManager/extensions/IObjectSchemaProvider';
import { IObjectSchemaSetter, isObjectSchemaSetter } from '../../shared/NodesManager/extensions/IObjectSchemaSetter';
export interface IConnectionInfo {
name?: string;
driverIcon?: string;
}
@injectable()
export class ConnectionSchemaManagerService {
get currentConnection() {
return this.currentTabState
? this.store.getConnectionById(this.currentTabState.connectionId) : null;
}
get schemaList(): ISchema[] {
return this.store.schemaList;
}
get connectionsList() {
return this.store.connectionsList;
}
get catalogsList() {
return this.store.catalogsList;
}
@computed get connectionSelectionDisabled(): boolean {
return !this.isTabStateChangeable
|| this.connectionsList.length === 0;
}
@computed get isTabStateChangeable(): boolean {
return Boolean(this.currentTabState && this.currentTabState.changeSchema);
}
@computed get isSelectorVisible(): boolean {
return Boolean(this.currentTabState);
}
@computed get currentTabState(): ITabHasConnectionChangeBehavior | null {
const tab = this.navigationTabsService.getTab(this.navigationTabsService.currentTabId);
if (!tab) {
return null;
get currentConnectionId(): string | undefined {
if (!this.getCurrentConnectionId || !this.currentTab) {
return;
}
// todo everything here should be refactored and moved to a specific tab strategy
const sqlHandlerState = tab.getHandlerState<ITabHasConnectionChangeBehavior>('sql_editor');
if (sqlHandlerState) {
return sqlHandlerState;
return this.getCurrentConnectionId(this.currentTab);
}
get currentObjectCatalogId(): string | undefined {
if (!this.getCurrentCatalogId || !this.currentTab) {
return;
}
return this.getCurrentCatalogId(this.currentTab);
}
get currentObjectSchemaId(): string | undefined {
if (!this.getCurrentSchemaId || !this.currentTab) {
return;
}
return this.getCurrentSchemaId(this.currentTab);
}
@computed get currentObjectCatalog(): ObjectContainer | undefined {
if (!this.currentConnectionId || !this.currentObjectCatalogId) {
return;
}
const isObjectViewer = tab.hasHandler('object_viewer_properties') || tab.hasHandler('data_viewer_data');
if (isObjectViewer) {
const connectionAndSchema = this.nodesManagerService.getConnectionCatalogSchema(tab.nodeId);
if (connectionAndSchema.connectionId) {
// connection node id differs from connection id
const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(connectionAndSchema.connectionId);
return {
connectionId,
schemaId: connectionAndSchema.schemaId || null,
catalogId: connectionAndSchema.catalogId || null,
};
}
}
return null;
return this.connectionsManagerService.getObjectContainerById(
this.currentConnectionId,
this.currentObjectCatalogId
);
}
private store = new ConnectionSchemaStore();
@computed get currentObjectSchema(): ObjectContainer | undefined {
if (!this.currentConnectionId || !this.currentObjectSchemaId || !this.currentObjectCatalogId) {
return;
}
return this.connectionsManagerService.getObjectContainerById(
this.currentConnectionId,
this.currentObjectCatalogId,
this.currentObjectSchemaId
);
}
@computed get objectContainerList(): ObjectContainer[] | undefined {
if (!this.currentConnectionId) {
return;
}
return this.connectionsManagerService.connectionObjectContainers.data.get(this.currentConnectionId);
}
get isConnectionChangeable(): boolean {
return !!this.changeConnectionId
&& !this.connectionsManagerService.connectionObjectContainers.isLoading();
}
get isObjectCatalogChangeable(): boolean {
return !!this.changeCatalogId
&& !this.connectionsManagerService.connectionObjectContainers.isLoading();
}
get isObjectSchemaChangeable(): boolean {
return !!this.changeSchemaId
&& !this.connectionsManagerService.connectionObjectContainers.isLoading();
}
@observable private currentTab: ITab | null = null;
@observable private getCurrentConnectionId: IConnectionProvider<ITab> | null = null;
@observable private getCurrentSchemaId: IObjectSchemaProvider<ITab> | null = null;
@observable private getCurrentCatalogId: IObjectCatalogProvider<ITab> | null = null;
@observable private changeConnectionId: IConnectionSetter<ITab> | null = null;
@observable private changeCatalogId: IObjectCatalogSetter<ITab> | null = null;
@observable private changeSchemaId: IObjectSchemaSetter<ITab> | null = null;
constructor(private navigationTabsService: NavigationTabsService,
private nodesManagerService: NodesManagerService,
private connectionsManagerService: ConnectionsManagerService,
private notificationService: NotificationService) {
this.connectionsManagerService.connections.forEach((connection) => {
this.addConnectionToList(connection);
});
}
this.connectionsManagerService.onOpenConnection
.subscribe((connection) => {
this.addConnectionToList(connection);
});
registerCallbacks() {
this.navigationTabsService.onTabSelect
.subscribe(this.onTabSelect.bind(this));
this.connectionsManagerService.onCloseConnection
.subscribe((connectionId) => {
this.removeConnectionFromList(connectionId);
});
this.navigationTabsService.onTabActivate
.subscribe(tab => this.onTabActivate());
this.onTabActivate();
this.navigationTabsService.onTabClose
.subscribe(this.onTabClose.bind(this));
}
/**
* Trigger when user select connection in dropdown
*/
onSelectConnection(connection: IConnectionWithIcon) {
if (!this.currentTabState) {
async selectConnection(connectionId: string) {
if (!this.changeConnectionId || !this.currentTab) {
return;
}
if (this.currentTabState.connectionId === connection.id) {
return;
}
this.changeTabConnection({
connectionId: connection.id,
catalogId: null,
schemaId: null,
});
}
/**
* Trigger when user select schema in dropdown
*/
onSelectSchema(schemaId: string) {
if (!this.currentConnection) {
throw new Error('The try to change schema without connection');
}
if (this.currentTabState?.schemaId === schemaId) {
return;
}
const params = {
connectionId: this.currentConnection.id,
catalogId: this.currentTabState?.catalogId || null,
schemaId,
};
this.changeTabConnection(params);
this.changeConnectionId(connectionId, this.currentTab);
this.loadConnection(connectionId);
}
/**
* Trigger when user select catalog in dropdown
*/
onSelectCatalog(catalogId: string) {
if (!this.currentConnection) {
selectCatalog(catalogId: string) {
if (!this.changeCatalogId || !this.currentTab) {
throw new Error('The try to change catalog without connection');
}
this.changeCatalogId(catalogId, this.currentTab);
this.loadConnection(this.currentConnectionId!, catalogId);
}
/**
* Trigger when user select schema in dropdown
*/
selectSchema(schemaId: string) {
if (!this.changeSchemaId || !this.currentTab) {
throw new Error('The try to change schema without connection');
}
if (this.currentTabState?.catalogId === catalogId) {
return;
}
const params = {
connectionId: this.currentConnection.id,
catalogId,
schemaId: null,
};
this.changeTabConnection(params);
this.changeSchemaId(schemaId, this.currentTab);
}
private onTabActivate() {
if (this.currentTabState && this.isTabStateChangeable) {
this.refreshSchemasList(this.currentTabState.connectionId, this.currentTabState.catalogId);
}
}
private onTabSelect(tab: ITab) {
this.clear();
this.currentTab = tab;
const handler = this.navigationTabsService.getTabHandler(tab.handlerId);
private async changeTabConnection(connectionCatalogSchema: IConnectionCatalogSchema): Promise<void> {
if (handler && handler.extensions) {
for (const extension of handler.extensions) {
if (isConnectionProvider(extension)) {
this.getCurrentConnectionId = extension;
}
if (isObjectCatalogProvider(extension)) {
this.getCurrentCatalogId = extension;
}
if (isObjectSchemaProvider(extension)) {
this.getCurrentSchemaId = extension;
}
if (!this.currentTabState || !this.currentTabState.changeSchema) {
return;
}
const currentConnectionId = this.currentTabState.connectionId;
const currentCatalogId = this.currentTabState.catalogId;
const res = await this.currentTabState.changeSchema(connectionCatalogSchema);
if (currentConnectionId !== res.connectionId || currentCatalogId !== res.catalogId) {
this.refreshSchemasList(res.connectionId, res.catalogId);
}
}
private addConnectionToList(connection: Connection) {
this.store.addConnection({
...connection,
icon: '',
});
this.loadIcon(connection);
}
private async loadIcon(connection: Connection): Promise<void> {
try {
const res = await this.connectionsManagerService.loadConnectionDriver(connection.driverId);
this.store.updateIcon(connection.id, res?.icon);
} catch (exception) {
this.notificationService.logException(exception, `Can't load icon for driver: ${connection.driverId}`);
}
}
private removeConnectionFromList(connectionId: string) {
this.store.removeConnection(connectionId);
}
private async refreshSchemasList(connectionId: string, catalogId: string | null) {
if (this.store.listsParams.connectionId === connectionId
&& this.store.listsParams.catalogId === catalogId) {
return;
}
// hide non-actual data while loading
this.store.setSchemasAndCatalogs(
{
connectionId: null,
catalogId: null,
},
{
catalogs: [],
schemas: [],
if (isConnectionSetter(extension)) {
this.changeConnectionId = extension;
}
if (isObjectCatalogSetter(extension)) {
this.changeCatalogId = extension;
}
if (isObjectSchemaSetter(extension)) {
this.changeSchemaId = extension;
}
}
);
}
const catalogAndSchemas = await this.connectionsManagerService
.getCatalogsAndSchemas(connectionId, catalogId || undefined);
if (this.currentConnectionId) {
this.loadConnection(this.currentConnectionId, this.currentObjectCatalogId);
}
}
this.store.setSchemasAndCatalogs(
{
connectionId,
catalogId: catalogId || null,
},
catalogAndSchemas
);
private onTabClose(tab: ITab) {
if (tab.id === this.currentTab?.id) {
this.clear();
}
}
private clear() {
this.getCurrentConnectionId = null;
this.getCurrentCatalogId = null;
this.getCurrentSchemaId = null;
this.changeConnectionId = null;
this.changeCatalogId = null;
this.changeSchemaId = null;
this.currentTab = null;
}
private async loadConnection(connectionId: string, catalogId?: string) {
try {
await this.connectionsManagerService.dbDrivers.load();
await this.connectionsManagerService.loadObjectContainer(connectionId, catalogId);
} catch (exception) {
this.notificationService.logException(exception, `Can't load connection: ${connectionId}`);
}
}
}
@@ -1,77 +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 { action } from 'mobx';
import { OrderedMap } from '@dbeaver/core/utils';
import { Connection, ICatalogsAndSchemas, ISchema } from '../../shared/ConnectionsManager/ConnectionsManagerService';
export interface IConnectionWithIcon extends Connection {
icon?: string;
}
interface IListsParams {
connectionId: string | null;
catalogId: string | null;
}
export class ConnectionSchemaStore {
private connectionsMap = new OrderedMap<string, IConnectionWithIcon>(c => c.id);
private schemaMap = new OrderedMap<string, ISchema>(s => s.id);
private catalogsMap = new OrderedMap<string, ISchema>(s => s.id);
listsParams: IListsParams = {
connectionId: null,
catalogId: null,
};
get schemaList(): ISchema[] {
return this.schemaMap.values;
}
get connectionsList() {
return this.connectionsMap.values;
}
get catalogsList() {
return this.catalogsMap.values;
}
getConnectionById(connectionId: string | null): IConnectionWithIcon | null {
if (connectionId == null) {
return null;
}
return this.connectionsMap.get(connectionId) || null;
}
@action updateIcon(connectionId: string, icon?: string) {
const connection = this.getConnectionById(connectionId);
if (connection) {
connection.icon = icon;
}
}
@action
setSchemasAndCatalogs(params: IListsParams, catalogsAndSchemas: ICatalogsAndSchemas): void {
this.listsParams = params;
this.schemaMap.bulkRewrite(catalogsAndSchemas.schemas);
this.catalogsMap.bulkRewrite(catalogsAndSchemas.catalogs);
}
@action
addConnection(connection: IConnectionWithIcon) {
this.connectionsMap.addValue(connection);
}
@action
removeConnection(connectionId: string) {
this.connectionsMap.remove(connectionId);
}
}
@@ -52,10 +52,10 @@ export const ConnectionSelector = observer(function ConnectionSelector() {
const SchemaOrCatalogMenu = TopMenuItem;
return styled(useStyles(style))(
<connection-selector as="div" {...use({ isVisible: controller.isSelectorVisible })}>
<connection-selector as="div" {...use({ isVisible: controller.isConnectionSelectorVisible })}>
<ConnectionMenu menuItem={controller.connectionMenu} style={[menuStyles, connectionMenu]}/>
{controller.isSchemaSelectorVisible && (
<SchemaOrCatalogMenu menuItem={controller.schemaOrCatalogMenu} style={[menuStyles]}/>
{controller.isObjectContainerSelectorVisible && (
<SchemaOrCatalogMenu menuItem={controller.objectContainerMenu} style={[menuStyles]}/>
)}
</connection-selector>
);
@@ -8,109 +8,138 @@
import { computed } from 'mobx';
import { ISchema } from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { ComputedMenuItemModel, ComputedMenuPanelModel, IMenuItem } from '@dbeaver/core/dialogs';
import { ConnectionsManagerService, Connection } from '../../../shared/ConnectionsManager/ConnectionsManagerService';
import { EObjectFeature } from '../../../shared/NodesManager/EObjectFeature';
import { concatSchemaAndCatalog } from '../../../shared/NodesManager/NodesManagerService';
import { ConnectionSchemaManagerService } from '../ConnectionSchemaManagerService';
@injectable()
export class ConnectionSelectorController {
connectionMenu: IMenuItem;
schemaOrCatalogMenu: IMenuItem;
objectContainerMenu: IMenuItem;
get isSelectorVisible() {
return this.connectionSelectorService.isSelectorVisible;
}
get isSchemaSelectorVisible() {
return this.connectionSelectorService.isSelectorVisible && Boolean(this.currentSchemaTitle);
}
@computed get schemaSelectionDisabled(): boolean {
return !this.connectionSelectorService.isTabStateChangeable
|| this.getSchemaOrCatalogItems().length === 0;
}
@computed private get currentSchemaTitle(): string {
const currentTabState = this.connectionSelectorService.currentTabState;
if (!currentTabState) {
return '';
@computed get currentConnection(): Connection | undefined {
if (!this.connectionSelectorService.currentConnectionId) {
return;
}
if (currentTabState.schemaId && currentTabState.catalogId) {
return `${currentTabState.schemaId}@${currentTabState.catalogId}`;
}
return currentTabState.schemaId || currentTabState.catalogId || '';
return this.connectionsManagerService.getConnectionById(
this.connectionSelectorService.currentConnectionId
);
}
@computed private get currentSchemaIcon(): string {
const currentTabState = this.connectionSelectorService.currentTabState;
if (currentTabState && currentTabState.schemaId) {
// TODO move such kind of icon paths to a set of constants
@computed get currentConnectionIcon(): string | undefined {
if (!this.currentConnection) {
return;
}
const driverIcon = this.connectionsManagerService.dbDrivers.data.get(this.currentConnection.driverId)?.icon;
return driverIcon;
}
get isConnectionSelectorVisible() {
return !!this.connectionSelectorService.currentConnectionId;
}
get isObjectContainerSelectorVisible() {
return !!this.connectionSelectorService.currentObjectCatalogId
|| !!this.connectionSelectorService.currentObjectSchemaId;
}
@computed get objectContainerSelectionDisabled(): boolean {
return !this.connectionSelectorService.isConnectionChangeable
|| this.getObjectContainerItems().length === 0;
}
private get currentObjectContainerTitle(): string | undefined {
return concatSchemaAndCatalog(
this.connectionSelectorService.currentObjectCatalogId,
this.connectionSelectorService.currentObjectSchemaId
);
}
@computed private get currentObjectContainerIcon(): string {
if (this.connectionSelectorService.currentObjectSchema?.features?.includes(EObjectFeature.schema)) {
// TODO move such kind of icon paths to a set of constants
return 'schema_system';
}
if (currentTabState && currentTabState.catalogId) {
if (this.connectionSelectorService.currentObjectCatalog?.features?.includes(EObjectFeature.catalog)) {
return 'database';
}
return '';
return 'database';
}
constructor(private connectionSelectorService: ConnectionSchemaManagerService) {
constructor(private connectionSelectorService: ConnectionSchemaManagerService,
private connectionsManagerService: ConnectionsManagerService) {
this.connectionMenu = new ComputedMenuItemModel({
id: 'connectionsDropdown',
isDisabled: () => this.connectionSelectorService.connectionSelectionDisabled,
titleGetter: () => this.connectionSelectorService?.currentConnection?.name,
iconGetter: () => this.connectionSelectorService?.currentConnection?.icon,
isDisabled: () => !this.connectionSelectorService.isConnectionChangeable,
titleGetter: () => this.currentConnection?.name,
iconGetter: () => this.currentConnectionIcon,
panel: new ComputedMenuPanelModel({
id: 'connectionsDropdownPanel',
menuItemsGetter: () => this.getConnectionItems(),
}),
});
this.schemaOrCatalogMenu = new ComputedMenuItemModel({
this.objectContainerMenu = new ComputedMenuItemModel({
id: 'connectionsDropdown',
isDisabled: () => this.schemaSelectionDisabled,
titleGetter: () => this.currentSchemaTitle,
iconGetter: () => this.currentSchemaIcon,
isDisabled: () => this.objectContainerSelectionDisabled,
titleGetter: () => this.currentObjectContainerTitle,
iconGetter: () => this.currentObjectContainerIcon,
panel: new ComputedMenuPanelModel({
id: 'connectionsDropdownPanel',
menuItemsGetter: () => this.getSchemaOrCatalogItems(),
menuItemsGetter: () => this.getObjectContainerItems(),
}),
});
}
private getConnectionItems(): IMenuItem[] {
return this.connectionSelectorService.connectionsList.map((item) => {
return this.connectionsManagerService.connections.map((item) => {
const menuItem: IMenuItem = {
id: item.id,
title: item.name || item.id,
onClick: () => this.connectionSelectorService.onSelectConnection(item),
onClick: () => this.connectionSelectorService.selectConnection(item.id),
};
return menuItem;
});
}
private getSchemaOrCatalogItems(): IMenuItem[] {
return this.connectionSelectorService.schemaList.length
? this.listToMenuItems(this.connectionSelectorService.schemaList, true)
: this.listToMenuItems(this.connectionSelectorService.catalogsList, false);
}
private getObjectContainerItems(): IMenuItem[] {
if (!this.connectionSelectorService.objectContainerList) {
return [];
}
return this.connectionSelectorService.objectContainerList
.filter(item => !!item.name)
.map((item) => {
if (item.features?.includes(EObjectFeature.catalog)) {
const title = concatSchemaAndCatalog(item.name, this.connectionSelectorService.currentObjectSchemaId);
const handler = item.features?.includes(EObjectFeature.catalog)
? () => this.connectionSelectorService.selectCatalog(item.name!)
: () => this.connectionSelectorService.selectSchema(item.name!);
private listToMenuItems(list: ISchema[], isSchema: boolean): IMenuItem[] {
return list.map((item) => {
const menuItem: IMenuItem = {
id: item.id,
title: item.id,
onClick: () => {
if (isSchema) {
this.connectionSelectorService.onSelectSchema(item.id);
} else {
this.connectionSelectorService.onSelectCatalog(item.id);
}
},
};
return menuItem;
});
const menuItem: IMenuItem = {
id: item.name!,
title,
onClick: handler,
};
return menuItem;
}
const title = concatSchemaAndCatalog(this.connectionSelectorService.currentObjectCatalogId, item.name);
const handler = item.features?.includes(EObjectFeature.catalog)
? () => this.connectionSelectorService.selectCatalog(item.name!)
: () => this.connectionSelectorService.selectSchema(item.name!);
const menuItem: IMenuItem = {
id: item.name!,
title,
onClick: handler,
};
return menuItem;
});
}
}
@@ -1,19 +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.
*/
export interface IConnectionCatalogSchema {
connectionId: string;
schemaId: string | null; // some dbs has no schema
catalogId: string | null; // some dbs has no catalog
}
export interface ITabHasConnectionChangeBehavior extends IConnectionCatalogSchema {
// todo remove optional sign whem we can destinguish ITabHasConnectionBehavior and ITabHasConnectionBehavior
changeSchema?: (connectionCatalogSchema: IConnectionCatalogSchema) => Promise<IConnectionCatalogSchema>;
}
@@ -4,11 +4,13 @@ query navGetStructContainers($connectionId: ID!, $catalogId: ID) {
name
description
type
features
}
schemaList {
name
description
type
features
}
}
}
+8 -3
View File
@@ -4,8 +4,15 @@ export * from './TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerServic
export * from './TopNavBar/SettingsMenu/SettingsMenuService';
export * from './shared/ConnectionsManager/ConnectionsManagerService';
export * from './shared/ConnectionsManager/ConnectionDialogsService';
export * from './shared/ConnectionsManager/extensions/IConnectionProvider';
export * from './shared/ConnectionsManager/extensions/IConnectionSetter';
export * from './shared/NodesManager/extensions/IObjectCatalogProvider';
export * from './shared/NodesManager/extensions/IObjectCatalogSetter';
export * from './shared/NodesManager/extensions/IObjectSchemaProvider';
export * from './shared/NodesManager/extensions/IObjectSchemaSetter';
export * from './shared/Navigation/NavigationService';
export * from './shared/NavigationTabs/NavigationTabsService';
export * from './shared/NavigationTabs/TabNavigationContext';
export * from './shared/NodesManager/NodesManagerService';
export * from './NavigationTree/NavigationTreeService';
export * from './NavigationTree/NavigationTreeContextMenuService';
@@ -13,7 +20,7 @@ export * from './shared/ToolsPanel/LogViewTab/LogViewerMenuService';
export * from './shared/ToolsPanel/LogViewTab/LogViewerService';
// Models
export * from './shared/NavigationTabs/Tab';
export * from './shared/NavigationTabs/ITab';
export * from './shared/NavigationTabs/TabHandler';
export * from './shared/Navigation/NavigationContext';
export * from './shared/TabEntity/TabEntity';
@@ -23,7 +30,6 @@ export * from './shared/TabEntity/TabEntity';
// hooks
export * from './shared/useChildren';
export * from './shared/NavigationTabs/NavigationTabsBar/Tabs/useTab';
export * from './shared/NavigationTabs/useTabHandlerState';
export * from './shared/NodesManager/useDatabaseObjectInfo';
export * from './shared/NodesManager/useNode';
export * from './shared/InlineEditor/InlineEditor';
@@ -33,7 +39,6 @@ export * from './Body';
export * from './Notifications/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog';
// Interfaces
export * from './TopNavBar/ConnectionSchemaManager/IConnectionCatalogSchema';
export * from './shared/Navigation';
export * from './shared/NodesManager/ENodeFeature';
export * from './shared/NodesManager/EObjectFeature';
@@ -18,9 +18,9 @@ import {
GraphQLService,
NavGetStructContainersQuery,
CachedResource,
DatabaseObjectInfo,
} from '@dbeaver/core/sdk';
import { NavigationTabsService } from '../NavigationTabs/NavigationTabsService';
import { NodesManagerService } from '../NodesManager/NodesManagerService';
export type DBDriver = Pick<
@@ -37,21 +37,14 @@ export type DBDriver = Pick<
>
export type DBSource = Pick<DataSourceInfo, 'id' | 'name' | 'driverId' | 'description'>
export type Connection = Pick<ConnectionInfo, 'id' | 'name' | 'connected' | 'driverId'>
export interface ISchema {
id: string;
}
export interface ICatalogsAndSchemas {
catalogs: ISchema[];
schemas: ISchema[];
}
export type ObjectContainer = Pick<DatabaseObjectInfo, 'name' | 'description' | 'type' | 'features'>
@injectable()
export class ConnectionsManagerService {
@observable private connectionsMap: Map<string, Connection> = new Map();
private dbDrivers = new CachedResource(new Map(), this.refreshDriversAsync.bind(this));
dbDrivers = new CachedResource(new Map(), this.refreshDriversAsync.bind(this));
connectionObjectContainers = new CachedResource(new Map(), this.refreshObjectContainersAsync.bind(this));
@computed get connections(): Connection[] {
return Array.from(this.connectionsMap.values());
@@ -61,7 +54,6 @@ export class ConnectionsManagerService {
onCloseConnection = new Subject<string>();
constructor(private graphQLService: GraphQLService,
private navigationTabsService: NavigationTabsService,
private nodesManagerService: NodesManagerService,
private sessionService: SessionService) {
}
@@ -84,6 +76,20 @@ export class ConnectionsManagerService {
return this.connectionsMap.get(connectionId);
}
getObjectContainerById(
connectionId: string,
objectCatalogId: string,
objectSchemaId?: string
): ObjectContainer | undefined {
const objectContainers = this.connectionObjectContainers.data.get(connectionId);
if (!objectContainers) {
return;
}
return objectContainers.find(
objectContainer => objectContainer.name === objectSchemaId || objectContainer.name === objectCatalogId
);
}
hasAnyConnection(): boolean {
return Boolean(this.connections.length);
}
@@ -96,9 +102,9 @@ export class ConnectionsManagerService {
}
async closeConnectionAsync(id: string, skipNodesRefresh?: boolean): Promise<void> {
await this.graphQLService.gql.closeConnection({ id });
await this.nodesManagerService.closeConnection(id);
this.onCloseConnection.next(id);
this.closeConnectionTabs(id);
await this.graphQLService.gql.closeConnection({ id });
this.connectionsMap.delete(id);
if (!skipNodesRefresh) {
@@ -106,24 +112,9 @@ export class ConnectionsManagerService {
}
}
async getCatalogsAndSchemas(connectionId: string, catalogId?: string): Promise<ICatalogsAndSchemas> {
const schemas = await this.loadSchemasAndCatalogs(connectionId, catalogId);
const catalogAndSchemas: ICatalogsAndSchemas = {
schemas: schemas.navGetStructContainers
.schemaList.map(s => ({
id: s.name || '',
})),
catalogs: schemas.navGetStructContainers
.catalogList.map(s => ({
id: s.name || '',
})),
};
return catalogAndSchemas;
}
async loadConnectionDriver(driverId: string): Promise<DBDriver | null> {
const drivers = await this.graphQLService.gql.getDriverById({ driverId });
return drivers.driverList[0] || null;
async loadObjectContainer(connectionId: string, catalogId?: string): Promise<ObjectContainer[]> {
const data = await this.connectionObjectContainers.load(connectionId, catalogId);
return data.get(connectionId)!;
}
async restoreConnections() {
@@ -132,6 +123,20 @@ export class ConnectionsManagerService {
}
}
private async refreshObjectContainersAsync(
data: Map<string, ObjectContainer[]>,
refresh: boolean,
connectionId: string,
catalogId?: string,
): Promise<Map<string, ObjectContainer[]>> {
if (refresh || !data.has(connectionId)) {
const { navGetStructContainers } = await this.loadSchemasAndCatalogs(connectionId, catalogId);
data.set(connectionId, [...navGetStructContainers.schemaList, ...navGetStructContainers.catalogList]);
}
return data;
}
private async refreshDriversAsync(data: Map<string, DBDriver>): Promise<Map<string, DBDriver>> {
const { driverList } = await this.graphQLService.gql.driverList();
@@ -144,14 +149,6 @@ export class ConnectionsManagerService {
return data;
}
private closeConnectionTabs(id: string) {
// todo must be called from navigation service
const activeTabs = this.navigationTabsService.tabIdList.filter(tabId => tabId.includes(id));
for (const tabId of activeTabs) {
this.navigationTabsService.closeTab(tabId); // here must be async, like when we want to show prompt 'save before close'
}
}
/**
* Note that this request returns either schemaList or catalogList. You never got both lists together
*/
@@ -0,0 +1,23 @@
/*
* 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 { createExtension, isExtension, IExtension } from '../../../../extensions';
const connectionProviderSymbol = Symbol('@extension/ConnectionProvider');
export interface IConnectionProvider<T = never> {
(context: T): string | undefined;
}
export function connectionProvider<T>(provider: IConnectionProvider<T>) {
return createExtension<T>(provider, connectionProviderSymbol);
}
export function isConnectionProvider<T>(obj: IExtension<T>): obj is IConnectionProvider<T> & IExtension<T> {
return isExtension(obj, connectionProviderSymbol);
}
@@ -0,0 +1,23 @@
/*
* 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 { createExtension, isExtension, IExtension } from '../../../../extensions';
const connectionSetterSymbol = Symbol('@extension/ConnectionSetter');
export interface IConnectionSetter<T = never> {
(connectionId: string, context: T): Promise<boolean> | boolean;
}
export function connectionSetter<T>(setter: IConnectionSetter<T>) {
return createExtension<T>(setter, connectionSetterSymbol);
}
export function isConnectionSetter<T>(obj: IExtension<T>): obj is IConnectionSetter<T> & IExtension<T> {
return isExtension(obj, connectionSetterSymbol);
}
@@ -0,0 +1,19 @@
/*
* 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.
*/
export interface ITabOptions<T = any> {
id?: string;
handlerId: string;
handlerState: T;
name?: string;
icon?: string;
}
export interface ITab<T = any> extends ITabOptions<T> {
id: string;
}
@@ -8,15 +8,17 @@
import { observer } from 'mobx-react';
import { useCallback } from 'react';
import { css } from 'reshadow';
import styled, { css } from 'reshadow';
import { TabsBoxFromArray, TextPlaceholder } from '@dbeaver/core/blocks';
import {
TextPlaceholder, TabsBox, TabPanel
} from '@dbeaver/core/blocks';
import { useService } from '@dbeaver/core/di';
import { composes } from '@dbeaver/core/theming';
import { composes, useStyles } from '@dbeaver/core/theming';
import { NavigationTabsService } from '../NavigationTabsService';
import { TabContent } from './Tabs/TabContent';
import { TabData } from './Tabs/TabData';
import { TabHandlerPanel } from './Tabs/TabHandlerPanel';
import { TabHandlerTab } from './Tabs/TabHandlerTab';
const styles = composes(
css`
@@ -28,6 +30,7 @@ const styles = composes(
}
`
);
const stylesArray = [styles];
export const NavigationTabsBar = observer(function NavigationTabsBar() {
const navigation = useService(NavigationTabsService);
@@ -42,15 +45,19 @@ export const NavigationTabsBar = observer(function NavigationTabsBar() {
);
}
return (
<TabsBoxFromArray
return styled(useStyles(styles))(
<TabsBox
currentTabId={navigation.currentTabId}
tabIdList={navigation.tabIdList}
tab={TabContent}
panel={TabData}
onOpen={handleSelect}
onClose={handleClose}
style={[styles]}
/>
tabs={navigation.tabIdList.map(tabId => (
<TabHandlerTab key={tabId} tabId={tabId} onSelect={handleSelect} onClose={handleClose} style={stylesArray}/>
))}
style={stylesArray}
>
{navigation.tabIdList.map(tabId => (
<TabPanel key={tabId} tabId={tabId}>
<TabHandlerPanel tabId={tabId} />
</TabPanel>
))}
</TabsBox>
);
});
@@ -1,30 +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 { observer } from 'mobx-react';
import { TabIcon, TabTitle } from '@dbeaver/core/blocks';
import { useTabHandler } from './useTabHandler';
type HandlerContentProps = {
handlerId: string;
}
export const HandlerContent = observer(function HandlerContent({
handlerId,
}: HandlerContentProps) {
const handler = useTabHandler(handlerId);
return (
<>
<TabIcon icon={handler.icon}/>
<TabTitle title={handler.name}/>
</>
);
});
@@ -1,29 +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 { observer } from 'mobx-react';
import { useTabHandler } from './useTabHandler';
type HandlerPanelProps = {
tabId: string;
handlerId: string;
className?: string;
}
export const HandlerData = observer(function HandlerData({
tabId,
handlerId,
}: HandlerPanelProps) {
const handler = useTabHandler(handlerId);
const TabHandler = handler.getHandler();
return (
<TabHandler tabId={tabId} handlerId={handlerId} />
);
});
@@ -1,31 +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 { observer } from 'mobx-react';
import { TabTitle, TabIcon } from '@dbeaver/core/blocks';
import { useTab } from './useTab';
type TabContentProps = {
tabId: string;
className?: string;
}
export const TabContent = observer(function TabContent({
tabId,
}: TabContentProps) {
const tab = useTab(tabId);
return (
<>
<TabIcon icon={tab.icon} />
<TabTitle title={tab.name} />
</>
);
});
@@ -1,63 +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 { observer } from 'mobx-react';
import { useCallback } from 'react';
import styled, { css } from 'reshadow';
import {
Tab, TabPanel, TabsBox, TabElementProps,
} from '@dbeaver/core/blocks';
import { useService } from '@dbeaver/core/di';
import { useStyles, composes } from '@dbeaver/core/theming';
import { NavigationTabsService } from '../../NavigationTabsService';
import { HandlerContent } from './HandlerContent';
import { HandlerData } from './HandlerData';
import { useTab } from './useTab';
import { useTabHandlers } from './useTabHandlers';
const styles = composes(
css`
Tab {
composes: theme-ripple theme-background-surface theme-text-text-primary-on-light from global;
}
tabs {
composes: theme-background-background theme-text-text-primary-on-light from global;
}
`
);
export const TabData = observer(function TabData({ tabId }: TabElementProps) {
const tab = useTab(tabId);
const handlers = useTabHandlers(tabId);
const navigation = useService(NavigationTabsService);
const handleSelectHandler = useCallback((handlerId: string) => navigation.selectHandler(handlerId), [navigation]);
return styled(useStyles(styles))(
<TabsBox
currentTabId={tab.handlerId}
tabs={handlers.length > 1 && handlers.map(handlerId => (
<Tab
key={handlerId}
tabId={handlerId}
onOpen={handleSelectHandler}
>
<HandlerContent handlerId={handlerId}/>
</Tab>
))}
style={[styles]}
>
{handlers.map(handlerId => (
<TabPanel key={handlerId} tabId={handlerId}>
<HandlerData tabId={tabId} handlerId={handlerId} />
</TabPanel>
))}
</TabsBox>
);
});
@@ -0,0 +1,24 @@
/*
* 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 { observer } from 'mobx-react';
import { TabElementProps } from '@dbeaver/core/blocks';
import { useTab } from './useTab';
import { useTabHandler } from './useTabHandler';
export const TabHandlerPanel = observer(function TabHandlerPanel({
tabId,
}: TabElementProps) {
const tab = useTab(tabId);
const handler = useTabHandler(tab.handlerId);
const TabHandlerPanel = handler.getPanelComponent();
return <TabHandlerPanel tab={tab} handler={handler} />;
});
@@ -0,0 +1,31 @@
/*
* 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 { observer } from 'mobx-react';
import { Style } from '@dbeaver/core/theming';
import { useTab } from './useTab';
import { useTabHandler } from './useTabHandler';
type Props = {
tabId: string;
onSelect: (tabId: string) => void;
onClose?: (tabId: string) => void;
style: Style[];
}
export const TabHandlerTab = observer(function TabHandlerTab({
tabId, onSelect, onClose, style,
}: Props) {
const tab = useTab(tabId);
const handler = useTabHandler(tab.handlerId);
const TabHandlerTab = handler.getTabComponent();
return <TabHandlerTab tab={tab} handler={handler} onSelect={onSelect} onClose={onClose} style={style} />;
});
@@ -1,21 +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 { useService } from '@dbeaver/core/di';
import { NavigationTabsService } from '../../NavigationTabsService';
export function useTabHandlers(nodeId: string) {
const navigation = useService(NavigationTabsService);
const handlers = navigation.sortedHandlerList.filter(
handlerId => navigation.getTabHandler(handlerId)!.isActive(nodeId)
);
return handlers;
}
@@ -1,24 +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 { useService } from '@dbeaver/core/di';
import { NavigationTabsService } from '../NavigationTabsService';
export function useNavigationTabs() {
const navigation = useService(NavigationTabsService);
return {
tabs: navigation.tabIdList,
handlers: navigation.sortedHandlerList,
currentId: navigation.currentTabId,
selectHandler: (handlerId: string) => navigation.selectHandler(handlerId),
selectTab: (tabId: string) => navigation.selectTab(tabId),
closeTab: (tabId: string) => navigation.closeTab(tabId),
};
}
@@ -16,7 +16,7 @@ import { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { LocalStorageSaveService } from '@dbeaver/core/settings';
import { Tab, TabHandlerState } from './Tab';
import { ITab } from './ITab';
import { TabHandler, TabHandlerOptions, TabHandlerEvent } from './TabHandler';
import { TabNavigationContext, ITabNavigationContext } from './TabNavigationContext';
@@ -31,17 +31,13 @@ const NAVIGATION_TABS_BASE_KEY = 'navigation_tabs';
@injectable()
export class NavigationTabsService {
@observable handlers = new Map<string, TabHandler>();
@observable tabsMap = new Map<string, Tab>();
@observable tabsMap = new Map<string, ITab>();
@observable state: TabsState = {
tabs: [],
history: [],
currentId: '',
};
@computed get sortedHandlerList(): string[] {
return Array.from(this.handlers.keys()).sort((a, b) => this.compareHandlers(a, b));
}
@computed get currentTabId(): string {
return this.state.currentId;
}
@@ -50,29 +46,24 @@ export class NavigationTabsService {
return this.state.tabs;
}
onTabActivate = new Subject<Tab>();
onTabSelect = new Subject<ITab>();
onTabClose = new Subject<ITab>();
constructor(private notificationService: NotificationService,
private autoSaveService: LocalStorageSaveService) {
this.autoSaveService.withAutoSave(
this.tabsMap,
`${NAVIGATION_TABS_BASE_KEY}_tab_map`,
(json): IKeyValueMap<Tab> => {
const map: IKeyValueMap<Tab> = {};
for (const [key, value] of Object.entries(json as IKeyValueMap<Tab>)) {
(json): IKeyValueMap<ITab> => {
const map: IKeyValueMap<ITab> = {};
for (const [key, value] of Object.entries(json as IKeyValueMap<ITab>)) {
if (
typeof value.nodeId === 'string'
typeof value.id === 'string'
&& typeof value.handlerId === 'string'
&& typeof value.handlerState === 'object'
&& !Array.isArray(value.handlerState) // temporary for old sessions, can be removed later
&& (!value.name || typeof value.name === 'string')
&& (!value.icon || typeof value.icon === 'string')
) {
const handlerStateMap = observable.map(value.handlerState);
if (Array.from(handlerStateMap.values()).every(v => typeof v.handlerId === 'string')) {
value.handlerState = handlerStateMap;
map[key] = new Tab(value);
}
map[key] = value;
}
}
return map;
@@ -82,12 +73,12 @@ export class NavigationTabsService {
this.autoSaveService.withAutoSave(this.state, NAVIGATION_TABS_BASE_KEY);
}
@action openTab(tab: Tab, isSelected?: boolean) {
this.tabsMap.set(tab.nodeId, tab);
this.state.tabs.push(tab.nodeId);
@action openTab(tab: ITab, isSelected?: boolean) {
this.tabsMap.set(tab.id, tab);
this.state.tabs.push(tab.id);
if (isSelected) {
this.selectTab(tab.nodeId);
this.selectTab(tab.id);
}
}
@@ -98,6 +89,7 @@ export class NavigationTabsService {
}
if (this.state.currentId !== tabId) {
this.state.history = this.state.history.filter(id => id !== tabId);
this.state.history.unshift(tabId);
this.state.currentId = tabId;
}
@@ -106,41 +98,36 @@ export class NavigationTabsService {
this.callHandlerCallback(tab, handler => handler.onSelect);
}
this.onTabActivate.next(this.getTab(tabId));
this.onTabSelect.next(tab);
}
@action closeTab(tabId: string, skipHandlers?: boolean) {
@action async closeTab(tabId: string, skipHandlers?: boolean) {
const tab = this.tabsMap.get(tabId);
if (tab && !skipHandlers) {
this.callHandlerCallback(tab, handler => handler.onClose);
await this.callHandlerCallback(tab, handler => handler.onClose);
}
this.onTabClose.next(tab);
this.state.history = this.state.history.filter(id => id !== tabId);
this.tabsMap.delete(tabId);
this.state.tabs = this.state.tabs.filter(id => id !== tabId);
if (this.state.currentId === tabId) {
this.selectTab(this.state.history[0] || '', skipHandlers);
}
this.tabsMap.delete(tabId);
this.state.tabs = this.state.tabs.filter(id => id !== tabId);
}
@action registerTabHandler(options: TabHandlerOptions) {
this.handlers.set(options.key, new TabHandler(options));
@action registerTabHandler<TState>(
options: TabHandlerOptions<TState>,
): TabHandler<TState> {
const tabHandler = new TabHandler(options);
this.handlers.set(options.key, tabHandler);
return tabHandler;
}
@action selectHandler(handlerId: string) {
const tab = this.tabsMap.get(this.state.currentId);
if (!tab || tab.handlerId === handlerId) {
return;
}
tab.selectHandler(handlerId);
this.callHandlerCallback(tab, handler => handler.onSelect);
}
@action updateHandlerState<T>(tabId: string, state: TabHandlerState<T>) {
@action updateHandlerState<T>(tabId: string, state: T) {
const tab = this.tabsMap.get(tabId);
if (tab) {
tab.updateHandlerState(state);
tab.handlerState = state;
}
}
@@ -148,20 +135,41 @@ export class NavigationTabsService {
return this.handlers.get(handlerId);
}
getHandlerState<T>(tabId: string, handlerId: string): T | undefined {
getHandlerState<T>(tabId: string): T | undefined {
const tab = this.tabsMap.get(tabId);
if (!tab) {
return;
}
return tab.getHandlerState<T>(handlerId);
return tab.handlerState;
}
getTab(tabId: string): Tab | undefined {
getTab(tabId: string): ITab | undefined {
return this.tabsMap.get(tabId);
}
findTab<S extends ITab>(predicate: (tab: ITab) => tab is S): S | null;
findTab(predicate: (tab: ITab) => boolean): ITab | null;
findTab(predicate: (tab: ITab) => boolean): ITab | null {
for (const tab of this.tabsMap.values()) {
if (predicate(tab)) {
return tab;
}
}
return null;
}
findTabs(predicate: (tab: ITab) => boolean): Generator<ITab>;
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)) {
yield tab;
}
}
}
// must be executed with low priority, because this call runs many requests to backend and blocks others
async restoreTabs() {
const removedTabs: string[] = [];
@@ -180,7 +188,7 @@ export class NavigationTabsService {
await Promise.all(restoreTasks);
if (removedTabs.length > 0) {
this.notificationService.logError({ title: 'Some tabs cannot be load properly' });
this.notificationService.logError({ title: 'Some tabs cannot be restored properly', isSilent: true });
}
for (const tabId of removedTabs) {
@@ -194,48 +202,31 @@ export class NavigationTabsService {
navigationTabContext = (): ITabNavigationContext => new TabNavigationContext(this)
private async callHandlerCallback(tab: Tab, selector: (handler: TabHandler) => TabHandlerEvent | undefined) {
for (const handlerState of tab.handlerState.values()) {
const handler = this.handlers.get(handlerState.handlerId);
if (handler) {
const callback = selector(handler);
if (callback) {
callback.call(handler, tab.nodeId, tab.handlerId);
}
}
private async callHandlerCallback(tab: ITab, selector: (handler: TabHandler) => TabHandlerEvent | undefined) {
const handler = this.handlers.get(tab.handlerId);
if (!handler) {
return;
}
const callback = selector(handler);
if (callback) {
await callback.call(handler, tab);
}
}
private async restoreTab(tab: Tab, removedTabs: string[]): Promise<void> {
private async restoreTab(tab: ITab, removedTabs: string[]): Promise<void> {
let restoreFail = false;
try {
for (const handlerState of tab.handlerState.values()) {
const handler = this.handlers.get(handlerState.handlerId);
const handler = this.handlers.get(tab.handlerId);
if (!handler) {
restoreFail = true;
break;
}
if (handler.onRestore && !await handler.onRestore(tab.nodeId, tab.handlerId)) {
restoreFail = true;
break;
}
if (!handler || (handler.onRestore && !await handler.onRestore(tab))) {
restoreFail = true;
}
} catch {
restoreFail = true;
}
if (restoreFail) {
removedTabs.push(tab.nodeId);
removedTabs.push(tab.id);
}
}
private compareHandlers(a: string, b: string) {
return this.getHandlerOrder(a) - this.getHandlerOrder(b);
}
private getHandlerOrder(handlerId: string) {
return this.handlers.get(handlerId)?.order || Number.MAX_SAFE_INTEGER;
}
}
@@ -1,59 +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 { observable, action } from 'mobx';
export type TabHandlerState<T = null> = {
handlerId: string;
state: T;
}
export type TabOptions = {
nodeId: string;
handlerId: string;
handlerState: Map<string, TabHandlerState<any>>;
name?: string;
icon?: string;
}
export class Tab {
@observable nodeId: string
@observable handlerId: string
@observable handlerState: Map<string, TabHandlerState<any>>
@observable name?: string
@observable icon?: string
constructor(options: TabOptions) {
this.nodeId = options.nodeId;
this.handlerId = options.handlerId;
this.name = options.name;
this.icon = options.icon;
this.handlerState = options.handlerState;
}
@action selectHandler(handlerId: string) {
this.handlerId = handlerId;
}
@action updateHandlerState<T>(state: TabHandlerState<T>) {
if (this.hasHandler(state.handlerId)) {
const handlerState = this.handlerState.get(this.handlerId)!;
handlerState.state = state.state;
} else {
this.handlerState.set(state.handlerId, state);
}
}
hasHandler(handlerId: string) {
return this.handlerState.has(handlerId);
}
getHandlerState<T>(handlerId: string): T | undefined {
return this.handlerState.get(handlerId)?.state;
}
}
@@ -6,60 +6,58 @@
* you may not use this file except in compliance with the License.
*/
import { observable } from 'mobx';
import { IExtension } from '../../../extensions';
import { Style } from '../../../theming';
import { ITab } from './ITab';
export type TabHandlerComponent = React.FunctionComponent<{
tabId: string;
handlerId: string;
}>
export type TabHandlerTabProps<T = any> = {
tab: ITab<T>;
handler: TabHandler<T>;
onSelect(tabId: string): void;
onClose?(tabId: string): void;
style: Style[];
}
export type TabHandlerTabComponent<T = any> = React.FunctionComponent<TabHandlerTabProps<T>>
export type TabHandlerEvent = (tabId: string, handlerId: string) => void
export type TabRestoreEvent = (
tabId: string,
handlerId: string,
) => Promise<boolean> | boolean
export type TabHandlerPanelProps<T = any> = {
tab: ITab<T>;
handler: TabHandler<T>;
}
export type TabHandlerPanelComponent<T = any> = React.FunctionComponent<TabHandlerPanelProps<T>>
export type TabHandlerFilter = (nodeId: string) => boolean
export type TabHandlerEvent<T = any> = (tab: ITab<T>) => Promise<void> | void
export type TabRestoreEvent<T = any> = (tab: ITab<T>) => Promise<boolean> | boolean
export type TabHandlerOptions = {
export type TabHandlerOptions<TState = any> = {
key: string;
name: string;
icon: string;
navigatorId: string;
order: number;
priority: number;
getTabHandlerComponent: () => TabHandlerComponent;
/** Executed in Tab rendering pipeline */
isActive?: TabHandlerFilter;
onSelect?: TabHandlerEvent;
onClose?: TabHandlerEvent;
onRestore?: TabRestoreEvent;
getTabComponent(): TabHandlerTabComponent<TState>;
getPanelComponent(): TabHandlerPanelComponent<TState>;
onSelect?: TabHandlerEvent<TState>;
onClose?: TabHandlerEvent<TState>;
onRestore?: TabRestoreEvent<TState>;
extensions?: IExtension<ITab<TState>>[];
}
export class TabHandler {
@observable key: string
@observable name: string
@observable icon: string
@observable navigatorId: string
@observable order: number
@observable priority: number
@observable getHandler: () => TabHandlerComponent
@observable isActive: TabHandlerFilter
@observable onSelect?: TabHandlerEvent
@observable onClose?: TabHandlerEvent
@observable onRestore?: TabRestoreEvent
export class TabHandler<TState = any> {
key: string
navigatorId: string
getTabComponent: () => TabHandlerTabComponent<TState>;
getPanelComponent: () => TabHandlerPanelComponent<TState>;
onSelect?: TabHandlerEvent<TState>
onClose?: TabHandlerEvent<TState>
onRestore?: TabRestoreEvent<TState>
extensions?: IExtension<ITab<TState>>[];
constructor(options: TabHandlerOptions) {
constructor(options: TabHandlerOptions<TState>) {
this.key = options.key;
this.name = options.name;
this.icon = options.icon;
this.navigatorId = options.navigatorId;
this.order = options.order;
this.priority = options.priority;
this.getHandler = options.getTabHandlerComponent;
this.isActive = options.isActive || (() => true);
this.getTabComponent = options.getTabComponent;
this.getPanelComponent = options.getPanelComponent;
this.onSelect = options.onSelect;
this.onClose = options.onClose;
this.onRestore = options.onRestore;
this.extensions = options.extensions;
}
}
@@ -6,15 +6,19 @@
* you may not use this file except in compliance with the License.
*/
import { observable } from 'mobx';
import { uuid } from '@dbeaver/core/utils';
import { ITab, ITabOptions } from './ITab';
import { NavigationTabsService } from './NavigationTabsService';
import { Tab, TabOptions } from './Tab';
import { TabHandlerOptions } from './TabHandler';
export interface ITabNavigationContext {
readonly isNewlyCreated: boolean;
readonly handlerPriority: number;
trySwitchHandler(handler: TabHandlerOptions): void;
openNewTab(options: TabOptions): Tab;
readonly tab: ITab | null;
openNewTab<T = any>(options: ITabOptions<T>): ITab<T>;
registerTab(tab: ITab): void;
}
export class TabNavigationContext implements ITabNavigationContext {
@@ -24,22 +28,26 @@ export class TabNavigationContext implements ITabNavigationContext {
get handlerPriority() {
return this._handlerPriority;
}
get tab() {
return this._tab;
}
private _isNewlyCreated = false;
private _handlerPriority = 0;
private _tab: ITab | null = null;
constructor(private navigationTabsService: NavigationTabsService) { }
trySwitchHandler(handler: TabHandlerOptions) {
if (this.handlerPriority < handler.priority) {
this._handlerPriority = handler.priority;
this.navigationTabsService.selectHandler(handler.key);
}
openNewTab<T = any>(options: ITabOptions<T>): ITab<T> {
this._tab = observable({
id: uuid(),
...options,
});
this._isNewlyCreated = true;
this.navigationTabsService.openTab(this._tab, true);
return this._tab;
}
openNewTab(options: TabOptions) {
const newTab = new Tab(options);
this._isNewlyCreated = true;
this.navigationTabsService.openTab(newTab, true);
return newTab;
registerTab(tab: ITab) {
this._tab = tab;
}
}
@@ -1,24 +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 { useService } from '@dbeaver/core/di';
import { NavigationTabsService } from './NavigationTabsService';
export function useTabHandlerState<T>(tabId: string, handlerId: string) {
const navigationTabs = useService(NavigationTabsService);
const tab = navigationTabs.getTab(tabId);
if (!tab) {
throw new Error(`Tab ${tabId} not found`);
}
const state = tab.getHandlerState<T>(handlerId);
return state;
}
@@ -7,53 +7,75 @@
*/
import { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { DatabaseObjectInfo, GraphQLService } from '@dbeaver/core/sdk';
import { IConnectionCatalogSchema } from '../../TopNavBar/ConnectionSchemaManager/IConnectionCatalogSchema';
import { INavigator } from '../Navigation/INavigator';
import { IContextProvider } from '../Navigation/NavigationContext';
import { NavigationService } from '../Navigation/NavigationService';
import { NavigationTabsService } from '../NavigationTabs/NavigationTabsService';
import { ENodeFeature } from './ENodeFeature';
import { EObjectFeature } from './EObjectFeature';
import { NodesStore } from './NodesStore';
import { DatabaseObjectInfoWithId, NodeChildren, NodeWithParent } from './NodeWithParent';
export enum NavigationType {
open,
closeConnection
}
export interface INodeContainerInfo {
connectionId?: string;
catalogId?: string;
schemaId?: string;
}
export interface INodeNavigationContext {
type: NavigationType;
nodeId: string;
childrenId: string;
folderId: string;
name?: string;
icon?: string;
}
export interface INodeNavigationData {
type: NavigationType;
nodeId: string;
folderId?: string;
}
const ROOT_NODE_PATH = '/';
// TODO: should be renamed to DBObjectManagerService
@injectable()
export class NodesManagerService {
readonly navigator!: INavigator<INodeNavigationData>;
private nodesStore = new NodesStore();
constructor(private graphQLService: GraphQLService,
private navigationService: NavigationService,
private navigationTabsService: NavigationTabsService,
private notificationService: NotificationService) {
private navigationService: NavigationService) {
this.navigator = this.navigationService.createNavigator<INodeNavigationData>(
data => data.nodeId,
this.navigateHandler.bind(this),
{
type: NavigationType.open,
nodeId: ROOT_NODE_PATH,
}
);
}
navToNode(nodeId: string) {
this.navigator.navigateTo({ nodeId });
async closeConnection(nodeId: string) {
await this.navigator.navigateTo({
type: NavigationType.closeConnection,
nodeId,
});
}
async navToNode(nodeId: string, folderId?: string) {
await this.navigator.navigateTo({
type: NavigationType.open,
nodeId,
folderId,
});
}
getDatabaseObjectInfo(nodeId: string): DatabaseObjectInfoWithId | undefined {
@@ -91,7 +113,7 @@ export class NodesManagerService {
const { objectInfo } = await this.graphQLService.gql.queryDatabaseObjectInfo({
nodeId,
filter: { features: ['viewable', 'editPossible'] },
// filter: { features: ['viewable', 'editPossible'] },
});
const info = { ...objectInfo.object, id: objectInfo.id };
@@ -120,7 +142,7 @@ export class NodesManagerService {
childrenDatabaseObjectInfo,
} = await this.graphQLService.gql.queryChildrenDatabaseObjectInfo({
nodePath: nodeId,
filter: { features: ['viewable', 'editPossible'] }, // filter properties for view
// filter: { features: ['viewable', 'editPossible'] }, // filter properties for view
});
const children = childrenDatabaseObjectInfo.map(children => ({
@@ -133,12 +155,21 @@ export class NodesManagerService {
}
async loadNodeInfo(nodeId: string): Promise<NodeWithParent> {
const node = this.nodesStore.getNode(nodeId);
let node = this.nodesStore.getNode(nodeId);
if (node) {
if (node.parentId !== ROOT_NODE_PATH) {
await this.loadNodeInfo(node.parentId);
}
return node;
}
return this.updateNodeInfo(nodeId);
node = await this.updateNodeInfo(nodeId);
if (node.parentId !== ROOT_NODE_PATH) {
await this.loadNodeInfo(node.parentId);
}
return node;
}
async loadChildren(parentId = ROOT_NODE_PATH): Promise<string[]> {
@@ -179,11 +210,11 @@ export class NodesManagerService {
return this.nodesStore.getNode(node.parentId);
}
getConnectionCatalogSchema(nodeId: string): Partial<IConnectionCatalogSchema> {
const initial: Partial<IConnectionCatalogSchema> = {};
getNodeContainerInfo(nodeId: string): INodeContainerInfo {
const initial: INodeContainerInfo = {};
const scanParents = (res: Partial<IConnectionCatalogSchema>,
nodeId?: string): Partial<IConnectionCatalogSchema> => {
const scanParents = (res: INodeContainerInfo,
nodeId?: string): INodeContainerInfo => {
if (!nodeId) {
return res;
}
@@ -194,11 +225,11 @@ export class NodesManagerService {
if (node?.object?.features?.includes(EObjectFeature.dataSource)) {
res.connectionId = node.id;
}
if (node?.object?.features?.includes(EObjectFeature.schema)) {
res.schemaId = node.name || null; // note that schemaId is node name
}
if (node?.object?.features?.includes(EObjectFeature.catalog)) {
res.catalogId = node.name || null; // note that catalogId is node name
res.catalogId = node.name; // note that catalogId is node name
}
if (node?.object?.features?.includes(EObjectFeature.schema)) {
res.schemaId = node.name; // note that schemaId is node name
}
return scanParents(res, node.parentId);
};
@@ -215,41 +246,40 @@ export class NodesManagerService {
data: INodeNavigationData
): Promise<INodeNavigationContext> => {
let nodeId = data.nodeId;
let childrenId = '';
const node = await this.loadNodeInfo(nodeId);
let name = node.name;
let icon = node.icon;
let folderId = '';
let name: string | undefined;
let icon: string | undefined;
if (node.folder) {
const parent = await this.loadNodeInfo(node.parentId);
childrenId = nodeId;
if (parent && !parent.folder) {
nodeId = parent.id;
name = parent.name;
icon = parent.icon;
if (isDatabaseObject(nodeId) && data.type !== NavigationType.closeConnection) {
const node = await this.loadNodeInfo(nodeId);
name = node.name;
icon = node.icon;
if (node.folder) {
const parent = await this.loadNodeInfo(node.parentId);
folderId = nodeId;
if (parent && !parent.folder) {
nodeId = parent.id;
name = parent.name;
icon = parent.icon;
}
}
if (data.folderId) {
folderId = data.folderId;
}
}
return {
type: data.type,
nodeId,
childrenId,
folderId,
name,
icon,
};
}
async navigateHandler(contexts: IContextProvider<INodeNavigationData>) {
try {
const nodeInfo = await contexts.getContext(this.navigationNodeContext);
const tab = this.navigationTabsService.getTab(nodeInfo.nodeId);
if (tab) {
tab.name = nodeInfo.name;
tab.icon = nodeInfo.icon;
}
} catch (exception) {
this.notificationService.logException(exception, 'Can\'t perform action with database object');
}
}
isNodeHasData(info?: string | DatabaseObjectInfo) {
@@ -264,5 +294,12 @@ export class NodesManagerService {
return info.features.includes(ENodeFeature.dataContainer)
|| info.features.includes(ENodeFeature.container);
}
}
export function isDatabaseObject(objectId: string) {
return /^database:\/\//.test(objectId);
}
export function concatSchemaAndCatalog(catalogId?: string, schemaId?: string) {
return `${schemaId || ''}${schemaId && catalogId ? '@' : ''}${catalogId || ''}`;
}
@@ -0,0 +1,23 @@
/*
* 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 { createExtension, isExtension, IExtension } from '../../../../extensions';
const objectCatalogProviderSymbol = Symbol('@extension/ObjectCatalogProvider');
export interface IObjectCatalogProvider<T = never> {
(context: T): string | undefined;
}
export function objectCatalogProvider<T>(provider: IObjectCatalogProvider<T>) {
return createExtension<T>(provider, objectCatalogProviderSymbol);
}
export function isObjectCatalogProvider<T>(obj: IExtension<T>): obj is IObjectCatalogProvider<T> & IExtension<T> {
return isExtension(obj, objectCatalogProviderSymbol);
}
@@ -0,0 +1,23 @@
/*
* 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 { createExtension, isExtension, IExtension } from '../../../../extensions';
const objectCatalogSetterSymbol = Symbol('@extension/ObjectCatalogSetter');
export interface IObjectCatalogSetter<T = never> {
(catalogId: string, context: T): Promise<boolean> | boolean;
}
export function objectCatalogSetter<T>(provider: IObjectCatalogSetter<T>) {
return createExtension<T>(provider, objectCatalogSetterSymbol);
}
export function isObjectCatalogSetter<T>(obj: IExtension<T>): obj is IObjectCatalogSetter<T> & IExtension<T> {
return isExtension(obj, objectCatalogSetterSymbol) as boolean;
}
@@ -0,0 +1,23 @@
/*
* 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 { createExtension, isExtension, IExtension } from '../../../../extensions';
const objectSchemaProviderSymbol = Symbol('@extension/ObjectSchemaProvider');
export interface IObjectSchemaProvider<T = never> {
(context: T): string | undefined;
}
export function objectSchemaProvider<T>(provider: IObjectSchemaProvider<T>) {
return createExtension<T>(provider, objectSchemaProviderSymbol);
}
export function isObjectSchemaProvider<T>(obj: IExtension<T>): obj is IObjectSchemaProvider<T> & IExtension<T> {
return isExtension(obj, objectSchemaProviderSymbol);
}
@@ -0,0 +1,23 @@
/*
* 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 { createExtension, isExtension, IExtension } from '../../../../extensions';
const objectSchemaSetterSymbol = Symbol('@extension/ObjectSchemaSetter');
export interface IObjectSchemaSetter<T = never> {
(schemaId: string, context: T): Promise<boolean> | boolean;
}
export function objectSchemaSetter<T>(provider: IObjectSchemaSetter<T>) {
return createExtension<T>(provider, objectSchemaSetterSymbol);
}
export function isObjectSchemaSetter<T>(obj: IExtension<T>): obj is IObjectSchemaSetter<T> & IExtension<T> {
return isExtension(obj, objectSchemaSetterSymbol) as boolean;
}
@@ -44,7 +44,6 @@ export function TabsBox({
{tabs && (
<tabs as="div">
{tabs}
<tab-fill as="div" />
</tabs>
)}
<tab-panels as="div">
@@ -0,0 +1,15 @@
/*
* 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 { EXTENSION_SYMBOL, EXTENSION_TYPE_SYMBOL } from './constants';
export interface IExtension<T> {
[EXTENSION_SYMBOL]: symbol;
[EXTENSION_TYPE_SYMBOL]?: T;
}
@@ -0,0 +1,10 @@
/*
* 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.
*/
export const EXTENSION_SYMBOL = Symbol('@extension');
export const EXTENSION_TYPE_SYMBOL = Symbol('@extension/type');
@@ -0,0 +1,30 @@
/*
* 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 { EXTENSION_SYMBOL } from './constants';
import { IExtension } from './IExtension';
export function createExtension<T>(
extension: object,
key: symbol
): IExtension<T> {
return Object.assign(extension, { [EXTENSION_SYMBOL]: key });
}
export function isExtension<T>(
obj: any,
key?: symbol,
): obj is IExtension<T> {
if (!(EXTENSION_SYMBOL in obj)) {
return false;
}
if (key && obj[EXTENSION_SYMBOL] !== key) {
return false;
}
return true;
}
@@ -0,0 +1,2 @@
export * from './IExtension';
export * from './createExtension';
+44 -15
View File
@@ -8,14 +8,15 @@
import { observable } from 'mobx';
export type Loader<TData, TArgs extends any[]> = (current: TData, ...args: TArgs) => Promise<TData>
export type Loader<TData, TArgs extends any[]> = (current: TData, update: boolean, ...args: TArgs) => Promise<TData>
export class CachedResource<TData, TArgs extends any[] = []> {
@observable data: TData;
@observable private loaded = false;
@observable private loading = false;
private promise: Promise<TData> | null = null;
private refreshPromise: Promise<TData> | null = null;
private singleElementPromise: Promise<TData> | null = null;
private loader: Loader<TData, TArgs>;
constructor(defaultValue: TData, loader: Loader<TData, TArgs>) {
@@ -32,36 +33,64 @@ export class CachedResource<TData, TArgs extends any[] = []> {
}
async refresh(...args: TArgs): Promise<TData> {
if (this.promise) {
return this.promise;
}
this.promise = this.loadingTask(...args);
try {
await this.promise;
} finally {
this.promise = null;
if (args.length > 0) {
await this.loadSingle(true, args);
} else {
await this.loadAll(true, args);
}
return this.data;
}
async load(...args: TArgs): Promise<TData> {
if (!this.loaded) {
await this.refresh(...args);
if (args.length > 0) {
await this.loadSingle(false, args);
} else {
await this.loadAll(false, args);
}
}
return this.data;
}
private async loadingTask(...args: TArgs): Promise<TData> {
this.loaded = false;
private async loadAll(update: boolean, args: TArgs) {
if (this.refreshPromise) {
return this.refreshPromise;
}
this.refreshPromise = this.loadingTask(update, args);
try {
await this.refreshPromise;
} finally {
this.refreshPromise = null;
}
}
private async loadSingle(update: boolean, args: TArgs) {
if (this.singleElementPromise) {
return this.singleElementPromise;
}
this.singleElementPromise = this.loadingTask(update, args);
try {
await this.singleElementPromise;
} finally {
this.singleElementPromise = null;
}
}
private async loadingTask(update: boolean, args: TArgs): Promise<TData> {
if (args.length === 0) {
this.loaded = false;
}
this.loading = true;
try {
this.data = await this.loader(this.data, ...args);
this.data = await this.loader(this.data, update, ...args);
} finally {
this.loading = false;
}
this.loaded = true;
if (args.length === 0) {
this.loaded = true;
}
return this.data;
}
}
+6 -10
View File
@@ -1,11 +1,3 @@
/*
* 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.
*/
/* eslint-disable */
import { GraphQLClient } from "graphql-request";
import { print } from "graphql";
@@ -142,6 +134,7 @@ export type DriverInfo = {
licenseRequired?: Maybe<Scalars["Boolean"]>;
license?: Maybe<Scalars["String"]>;
custom?: Maybe<Scalars["Boolean"]>;
/** Driver score for ordering, biggest first */
promotedScore?: Maybe<Scalars["Int"]>;
connectionProperties?: Maybe<Scalars["Object"]>;
defaultConnectionProperties?: Maybe<Scalars["Object"]>;
@@ -427,6 +420,7 @@ export type ServerConfig = {
supportsWorkspaces?: Maybe<Scalars["Boolean"]>;
supportedLanguages?: Maybe<Array<Maybe<ServerLanguage>>>;
services?: Maybe<Array<Maybe<WebServiceConfig>>>;
productConfiguration: Scalars["Object"];
};
export type ServerError = {
@@ -554,10 +548,10 @@ export type NavGetStructContainersQueryVariables = {
export type NavGetStructContainersQuery = {
navGetStructContainers: {
catalogList: Array<
Pick<DatabaseObjectInfo, "name" | "description" | "type">
Pick<DatabaseObjectInfo, "name" | "description" | "type" | "features">
>;
schemaList: Array<
Pick<DatabaseObjectInfo, "name" | "description" | "type">
Pick<DatabaseObjectInfo, "name" | "description" | "type" | "features">
>;
};
};
@@ -1143,11 +1137,13 @@ export const NavGetStructContainersDocument = gql`
name
description
type
features
}
schemaList {
name
description
type
features
}
}
}
@@ -13,6 +13,7 @@
},
"peerDependencies": {
"@dbeaver/ag-grid-plugin": "^0.1.0",
"@dbeaver/object-viewer-plugin": "^0.1.0",
"@dbeaver/core": "^0.1.0",
"@material/button": "^4.0.0",
"@material/elevation": "^4.0.0",
@@ -6,19 +6,13 @@
* you may not use this file except in compliance with the License.
*/
import { PropsWithChildren } from 'react';
import { ObjectPagePanelProps } from '@dbeaver/object-viewer-plugin';
import { TableViewer } from '../TableViewer/TableViewer';
export const DataViewerPanel = function DataViewerPanel({
tab,
}: ObjectPagePanelProps) {
type DataViewerProps = PropsWithChildren<{
tabId: string;
handlerId: string;
}>
export const DataViewer = function DataViewer({
tabId,
}: DataViewerProps) {
return <TableViewer tableId={tabId}/>;
return <TableViewer tableId={tab.handlerState.objectId}/>;
};
@@ -0,0 +1,33 @@
/*
* 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 { observer } from 'mobx-react';
import styled from 'reshadow';
import { NodesManagerService } from '@dbeaver/core/app';
import { TabIcon, Tab, TabTitle } from '@dbeaver/core/blocks';
import { useService } from '@dbeaver/core/di';
import { useStyles } from '@dbeaver/core/theming';
import { ObjectPageTabProps } from '@dbeaver/object-viewer-plugin';
export const DataViewerTab = observer(function DataViewerTab({
tab, page, onSelect, style,
}: ObjectPageTabProps) {
const nodesManagerService = useService(NodesManagerService);
if (!nodesManagerService.isNodeHasData(tab.handlerState.objectId)) {
return null;
}
return styled(useStyles(...style))(
<Tab tabId={page.key} onOpen={onSelect} >
<TabIcon icon='/icons/grid.png' />
<TabTitle title='Data' />
</Tab>
);
});
@@ -7,116 +7,101 @@
*/
import {
NavigationTabsService,
NodesManagerService,
TabHandlerOptions,
INodeNavigationData,
IContextProvider,
ITab,
NavigationType,
} from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import {
DBObjectPageService, ObjectPage, ObjectViewerTabService, IObjectViewerTabState
} from '@dbeaver/object-viewer-plugin';
import { dataViewerHandlerKey } from './dataViewerHandlerKey';
import { DataViewer } from './DataViewerTab/DataViewer';
import { DataViewerPanel } from './DataViewerPage/DataViewerPanel';
import { DataViewerTab } from './DataViewerPage/DataViewerTab';
import { DataViewerTableService } from './DataViewerTableService';
@injectable()
export class DataViewerTabService {
private tabHandler: TabHandlerOptions = {
key: dataViewerHandlerKey,
name: 'Data',
icon: '/icons/grid.png',
navigatorId: 'database',
order: 2,
priority: 2,
getTabHandlerComponent: () => DataViewer,
onSelect: this.handleTabSelect.bind(this),
onRestore: this.handleTabRestore.bind(this),
onClose: this.handleTabClose.bind(this),
isActive: this.isTabActive.bind(this), // executed in Tab rendering pipeline
};
page: ObjectPage;
constructor(private nodesManagerService: NodesManagerService,
private dataViewerTableService: DataViewerTableService,
private navigationTabsService: NavigationTabsService,
private objectViewerTabService: ObjectViewerTabService,
private dbObjectPageService: DBObjectPageService,
private notificationService: NotificationService) {
this.page = this.dbObjectPageService.register({
key: dataViewerHandlerKey,
navigatorId: 'database',
priority: 2,
order: 2,
getTabComponent: () => DataViewerTab,
getPanelComponent: () => DataViewerPanel,
onSelect: this.handleTabSelect.bind(this),
onRestore: this.handleTabRestore.bind(this),
onClose: this.handleTabClose.bind(this),
});
}
registerTabHandler() {
this.navigationTabsService.registerTabHandler(this.tabHandler);
this.nodesManagerService.navigator.addHandler(this.navigationHandler.bind(this));
}
isTabActive(tabId: string) {
return this.nodesManagerService.isNodeHasData(tabId);
}
private async navigationHandler(contexts: IContextProvider<INodeNavigationData>) {
try {
const tabInfo = await contexts.getContext(this.navigationTabsService.navigationTabContext);
const nodeInfo = await contexts.getContext(this.nodesManagerService.navigationNodeContext);
const {
nodeInfo,
tabInfo,
trySwitchPage,
} = await contexts.getContext(this.objectViewerTabService.objectViewerTabContext);
if (nodeInfo.type === NavigationType.closeConnection) {
return;
}
// const tabInfo = await contexts.getContext(this.navigationTabsService.navigationTabContext);
const objectInfo = await this.nodesManagerService.loadDatabaseObjectInfo(nodeInfo.nodeId);
if (!this.nodesManagerService.isNodeHasData(objectInfo)) {
return;
}
const tab = this.navigationTabsService.getTab(nodeInfo.nodeId);
if (tab) {
if (!tab.hasHandler(dataViewerHandlerKey)) {
tab.updateHandlerState({
handlerId: dataViewerHandlerKey,
state: null,
});
}
} else {
tabInfo.openNewTab({
nodeId: nodeInfo.nodeId,
handlerId: dataViewerHandlerKey,
handlerState: new Map([[
dataViewerHandlerKey,
{
handlerId: dataViewerHandlerKey,
state: null,
},
]]),
name: nodeInfo.name,
icon: nodeInfo.icon,
});
tabInfo.trySwitchHandler(this.tabHandler); // todo it is wrong place to call it
return;
}
this.navigationTabsService.selectTab(nodeInfo.nodeId);
if (nodeInfo.childrenId === '') {
tabInfo.trySwitchHandler(this.tabHandler);
if (tabInfo.isNewlyCreated) {
trySwitchPage(this.page);
}
// if (nodeInfo.childrenId === '') {
// tabInfo.trySwitchHandler(this.tabHandler);
// }
} catch (exception) {
this.notificationService.logException(exception, 'Error in Data Viewer while processing action with database node');
}
}
private handleTabSelect(tableId: string, handlerId: string) {
if (handlerId !== dataViewerHandlerKey) {
private async handleTabSelect(tab: ITab<IObjectViewerTabState>) {
const objectInfo = await this.nodesManagerService.loadDatabaseObjectInfo(tab.handlerState.objectId);
if (!this.nodesManagerService.isNodeHasData(objectInfo)) {
return;
}
this.dataViewerTableService.createTableModelIfNotExists(tableId);
this.dataViewerTableService.createTableModelIfNotExists(tab.handlerState.objectId);
}
private async handleTabRestore(tabId: string, handlerId: string) {
const tab = this.navigationTabsService.getTab(tabId);
if (tab && tab.hasHandler(dataViewerHandlerKey)) {
const info = await this.nodesManagerService.loadDatabaseObjectInfo(tabId);
if (info) {
return true;
}
private async handleTabRestore(tab: ITab<IObjectViewerTabState>) {
// if (!this.nodesManagerService.isNodeHasData(tab.handlerState.objectId)) {
// return;
// }
const info = await this.nodesManagerService.loadDatabaseObjectInfo(tab.handlerState.objectId);
if (info) {
return true;
}
return false;
}
private handleTabClose(tableId: string) {
this.dataViewerTableService.removeTableModel(tableId);
private handleTabClose(tab: ITab<IObjectViewerTabState>) {
this.dataViewerTableService.removeTableModel(tab.handlerState.objectId);
}
}
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { ConnectionsManagerService, IConnectionCatalogSchema } from '@dbeaver/core/app';
import { ConnectionsManagerService } from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { CommonDialogService } from '@dbeaver/core/dialogs';
import { GraphQLService } from '@dbeaver/core/sdk';
@@ -15,8 +15,12 @@ import { RowDiff } from './TableViewer/TableDataModel/EditedRow';
import { IRequestDataResult, ITableViewerModelInit, TableViewerModel } from './TableViewer/TableViewerModel';
import { TableViewerStorageService } from './TableViewer/TableViewerStorageService';
interface ISqlContextParams extends IConnectionCatalogSchema {
interface ISqlContextParams {
contextId: string;
connectionId: string;
objectCatalogId?: string;
objectSchemaId?: string;
}
export interface IDataViewerTableModel {
@@ -64,14 +68,22 @@ export class DataViewerTableService {
return new TableViewerModel(callbacks, this.commonDialogService);
}
private async createSqlContext(connectionCatalogSchema: IConnectionCatalogSchema): Promise<ISqlContextParams> {
private async createSqlContext(
connectionId: string,
defaultCatalog?: string,
defaultSchema?: string
): Promise<ISqlContextParams> {
const response = await this.graphQLService.gql.sqlContextCreate(connectionCatalogSchema);
const response = await this.graphQLService.gql.sqlContextCreate({
connectionId,
defaultCatalog,
defaultSchema,
});
return {
contextId: response.context.id,
connectionId: connectionCatalogSchema.connectionId,
catalogId: response.context.defaultCatalog || null,
schemaId: response.context.defaultSchema || null,
connectionId,
objectCatalogId: response.context.defaultCatalog,
objectSchemaId: response.context.defaultSchema,
};
}
@@ -112,11 +124,7 @@ export class DataViewerTableService {
if (!data.sqlContextParams) {
// it is first data request
const sqlContextParams: ISqlContextParams = await this.createSqlContext({
connectionId: data.connectionId,
catalogId: null,
schemaId: null,
});
const sqlContextParams: ISqlContextParams = await this.createSqlContext(data.connectionId);
data.sqlContextParams = sqlContextParams;
}
+1 -1
View File
@@ -44,7 +44,7 @@
"module-alias": "^2.2.2",
"node-sass": "^4.13.0",
"postcss-import": "^12.0.1",
"preact": "^10.4.0",
"preact": "^10.4.1",
"preact-render-to-string": "^5.1.6",
"preact-ssr-prepass": "^1.0.1",
"react": "^16.13.1",
@@ -55,7 +55,7 @@ export class DdlViewerController implements IInitializableController, IDestructi
}
private async loadDialect(nodeId: string): Promise<void> {
const { connectionId } = this.nodesManagerService.getConnectionCatalogSchema(nodeId);
const { connectionId } = this.nodesManagerService.getNodeContainerInfo(nodeId);
if (!connectionId) {
return;
}
@@ -6,20 +6,27 @@
* you may not use this file except in compliance with the License.
*/
import { EObjectFeature, NavigationTabsService, NodesManagerService } from '@dbeaver/core/app';
import {
EObjectFeature, NodesManagerService, IContextProvider, INodeNavigationData
} from '@dbeaver/core/app';
import { ITab } from '@dbeaver/core/blocks';
import { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { ObjectViewerTabService } from '@dbeaver/object-viewer-plugin';
import { ddlViewer } from './DdlViewer/DdlViewer';
import { DdlViewerService } from './DdlViewerService';
const ddlViewerTabId = 'ddl-viewer';
@injectable()
export class DdlViewerTabService {
constructor(private nodesManagerService: NodesManagerService,
private navigationTabsService: NavigationTabsService,
private objectViewerTabService: ObjectViewerTabService,
private notificationService: NotificationService,
private ddlViewerService: DdlViewerService) {
this.nodesManagerService.navigator.addHandler(this.navigationHandler.bind(this));
}
buildTab(nodeId: string): ITab | null {
@@ -29,7 +36,7 @@ export class DdlViewerTabService {
return null;
}
const ddlTab: ITab = {
tabId: 'ddl-viewer',
tabId: ddlViewerTabId,
title: 'DDL',
icon: 'sql-text',
onActivate: () => this.activateDDLTab(nodeId),
@@ -38,17 +45,19 @@ export class DdlViewerTabService {
return ddlTab;
}
private activateDDLTab(nodeId: string) {
const navigationTab = this.navigationTabsService.getTab(nodeId);
private async navigationHandler(contexts: IContextProvider<INodeNavigationData>) {
try {
const tabContext = await contexts.getContext(this.objectViewerTabService.objectViewerTabContext);
if (!navigationTab) {
throw new Error(`Tab ${nodeId} not found`);
if (tabContext.nodeInfo.folderId === ddlViewerTabId) {
this.ddlViewerService.loadDdlMetadata(tabContext.nodeInfo.nodeId);
}
} catch (exception) {
this.notificationService.logException(exception, 'Error in Object Viewer while processing action with ddl-viewer');
}
// todo this must be refactored
navigationTab.updateHandlerState({
handlerId: navigationTab.handlerId,
state: 'ddl-viewer',
});
this.ddlViewerService.loadDdlMetadata(nodeId);
}
private activateDDLTab(nodeId: string) {
this.nodesManagerService.navToNode(nodeId, ddlViewerTabId);
}
}
@@ -0,0 +1,20 @@
/*
* 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 { ITabNavigationContext, INodeNavigationContext, ITab } from '@dbeaver/core/app';
import { IObjectViewerTabState } from './IObjectViewerTabState';
import { ObjectPage } from './ObjectPage/ObjectPage';
export interface IObjectViewerTabContext {
tab: ITab<IObjectViewerTabState> | null;
page?: ObjectPage;
trySwitchPage(page: ObjectPage): boolean;
tabInfo: ITabNavigationContext;
nodeInfo: INodeNavigationContext;
}
@@ -0,0 +1,14 @@
/*
* 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.
*/
export interface IObjectViewerTabState {
objectId: string;
folderId: string;
pageId: string;
pagesState: Map<string, any>;
}
@@ -0,0 +1,19 @@
/*
* 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 { observer } from 'mobx-react';
import { ObjectPagePanelProps } from './ObjectPage';
export const DBObjectPagePanel = observer(function DBObjectPagePanel({
tab,
page,
}: ObjectPagePanelProps) {
const Panel = page.getPanelComponent();
return <Panel tab={tab} page={page} />;
});
@@ -0,0 +1,84 @@
/*
* 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 { observable, action, computed } from 'mobx';
import { ITab } from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { IObjectViewerTabState } from '../IObjectViewerTabState';
import { ObjectPage, ObjectPageOptions, ObjectPageCallback } from './ObjectPage';
@injectable()
export class DBObjectPageService {
@observable pages = new Map<string, ObjectPage>();
@computed get orderedPages(): ObjectPage[] {
return Array.from(this.pages.values())
.sort(this.comparePages.bind(this));
}
@action register(options: ObjectPageOptions): ObjectPage {
const objectPage = new ObjectPage(options);
this.pages.set(options.key, objectPage);
return objectPage;
}
getPage(pageId: string) {
return this.pages.get(pageId);
}
trySwitchPage(tab: ITab<IObjectViewerTabState>, page: ObjectPage): boolean {
const currentPage = this.getPage(tab.handlerState.pageId);
if ((currentPage?.priority || 0) < page.priority) {
this.selectPage(tab, page);
return true;
}
return false;
}
selectPage = async (tab: ITab<IObjectViewerTabState>, page: ObjectPage) => {
tab.handlerState.pageId = page.key;
await this.callHandlerCallback(tab, page => page.onSelect);
}
async restorePages(tab: ITab<IObjectViewerTabState>): Promise<boolean> {
for (const page of this.pages.values()) {
if (page.onRestore && !page.onRestore(tab)) {
return false;
}
}
return true;
}
async closePages(tab: ITab<IObjectViewerTabState>) {
await this.callHandlerCallback(tab, page => page.onClose);
}
private async callHandlerCallback(
tab: ITab<IObjectViewerTabState>,
selector: (page: ObjectPage) => ObjectPageCallback | undefined
) {
for (const page of this.pages.values()) {
const callback = selector(page);
if (callback) {
await callback.call(page, tab);
}
}
}
private comparePages(pageA: ObjectPage, pageB: ObjectPage) {
return this.getPageOrder(pageA) - this.getPageOrder(pageB);
}
private getPageOrder(page: ObjectPage) {
// TODO: can be configurable later
return page.order || Number.MAX_SAFE_INTEGER;
}
}
@@ -0,0 +1,32 @@
/*
* 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 { observer } from 'mobx-react';
import { useCallback } from 'react';
import { ITab } from '@dbeaver/core/app';
import { Style } from '@dbeaver/core/theming';
import { IObjectViewerTabState } from '../IObjectViewerTabState';
import { ObjectPage } from './ObjectPage';
export type DBObjectPageTabProps = {
tab: ITab<IObjectViewerTabState>;
page: ObjectPage;
onSelect(tab: ITab<IObjectViewerTabState>, page: ObjectPage): void;
style: Style[];
}
export const DBObjectPageTab = observer(function DBObjectPageTab({
tab, page, onSelect, style,
}: DBObjectPageTabProps) {
const handleSelect = useCallback(() => onSelect(tab, page), [tab, page, onSelect]);
const TabComponent = page.getTabComponent();
return <TabComponent tab={tab} page={page} onSelect={handleSelect} style={style}/>;
});
@@ -0,0 +1,67 @@
/*
* 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 { observable } from 'mobx';
import { ITab } from '@dbeaver/core/app';
import { Style } from '@dbeaver/core/theming';
import { IObjectViewerTabState } from '../IObjectViewerTabState';
export type ObjectPageTabProps = {
tab: ITab<IObjectViewerTabState>;
page: ObjectPage;
onSelect(): void;
style: Style[];
}
export type ObjectPageTabComponent = React.FunctionComponent<ObjectPageTabProps>
export type ObjectPagePanelProps = {
tab: ITab<IObjectViewerTabState>;
page: ObjectPage;
}
export type ObjectPagePanelComponent = React.FunctionComponent<ObjectPagePanelProps>
export type ObjectPageCallback = (tab: ITab<IObjectViewerTabState>) => Promise<void> | void
export type ObjectPageRestoreCallback = (tab: ITab<IObjectViewerTabState>) => Promise<boolean> | boolean
export interface ObjectPageOptions {
key: string;
navigatorId: string;
priority: number;
order?: number;
getTabComponent(): ObjectPageTabComponent;
getPanelComponent(): ObjectPagePanelComponent;
onSelect?: ObjectPageCallback;
onClose?: ObjectPageCallback;
onRestore?: ObjectPageRestoreCallback;
}
export class ObjectPage {
key: string;
navigatorId: string;
priority: number;
@observable order?: number;
getTabComponent: () => ObjectPageTabComponent;
getPanelComponent: () => ObjectPagePanelComponent;
onSelect?: ObjectPageCallback;
onClose?: ObjectPageCallback;
onRestore?: ObjectPageRestoreCallback;
constructor(options: ObjectPageOptions) {
this.key = options.key;
this.navigatorId = options.navigatorId;
this.priority = options.priority;
this.order = options.order;
this.getTabComponent = options.getTabComponent;
this.getPanelComponent = options.getPanelComponent;
this.onSelect = options.onSelect;
this.onClose = options.onClose;
this.onRestore = options.onRestore;
}
}
@@ -6,12 +6,15 @@
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react';
import styled, { css } from 'reshadow';
import { ITab } from '@dbeaver/core/app';
import { VerticalTabs } from '@dbeaver/core/blocks';
import { useController } from '@dbeaver/core/src/di/useController';
import { useController } from '@dbeaver/core/di';
import { useStyles } from '@dbeaver/core/theming';
import { IObjectViewerTabState } from '../IObjectViewerTabState';
import { ObjectFoldersController } from './ObjectFoldersController';
const styles = css`
@@ -23,15 +26,15 @@ const styles = css`
`;
type ObjectFoldersProps = {
objectId: string;
tab: ITab<IObjectViewerTabState>;
}
export function ObjectFolders({ objectId }: ObjectFoldersProps) {
const controller = useController(ObjectFoldersController, objectId);
export const ObjectFolders = observer(function ObjectFolders({ tab }: ObjectFoldersProps) {
const controller = useController(ObjectFoldersController, tab);
return styled(useStyles(styles))(
<folders as="div">
<VerticalTabs tabContainer={controller.getTabContainer()}/>
</folders>
);
}
});
@@ -9,11 +9,10 @@
import { autorun } from 'mobx';
import { IReactionDisposer } from 'mobx/lib/internal';
import {
NavigationTabsService, Tab,
} from '@dbeaver/core/app';
import { ITab } from '@dbeaver/core/app';
import { IDestructibleController, IInitializableController, injectable } from '@dbeaver/core/di';
import { IObjectViewerTabState } from '../IObjectViewerTabState';
import { ObjectFoldersService } from './ObjectFoldersService';
import { ObjectFoldersTabContainer } from './ObjectFoldersTabsContainer/ObjectFoldersTabContainer';
@@ -22,39 +21,29 @@ import { ObjectFoldersTabContainer } from './ObjectFoldersTabsContainer/ObjectFo
export class ObjectFoldersController implements IInitializableController, IDestructibleController {
private tabContainer!: ObjectFoldersTabContainer;
private objectId!: string;
private navigationTab!: Tab
private navigationTab!: ITab<IObjectViewerTabState>
private disposer!: IReactionDisposer;
constructor(private navigationTabsService: NavigationTabsService,
private objectFoldersService: ObjectFoldersService) {
}
constructor(private objectFoldersService: ObjectFoldersService) { }
getTabContainer() {
return this.tabContainer;
}
init(objectId: string): void {
const navigationTab = this.navigationTabsService.getTab(objectId);
if (!navigationTab) {
throw new Error(`Tab ${objectId} not found`);
}
this.navigationTab = navigationTab;
init(tab: ITab<IObjectViewerTabState>): void {
this.navigationTab = tab;
this.objectId = objectId;
this.tabContainer = this.objectFoldersService.createTabsContainer(objectId);
this.tabContainer = this.objectFoldersService.createTabsContainer(tab.handlerState.objectId);
this.disposer = autorun(() => {
const currentTabId = this.navigationTab.getHandlerState<string>(this.navigationTab.handlerId) || null;
this.activateTab(currentTabId);
const currentFolderId = tab.handlerState.folderId || null;
this.activateTab(currentFolderId);
});
}
activateTab(tabId: string | null) {
activateTab(folderId: string | null) {
try {
this.tabContainer.activateTab(tabId);
this.tabContainer.activateTab(folderId);
} catch {
// no tab with tabId
this.activateFirstTab();
@@ -68,6 +57,6 @@ export class ObjectFoldersController implements IInitializableController, IDestr
destruct(): void {
this.disposer();
this.objectFoldersService.destroyTabContainer(this.objectId);
this.objectFoldersService.destroyTabContainer(this.navigationTab.handlerState.objectId);
}
}
@@ -7,7 +7,7 @@
*/
import { TabEntity } from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/src/di';
import { injectable } from '@dbeaver/core/di';
import { ObjectInfoTabModel } from './ObjectInfoTabModel';
@@ -14,7 +14,7 @@ import {
ObjectPropertyInput,
ObjectPropertyProps,
} from '../formControls';
import { filterProperty, matchType } from '../tools';
import { filterProperty, matchType } from '../helpers';
const propertyStyles = css`
property {
@@ -13,6 +13,7 @@ import { useChildren } from '@dbeaver/core/app';
import { Loader } from '@dbeaver/core/blocks';
import { useStyles, composes } from '@dbeaver/core/theming';
import { ObjectPagePanelProps } from '../ObjectPage/ObjectPage';
import { ObjectFolders } from './ObjectFolders';
const viewerStyles = composes(
@@ -31,14 +32,10 @@ const viewerStyles = composes(
`
);
type ObjectViewerProps = {
objectId: string;
}
export const ObjectViewer = observer(function ObjectViewer({
objectId,
}: ObjectViewerProps) {
const children = useChildren(objectId);
export const ObjectPropertiesPagePanel = observer(function ObjectPropertiesPagePanel({
tab,
}: ObjectPagePanelProps) {
const children = useChildren(tab.handlerState.objectId);
if (!children?.isLoaded) {
return <Loader />;
@@ -46,7 +43,7 @@ export const ObjectViewer = observer(function ObjectViewer({
return styled(useStyles(viewerStyles))(
<wrapper as="div">
<ObjectFolders objectId={objectId} />
<ObjectFolders tab={tab} />
</wrapper>
);
});
@@ -0,0 +1,70 @@
/*
* 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 {
NodesManagerService, ITab, IContextProvider, INodeNavigationData
} from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { IObjectViewerTabState } from '../IObjectViewerTabState';
import { DBObjectPageService } from '../ObjectPage/DBObjectPageService';
import { ObjectPage } from '../ObjectPage/ObjectPage';
import { ObjectViewerTabService } from '../ObjectViewerTabService';
import { ObjectPropertiesPagePanel } from './ObjectPropertiesPagePanel';
import { ObjectPropertiesPageTab } from './ObjectPropertiesPageTab';
@injectable()
export class ObjectPropertiesPageService {
page?: ObjectPage;
constructor(
private nodesManagerService: NodesManagerService,
private notificationService: NotificationService,
private objectViewerTabService: ObjectViewerTabService,
private dbObjectPageService: DBObjectPageService
) {
}
registerDBObjectPage() {
this.page = this.dbObjectPageService.register({
key: 'properties',
navigatorId: 'database',
priority: 1,
order: 1,
getTabComponent: () => ObjectPropertiesPageTab,
getPanelComponent: () => ObjectPropertiesPagePanel,
onSelect: this.handlePageSelect.bind(this),
});
this.nodesManagerService.navigator.addHandler(this.navigationHandler.bind(this));
}
private async handlePageSelect(tab: ITab<IObjectViewerTabState>) {
// TODO: must be loaded by info folder?
// await this.nodesManagerService.loadDatabaseObjectInfo(tab.handlerState.objectId);
// await this.nodesManagerService.loadNodeInfo(tab.handlerState.objectId);
}
private async navigationHandler(contexts: IContextProvider<INodeNavigationData>) {
if (!this.page) { // TODO: it will be never true, because navHandler registers after page creation
return;
}
try {
const tabContext = await contexts.getContext(this.objectViewerTabService.objectViewerTabContext);
if (tabContext.tab && !tabContext.tab.handlerState.folderId) {
tabContext.tab.handlerState.folderId = 'infoTab';
}
tabContext.trySwitchPage(this.page);
} catch (exception) {
this.notificationService.logException(exception, 'Error in Object Viewer while processing action with database node');
}
}
}
@@ -0,0 +1,27 @@
/*
* 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 { observer } from 'mobx-react';
import styled from 'reshadow';
import { TabIcon, Tab, TabTitle } from '@dbeaver/core/blocks';
import { useStyles } from '@dbeaver/core/theming';
import { ObjectPageTabProps } from '../ObjectPage/ObjectPage';
export const ObjectPropertiesPageTab = observer(function ObjectPropertiesPageTab({
page, onSelect, style,
}: ObjectPageTabProps) {
return styled(useStyles(...style))(
<Tab tabId={page.key} onOpen={onSelect} >
<TabIcon icon='/icons/properties.png' />
<TabTitle title='Properties' />
</Tab>
);
});
@@ -15,7 +15,7 @@ import { StaticImage } from '@dbeaver/core/blocks';
import { useService } from '@dbeaver/core/di';
import { useStyles } from '@dbeaver/core/theming';
import { getValue } from '../tools';
import { getValue } from '../helpers';
const itemStyles = css`
icon {
@@ -12,7 +12,7 @@ import styled, { css } from 'reshadow';
import { useChildren } from '@dbeaver/core/app';
import { Loader } from '@dbeaver/core/blocks';
import { useObjectFolder } from '../useObjectFolder';
import { useObjectFolder } from '../../useObjectFolder';
import { ObjectChildrenPropertyTable } from './ObjectChildrenPropertyTable';
const styles = css`
@@ -11,7 +11,7 @@ import styled, { css } from 'reshadow';
import { useStyles } from '@dbeaver/core/theming';
import { additionalProps, getValue, matchType } from '../tools';
import { additionalProps, getValue, matchType } from '../helpers';
import { ObjectPropertyProps } from './ObjectPropertyProps';
const styles = css`
@@ -11,7 +11,7 @@ import styled, { css } from 'reshadow';
import { useStyles } from '@dbeaver/core/theming';
import { additionalProps, getValue, matchType } from '../tools';
import { additionalProps, getValue, matchType } from '../helpers';
import { ObjectPropertyProps } from './ObjectPropertyProps';
const styles = css`
@@ -37,9 +37,5 @@ export function additionalProps(property: ObjectPropertyInfo) {
}
export function filterProperty(property: ObjectPropertyInfo) {
if (property.value === null) {
return false;
}
return true;
}
@@ -8,14 +8,19 @@
import { injectable } from '@dbeaver/core/di';
import { ObjectPropertiesPageService } from './ObjectPropertiesPage/ObjectPropertiesPageService';
import { ObjectViewerTabService } from './ObjectViewerTabService';
@injectable()
export class ObjectViewerBootstrap {
constructor(private objectViewerTabService: ObjectViewerTabService) { }
constructor(
private objectViewerTabService: ObjectViewerTabService,
private objectPropertiesPageService: ObjectPropertiesPageService
) { }
bootstrap() {
this.objectViewerTabService.registerTabHandler();
this.objectPropertiesPageService.registerDBObjectPage();
}
}
@@ -0,0 +1,71 @@
/*
* 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 { observer } from 'mobx-react';
import styled, { css } from 'reshadow';
import { useChildren, TabHandlerPanelProps } from '@dbeaver/core/app';
import { Loader, TabsBox, TabPanel } from '@dbeaver/core/blocks';
import { useService } from '@dbeaver/core/di';
import { useStyles, composes } from '@dbeaver/core/theming';
import { IObjectViewerTabState } from './IObjectViewerTabState';
import { DBObjectPagePanel } from './ObjectPage/DBObjectPagePanel';
import { DBObjectPageService } from './ObjectPage/DBObjectPageService';
import { DBObjectPageTab } from './ObjectPage/DBObjectPageTab';
const styles = composes(
css`
Tab {
composes: theme-ripple theme-background-surface theme-text-text-primary-on-light from global;
}
tabs {
composes: theme-background-background theme-text-text-primary-on-light from global;
}
`,
css`
Tab:only-child {
display: none;
}
`,
);
const stylesArray = [styles];
export const ObjectViewerPanel = observer(function ObjectViewerPanel({
tab, handler,
}: TabHandlerPanelProps<IObjectViewerTabState>) {
const children = useChildren(tab.handlerState.objectId);
const dbObjectPagesService = useService(DBObjectPageService);
const pages = dbObjectPagesService.orderedPages;
if (!children?.isLoaded) {
return <Loader />;
}
return styled(useStyles(styles))(
<TabsBox
currentTabId={tab.handlerState.pageId}
tabs={pages.map(page => (
<DBObjectPageTab
key={page.key}
tab={tab}
page={page}
onSelect={dbObjectPagesService.selectPage}
style={stylesArray}
/>
))}
style={stylesArray}
>
{pages.map(page => (
<TabPanel key={page.key} tabId={page.key}>
<DBObjectPagePanel tab={tab} page={page} />
</TabPanel>
))}
</TabsBox>
);
});
@@ -1,86 +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 { observable } from 'mobx';
import {
NodesManagerService,
NavigationTabsService,
} from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { objectViewerTabHandlerKey } from './objectViewerTabHandlerKey';
@injectable()
export class ObjectViewerService {
@observable private propertiesTabLoadingState: Map<string, boolean> = new Map();
constructor(private nodesManagerService: NodesManagerService,
private notificationService: NotificationService,
private navigationTabsService: NavigationTabsService) {
}
isTabLoading(id: string): boolean {
return !this.propertiesTabLoadingState.get(id);
}
async selectObjectTab(nodeId: string, handlerId: string) {
if (handlerId !== objectViewerTabHandlerKey) {
return;
}
try {
const tab = this.navigationTabsService.getTab(nodeId);
if (tab) {
await this.nodesManagerService.loadDatabaseObjectInfo(nodeId);
const children = await this.nodesManagerService.loadChildren(nodeId);
let childrenId = tab.getHandlerState<string>(handlerId)!;
const propertiesTabLoadingKey = `${nodeId}_${childrenId}`;
if (childrenId === '') {
childrenId = 'infoTab';
tab.updateHandlerState({
handlerId: objectViewerTabHandlerKey,
state: childrenId,
});
}
if (children.length === 0) {
this.propertiesTabLoadingState.set(propertiesTabLoadingKey, true);
return;
}
this.propertiesTabLoadingState.set(propertiesTabLoadingKey, false);
try {
// todo temporary dirty hack before navigation system refactoring
if (childrenId !== 'infoTab' && childrenId !== 'ddl-viewer') {
await this.nodesManagerService.loadChildrenDatabaseObjectInfo(childrenId);
}
} finally {
this.propertiesTabLoadingState.set(propertiesTabLoadingKey, true);
}
}
} catch (exception) {
this.notificationService.logException(exception, 'Error in Object Viewer while tab selecting');
}
}
async restoreObjectTab(tabId: string, handlerId: string) {
const tab = this.navigationTabsService.getTab(tabId);
if (tab && tab.hasHandler(objectViewerTabHandlerKey)) {
const node = await this.nodesManagerService.loadNodeInfo(tab.nodeId);
if (node) {
tab.icon = node.icon;
tab.name = node.name;
return true;
}
}
return false;
}
}
@@ -0,0 +1,29 @@
/*
* 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 { observer } from 'mobx-react';
import styled from 'reshadow';
import { TabHandlerTabProps, useNode } from '@dbeaver/core/app';
import { TabIcon, Tab, TabTitle } from '@dbeaver/core/blocks';
import { useStyles } from '@dbeaver/core/theming';
import { IObjectViewerTabState } from './IObjectViewerTabState';
export const ObjectViewerTab = observer(function ObjectViewerTab({
tab, handler, onSelect, onClose, style,
}: TabHandlerTabProps<IObjectViewerTabState>) {
const node = useNode(tab.handlerState.objectId);
return styled(useStyles(...style))(
<Tab tabId={tab.id} onOpen={onSelect} onClose={onClose} >
<TabIcon icon={node?.icon || tab.icon} />
<TabTitle title={node?.name || tab.name} />
</Tab>
);
});
@@ -6,84 +6,252 @@
* you may not use this file except in compliance with the License.
*/
import { observable } from 'mobx';
import {
NodesManagerService,
NavigationTabsService,
TabHandlerOptions,
INodeNavigationData,
IContextProvider,
ITab,
TabHandler,
isDatabaseObject,
NavigationType,
NodeManagerUtils,
connectionProvider,
objectCatalogProvider,
objectSchemaProvider,
} from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { ObjectViewerService } from './ObjectViewerService';
import { IObjectViewerTabContext } from './IObjectViewerTabContext';
import { IObjectViewerTabState } from './IObjectViewerTabState';
import { DBObjectPageService } from './ObjectPage/DBObjectPageService';
import { ObjectPage } from './ObjectPage/ObjectPage';
import { ObjectViewerPanel } from './ObjectViewerPanel';
import { ObjectViewerTab } from './ObjectViewerTab';
import { objectViewerTabHandlerKey } from './objectViewerTabHandlerKey';
import { Viewer } from './Viewer';
@injectable()
export class ObjectViewerTabService {
readonly tabHandler!: TabHandlerOptions
@observable private propertiesTabLoadingState: Map<string, boolean> = new Map();
readonly tabHandler: TabHandler<IObjectViewerTabState>
constructor(private nodesManagerService: NodesManagerService,
private objectViewerService: ObjectViewerService,
private dbObjectPageService: DBObjectPageService,
private notificationService: NotificationService,
private navigationTabsService: NavigationTabsService) {
this.tabHandler = {
key: objectViewerTabHandlerKey,
name: 'Properties',
icon: '/icons/properties.png',
navigatorId: 'database',
order: 1,
priority: 1,
getTabHandlerComponent: () => Viewer,
isActive: id => /^database:\/\//.test(id),
onRestore: this.objectViewerService.restoreObjectTab.bind(this.objectViewerService),
onSelect: this.objectViewerService.selectObjectTab.bind(this.objectViewerService),
};
this.tabHandler = this.navigationTabsService
.registerTabHandler<IObjectViewerTabState>({
key: objectViewerTabHandlerKey,
navigatorId: 'database',
priority: 1,
getTabComponent: () => ObjectViewerTab,
getPanelComponent: () => ObjectViewerPanel,
onRestore: this.restoreObjectTab.bind(this),
onSelect: this.selectObjectTab.bind(this),
onClose: this.closeObjectTab.bind(this),
extensions: [
connectionProvider(this.getConnection.bind(this)),
objectCatalogProvider(this.getDBObjectCatalog.bind(this)),
objectSchemaProvider(this.getDBObjectSchema.bind(this)),
],
});
}
getConnection(context: ITab<IObjectViewerTabState>) {
const nodeInfo = this.nodesManagerService
.getNodeContainerInfo(context.handlerState.objectId);
if (!nodeInfo.connectionId) {
return;
}
// connection node id differs from connection id
return NodeManagerUtils.connectionNodeIdToConnectionId(nodeInfo.connectionId);
}
getDBObjectCatalog(context: ITab<IObjectViewerTabState>) {
const nodeInfo = this.nodesManagerService
.getNodeContainerInfo(context.handlerState.objectId);
if (nodeInfo.catalogId) {
return;
}
return nodeInfo.catalogId;
}
getDBObjectSchema(context: ITab<IObjectViewerTabState>) {
const nodeInfo = this.nodesManagerService
.getNodeContainerInfo(context.handlerState.objectId);
if (!nodeInfo.schemaId) {
return;
}
return nodeInfo.schemaId;
}
registerTabHandler() {
this.navigationTabsService.registerTabHandler(this.tabHandler);
this.nodesManagerService.navigator.addHandler(this.navigationHandler.bind(this));
}
isTabLoading(id: string): boolean {
return !this.propertiesTabLoadingState.get(id);
}
async selectObjectTab(tab: ITab<IObjectViewerTabState>) {
try {
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.nodesManagerService.loadDatabaseObjectInfo(tab.handlerState.objectId);
await this.nodesManagerService.loadNodeInfo(tab.handlerState.objectId);
const children = await this.nodesManagerService.loadChildren(tab.handlerState.objectId);
const folderId = tab.handlerState.folderId;
const propertiesTabLoadingKey = `${tab.handlerState.objectId}_${folderId}`;
try {
if (children.length === 0 || !isDatabaseObject(folderId)) {
return;
}
this.propertiesTabLoadingState.set(propertiesTabLoadingKey, false);
await this.nodesManagerService.loadChildrenDatabaseObjectInfo(folderId);
} finally {
this.propertiesTabLoadingState.set(propertiesTabLoadingKey, true);
}
} catch (exception) {
this.notificationService.logException(exception, 'Error in Object Viewer while tab selecting');
}
}
async restoreObjectTab(tab: ITab<IObjectViewerTabState>) {
if (typeof tab.handlerState?.folderId === 'string' && typeof tab.handlerState?.objectId === 'string') {
const node = await this.nodesManagerService.loadNodeInfo(tab.handlerState.objectId);
if (node) {
tab.icon = node.icon;
tab.name = node.name;
return this.dbObjectPageService.restorePages(tab);
}
}
return false;
}
async closeObjectTab(tab: ITab<IObjectViewerTabState>) {
await this.dbObjectPageService.closePages(tab);
}
objectViewerTabContext = async (
contexts: IContextProvider<INodeNavigationData>,
data: INodeNavigationData
): Promise<IObjectViewerTabContext> => {
const tabInfo = await contexts.getContext(this.navigationTabsService.navigationTabContext);
const nodeInfo = await contexts.getContext(this.nodesManagerService.navigationNodeContext);
if (data.type !== NavigationType.closeConnection) {
// check if tab already exist for object
const tab = this.navigationTabsService.findTab(
isObjectViewerTab(tab => tab.handlerState.objectId === nodeInfo.nodeId)
);
if (tab) {
tab.name = nodeInfo.name;
tab.icon = nodeInfo.icon;
tabInfo.registerTab(tab);
} else {
tabInfo.openNewTab<IObjectViewerTabState>({
handlerId: objectViewerTabHandlerKey,
handlerState: {
objectId: nodeInfo.nodeId,
folderId: nodeInfo.folderId,
pageId: '',
pagesState: new Map(),
},
name: nodeInfo.name,
icon: nodeInfo.icon,
});
}
}
const getPage = () => {
if (!tabInfo.tab) {
return;
}
const pageId = (tabInfo.tab?.handlerState as IObjectViewerTabState | undefined)?.pageId;
if (!pageId) {
return;
}
return this.dbObjectPageService.getPage(pageId);
};
const trySwitchPage = (page: ObjectPage) => {
if (!tabInfo.tab) {
return false;
}
return this.dbObjectPageService.trySwitchPage(tabInfo.tab, page);
};
return {
get tab() {
return tabInfo.tab;
},
get page() {
return getPage();
},
trySwitchPage,
tabInfo,
nodeInfo,
};
}
private async navigationHandler(contexts: IContextProvider<INodeNavigationData>) {
try {
const tabInfo = await contexts.getContext(this.navigationTabsService.navigationTabContext);
const nodeInfo = await contexts.getContext(this.nodesManagerService.navigationNodeContext);
const tab = this.navigationTabsService.getTab(nodeInfo.nodeId);
const { tab, tabInfo, nodeInfo } = await contexts.getContext(this.objectViewerTabContext);
if (tab) {
const state = tab.getHandlerState(objectViewerTabHandlerKey);
if (!state || nodeInfo.childrenId !== state) {
tab.updateHandlerState({
handlerId: objectViewerTabHandlerKey,
state: nodeInfo.childrenId,
});
if (nodeInfo.type === NavigationType.closeConnection) {
for (const tab of this.navigationTabsService.findTabs(
isObjectViewerTab(tab => tab.handlerState.objectId.includes(nodeInfo.nodeId))
)) {
await this.navigationTabsService.closeTab(tab.id);
}
} else {
tabInfo.openNewTab({
nodeId: nodeInfo.nodeId,
handlerId: objectViewerTabHandlerKey,
handlerState: new Map([[
objectViewerTabHandlerKey,
{
handlerId: objectViewerTabHandlerKey,
state: nodeInfo.childrenId,
},
]]),
name: nodeInfo.name,
icon: nodeInfo.icon,
});
tabInfo.trySwitchHandler(this.tabHandler);
return;
}
this.navigationTabsService.selectTab(nodeInfo.nodeId);
tabInfo.trySwitchHandler(this.tabHandler);
if (tab) {
if (!tab.handlerState.folderId || (nodeInfo.folderId && tab.handlerState.folderId !== nodeInfo.folderId)) {
tab.handlerState.folderId = nodeInfo.folderId;
}
this.navigationTabsService.selectTab(tab.id);
}
} catch (exception) {
this.notificationService.logException(exception, 'Error in Object Viewer while processing action with database node');
}
}
}
export function isObjectViewerTab(tab: ITab): tab is ITab<IObjectViewerTabState>;
export function isObjectViewerTab(
predicate: (tab: ITab<IObjectViewerTabState>) => boolean
): (tab: ITab) => tab is ITab<IObjectViewerTabState>
export function isObjectViewerTab(
tab: ITab | ((tab: ITab<IObjectViewerTabState>) => boolean)
): boolean | ((tab: ITab) => tab is ITab<IObjectViewerTabState>) {
if (typeof tab === 'function') {
const predicate = tab;
return (tab: ITab): tab is ITab<IObjectViewerTabState> => {
const objectViewerTab = tab.handlerId === objectViewerTabHandlerKey;
if (!predicate || !objectViewerTab) {
return objectViewerTab;
}
return predicate(tab);
};
}
return tab.handlerId === objectViewerTabHandlerKey;
}
@@ -1,20 +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 { PropsWithChildren } from 'react';
import { ObjectViewer } from './ObjectViewer';
type ViewerProps = PropsWithChildren<{
tabId: string;
handlerId: string;
}>
export function Viewer({ tabId }: ViewerProps) {
return <ObjectViewer objectId={tabId} />;
}
@@ -3,5 +3,8 @@ import { manifest } from './manifest';
export default manifest;
// All Services and Components that is provided by this plugin should be exported here
export * from './ObjectViewerService';
export * from './ObjectFoldersService';
export * from './ObjectPage/DBObjectPageService';
export * from './ObjectPage/ObjectPage';
export * from './ObjectPropertiesPage/ObjectFoldersService';
export * from './ObjectViewerTabService';
export * from './IObjectViewerTabState';
@@ -8,24 +8,26 @@
import { IServiceInjector, PluginManifest } from '@dbeaver/core/di';
import { ObjectFoldersService } from './ObjectFoldersService';
import { ObjectFoldersTabService } from './ObjectFoldersTab/ObjectFoldersTabService';
import { ObjectInfoTabService } from './ObjectInfoTab/ObjectInfoTabService';
import { DBObjectPageService } from './ObjectPage/DBObjectPageService';
import { ObjectFoldersService } from './ObjectPropertiesPage/ObjectFoldersService';
import { ObjectFoldersTabService } from './ObjectPropertiesPage/ObjectFoldersTab/ObjectFoldersTabService';
import { ObjectInfoTabService } from './ObjectPropertiesPage/ObjectInfoTab/ObjectInfoTabService';
import { ObjectPropertiesPageService } from './ObjectPropertiesPage/ObjectPropertiesPageService';
import { VirtualFolderTabService } from './ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabService';
import { ObjectViewerBootstrap } from './ObjectViewerBootstrap';
import { ObjectViewerService } from './ObjectViewerService';
import { ObjectViewerTabService } from './ObjectViewerTabService';
import { VirtualFolderTabService } from './VirtualFolderTab/VirtualFolderTabService';
export const manifest: PluginManifest = {
info: { name: 'Object Viewer Plugin' },
providers: [
ObjectViewerService,
ObjectPropertiesPageService,
ObjectFoldersService,
ObjectViewerTabService,
ObjectInfoTabService,
ObjectFoldersTabService,
VirtualFolderTabService,
DBObjectPageService,
],
async initialize(injector: IServiceInjector) {
@@ -8,12 +8,12 @@
import { useService } from '@dbeaver/core/di';
import { ObjectViewerService } from './ObjectViewerService';
import { ObjectViewerTabService } from './ObjectViewerTabService';
export function useObjectFolder(objectId: string, folderId: string) {
const objectViewerService = useService(ObjectViewerService);
const objectViewerTabService = useService(ObjectViewerTabService);
const isLoading = objectViewerService.isTabLoading(`${objectId}_${folderId}`);
const isLoading = objectViewerTabService.isTabLoading(`${objectId}_${folderId}`);
return { isLoading };
}
@@ -6,13 +6,15 @@
* you may not use this file except in compliance with the License.
*/
import { IConnectionCatalogSchema, ITabHasConnectionChangeBehavior } from '@dbeaver/core/app';
import { SqlExecutionState } from './SqlExecutionState';
import { SQLQueryExecutionProcess } from './SqlResultTabs/SQLQueryExecutionProcess';
export interface ISqlContextParams extends IConnectionCatalogSchema {
export interface ISqlContextParams {
contextId: string;
connectionId: string;
objectCatalogId?: string;
objectSchemaId?: string;
}
export interface ISqlQueryParams extends ISqlContextParams {
@@ -38,10 +40,9 @@ export interface IResultsTabState {
panelParams: ISqlResultPanelParams;
}
export interface ISqlEditorTabState extends ITabHasConnectionChangeBehavior {
export interface ISqlEditorTabState extends ISqlContextParams {
order: number;
query: string;
contextId: string;
currentResultTabId?: string;
sqlExecutionState: SqlExecutionState;
resultTabs: IResultsTabState[];
@@ -12,14 +12,13 @@ import CodeMirror, {
import { computed } from 'mobx';
import { IControlledCodeMirror } from 'react-codemirror2';
import { Tab } from '@dbeaver/core/app';
import { ITab } from '@dbeaver/core/app';
import { IInitializableController, injectable } from '@dbeaver/core/di';
import { SqlDialectInfo } from '@dbeaver/core/sdk';
import { ISqlEditorTabState } from '../ISqlEditorTabState';
import { SqlDialectInfoService } from '../SqlDialectInfoService';
import { SqlEditorManagerService } from '../SqlEditorManagerService';
import { sqlEditorTabHandlerKey } from '../sqlEditorTabHandlerKey';
import { SqlResultTabsService } from '../SqlResultTabs/SqlResultTabsService';
import { SqlEditorService } from './SqlEditorService';
// allows to bypass rollup-commonjs error " 'showHint' is not exported by codemirror.js "
@@ -29,24 +28,19 @@ const showHint = CodeMirror.showHint;
export class SqlEditorController implements IInitializableController {
@computed get dialect(): SqlDialectInfo | undefined {
const state = this.tab.getHandlerState<ISqlEditorTabState>(sqlEditorTabHandlerKey);
if (!state) {
return;
}
return this.sqlDialectInfoService.getDialectInfo(state.connectionId);
return this.sqlDialectInfoService.getDialectInfo(this.tab.handlerState.connectionId);
}
@computed get isActionsDisabled(): boolean {
const state = this.tab.getHandlerState<ISqlEditorTabState>(sqlEditorTabHandlerKey)!;
return state.sqlExecutionState.isSqlExecuting;
return this.tab.handlerState.sqlExecutionState.isSqlExecuting;
}
handleExecute = () => {
this.sqlEditorManager.executeEditorQuery(this.editorId, this.getExecutingQuery());
this.sqlResultTabsService.executeEditorQuery(this.tab.handlerState, this.getExecutingQuery(), false);
}
handleExecuteNewTab = () => {
this.sqlEditorManager.executeEditorQuery(this.editorId, this.getExecutingQuery(), true);
this.sqlResultTabsService.executeEditorQuery(this.tab.handlerState, this.getExecutingQuery(), true);
}
readonly options: EditorConfiguration = {
@@ -84,28 +78,24 @@ export class SqlEditorController implements IInitializableController {
}
@computed get value() {
const state = this.tab.getHandlerState<ISqlEditorTabState>(sqlEditorTabHandlerKey)!;
return state.query;
return this.tab.handlerState.query;
}
private tab!: Tab;
private editorId!: string;
private tab!: ITab<ISqlEditorTabState>;
private editor?: Editor;
constructor(private sqlEditorManager: SqlEditorManagerService,
constructor(private sqlResultTabsService: SqlResultTabsService,
private sqlDialectInfoService: SqlDialectInfoService,
private sqlEditorService: SqlEditorService) {
}
init(tab: Tab) {
init(tab: ITab<ISqlEditorTabState>) {
this.tab = tab;
this.editorId = tab.nodeId;
}
private getExecutingQuery(): string {
if (!this.editor) {
const state = this.tab.getHandlerState<ISqlEditorTabState>(sqlEditorTabHandlerKey)!;
return state.query;
return this.tab.handlerState.query;
}
if (this.editor.somethingSelected()) {
@@ -140,7 +130,12 @@ export class SqlEditorController implements IInitializableController {
const [from, to] = getWordRange(editor, cursor);
this.sqlEditorService
.getAutocomplete(this.editorId, cursorPosition)
.getAutocomplete(
this.tab.handlerState.connectionId,
this.tab.handlerState.contextId,
this.tab.handlerState.query,
cursorPosition
)
.then((proposals) => {
if (!proposals) {
return;
@@ -196,8 +191,7 @@ export class SqlEditorController implements IInitializableController {
}
private handleQueryChange(editor: Editor, data: EditorChange, query: string) {
const state = this.tab.getHandlerState<ISqlEditorTabState>(sqlEditorTabHandlerKey)!;
state.query = query;
this.tab.handlerState.query = query;
}
}
@@ -9,25 +9,23 @@
import { injectable } from '@dbeaver/core/di';
import { GraphQLService, QuerySqlCompletionProposalsQuery } from '@dbeaver/core/sdk';
import { SqlEditorManagerService } from '../SqlEditorManagerService';
@injectable()
export class SqlEditorService {
constructor(private sqlEditorManagerService: SqlEditorManagerService,
private gql: GraphQLService) {
constructor(private gql: GraphQLService) {
}
async getAutocomplete(tabId: string, cursor: number): Promise<QuerySqlCompletionProposalsQuery['sqlCompletionProposals'] | null> {
const state = this.sqlEditorManagerService.getHandlerState(tabId);
if (!state) {
return null;
}
async getAutocomplete(
connectionId: string,
contextId: string,
query: string,
cursor: number
): Promise<QuerySqlCompletionProposalsQuery['sqlCompletionProposals'] | null> {
const result = await this.gql.gql.querySqlCompletionProposals({
connectionId: state.connectionId,
contextId: state.contextId,
query: state.query,
connectionId,
contextId,
query,
position: cursor,
});
@@ -16,7 +16,7 @@ import {
import { injectable } from '@dbeaver/core/di';
import { ContextMenuService, IContextMenuItem, IMenuContext } from '@dbeaver/core/dialogs';
import { SqlEditorManagerService } from './SqlEditorManagerService';
import { SqlEditorNavigatorService } from './SqlEditorNavigatorService';
import { SqlEditorTabService } from './SqlEditorTabService';
@injectable()
@@ -25,7 +25,7 @@ export class SqlEditorBootstrap {
private contextMenuService: ContextMenuService,
private connectionsManagerService: ConnectionsManagerService,
private sqlEditorTabService: SqlEditorTabService,
private sqlEditorManagerService: SqlEditorManagerService) {}
private sqlEditorNavigatorService: SqlEditorNavigatorService) {}
async bootstrap() {
this.sqlEditorTabService.registerTabHandler();
@@ -35,7 +35,7 @@ export class SqlEditorBootstrap {
id: 'sql-editor',
title: 'SQL',
order: 2,
onClick: () => this.sqlEditorManagerService.openNewEditor(),
onClick: () => this.sqlEditorNavigatorService.openNewEditor(),
isDisabled: () => !this.connectionsManagerService.hasAnyConnection(),
}
);
@@ -51,7 +51,7 @@ export class SqlEditorBootstrap {
onClick: (context: IMenuContext<NodeWithParent>) => {
const node = context.data;
const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(node.id);
this.sqlEditorManagerService.openNewEditor(connectionId);
this.sqlEditorNavigatorService.openNewEditor(connectionId);
},
};
this.contextMenuService.addMenuItem<NodeWithParent>(this.contextMenuService.getRootMenuToken(), openSqlEditor);
@@ -1,330 +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 {
ConnectionsManagerService,
IConnectionCatalogSchema,
IContextProvider,
INavigator,
NavigationService,
NavigationTabsService,
Tab,
} from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { GraphQLService } from '@dbeaver/core/sdk';
import { uuid } from '@dbeaver/core/utils';
import { ISqlContextParams, ISqlEditorTabState } from './ISqlEditorTabState';
import { SqlDialectInfoService } from './SqlDialectInfoService';
import { sqlEditorTabHandlerKey } from './sqlEditorTabHandlerKey';
import { SqlExecutionState } from './SqlExecutionState';
import { SqlResultTabsService } from './SqlResultTabs/SqlResultTabsService';
export interface SqlEditorAction {
createNew?: {
connectionId?: string;
catalogId?: string;
schemaId?: string;
};
open?: {
editorId: string;
resultId: string;
};
close?: {
editorId: string;
resultId: string;
};
}
export const SQL_EDITOR_URL_PATTERN = /^plugin:\/\/sql-editor\//;
@injectable()
export class SqlEditorManagerService {
private readonly navigator!: INavigator<SqlEditorAction>;
constructor(private sqlResultTabsService: SqlResultTabsService,
private navigationTabsService: NavigationTabsService,
private notificationService: NotificationService,
private gql: GraphQLService,
private connectionsManagerService: ConnectionsManagerService,
private sqlDialectInfoService: SqlDialectInfoService,
private navigationService: NavigationService) {
this.navigator = this.navigationService.createNavigator<SqlEditorAction>(
null,
this.navigateHandler.bind(this)
);
}
openNewEditor(connectionId?: string) {
this.navigator.navigateTo({
createNew: {
connectionId,
},
});
}
openEditorResult(editorId: string, resultId: string) {
this.navigator.navigateTo({ open: { editorId, resultId } });
}
closeEditorResult(editorId: string, resultId: string) {
this.navigator.navigateTo({ close: { editorId, resultId } });
}
isSqlEditorEntity = (id: string) => SQL_EDITOR_URL_PATTERN.test(id)
getUrlFromId(id: string) {
return `plugin://sql-editor/${id}`;
}
private async navigateHandler(contexts: IContextProvider<SqlEditorAction>, data: SqlEditorAction) {
try {
if (data.createNew) {
await this.createNewEditor(data.createNew.connectionId);
return;
}
if (data.open) {
const state = this.getHandlerState(data.open.editorId);
if (!state) {
return;
}
state.currentResultTabId = data.open.resultId;
this.navigationTabsService.selectTab(data.open.editorId);
return;
}
if (data.close) {
const state = this.getHandlerState(data.close.editorId);
if (!state) {
return;
}
state.resultTabs.splice(state.resultTabs.findIndex(result => result.resultTabId === data.close!.resultId), 1);
if (state.currentResultTabId === data.close.resultId) {
state.currentResultTabId = state.resultTabs[0]?.resultTabId || '';
}
this.navigationTabsService.selectTab(data.close.editorId);
}
} catch (exception) {
this.notificationService.logException(exception, 'Error in SQL Editor while processing action with editor');
}
}
async executeEditorQuery(editorId: string, query: string, inNewTab = false) {
const state = this.getHandlerState(editorId);
if (!state) {
return;
}
await this.sqlResultTabsService.executeEditorQuery(state, query, inNewTab);
}
async handleTabRestore(tabId: string, handlerId: string): Promise<boolean> {
const match = /^plugin:\/\/sql-editor\/(.*?)(\/|$)/.exec(tabId);
if (match && match.length > 0) {
const state = this.getHandlerState(tabId);
if (!state
|| typeof state.query !== 'string'
|| typeof state.connectionId !== 'string'
|| typeof state.order !== 'number'
|| typeof state.contextId !== 'string'
|| !['string', 'undefined'].includes(typeof state.currentResultTabId)
|| !Array.isArray(state.resultTabs)
) {
return false;
}
// the connection for this editor was closed
if (!this.connectionsManagerService.getConnectionById(state?.connectionId)) {
return false;
}
state.currentResultTabId = '';
state.resultTabs = []; // clean old results
state.sqlExecutionState = new SqlExecutionState();
state.changeSchema = connectionCatalogSchema => this.changeConnectionAndSchema(tabId, connectionCatalogSchema);
await this.sqlDialectInfoService.loadSqlDialectInfo(state.connectionId);
}
return true;
}
async handleTabClose(tabId: string, handlerId: string) {
if (!this.isSqlEditorEntity(tabId)) {
return;
}
const state = this.getHandlerState(tabId);
if (!state) {
return;
}
await this.destroySqlContext(state.connectionId, state.contextId);
}
private async destroySqlContext(connectionId: string, contextId: string): Promise<void> {
const connection = this.connectionsManagerService.getConnectionById(connectionId);
if (!connection) {
// connection was closed before, nothing to destroy
return;
}
try {
await this.gql.gql.sqlContextDestroy({ connectionId, contextId });
} catch (e) {
this.notificationService.logError({ title: `Failed to destroy SQL-context ${contextId}` });
}
}
/**
* 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(connectionCatalogSchema: IConnectionCatalogSchema): Promise<ISqlContextParams> {
const response = await this.gql.gql.sqlContextCreate(connectionCatalogSchema);
return {
contextId: response.context.id,
connectionId: connectionCatalogSchema.connectionId,
catalogId: response.context.defaultCatalog || null,
schemaId: response.context.defaultSchema || null,
};
}
/**
* Update catalog and schema for the exiting sql context in the certain connection
*/
private async updateSqlContext(params: ISqlContextParams): Promise<ISqlContextParams> {
await this.gql.gql.sqlContextSetDefaults({
contextId: params.contextId,
connectionId: params.connectionId,
defaultCatalog: params.catalogId || undefined,
defaultSchema: params.schemaId || undefined,
});
return params;
}
getHandlerState(editorId: string): ISqlEditorTabState | undefined {
return this.navigationTabsService.getHandlerState<ISqlEditorTabState>(editorId, sqlEditorTabHandlerKey);
}
private async createNewEditor(connectionId?: string) {
const order = this.getFreeEditorId();
const connection = connectionId
? this.connectionsManagerService.getConnectionById(connectionId)
: this.connectionsManagerService.connections[0];
if (!connection) {
return;
}
const sqlContextParams = await this.createSqlContext({
connectionId: connection.id,
catalogId: null,
schemaId: null,
});
const tabId = this.getUrlFromId(uuid());
const state: ISqlEditorTabState = {
query: '',
order,
contextId: sqlContextParams.contextId,
connectionId: connection.id,
schemaId: sqlContextParams.schemaId,
catalogId: sqlContextParams.catalogId,
changeSchema: connectionCatalogSchema => this.changeConnectionAndSchema(tabId, connectionCatalogSchema),
sqlExecutionState: new SqlExecutionState(),
resultTabs: [],
};
const newTab = new Tab({
nodeId: tabId,
handlerId: sqlEditorTabHandlerKey,
handlerState: new Map([[
sqlEditorTabHandlerKey,
{
handlerId: sqlEditorTabHandlerKey,
state,
},
]]),
name: this.generateTabName(state), // todo after refactoring it should be computable
icon: '/icons/sql_script.png',
});
newTab.name = this.generateTabName(state);
this.navigationTabsService.openTab(newTab, true);
this.navigationTabsService.selectTab(newTab.nodeId);
await this.sqlDialectInfoService.loadSqlDialectInfo(connection.id);
}
private getFreeEditorId() {
const openedEditors = this.navigationTabsService.tabIdList.filter(this.isSqlEditorEntity);
const ordered = openedEditors.map(tabId => this.getHandlerState(tabId)!.order);
return findMinimalFree(ordered, 1);
}
private async changeConnectionAndSchema(
tabId: string,
connectionCatalogSchema: IConnectionCatalogSchema
): Promise<IConnectionCatalogSchema> {
const state = this.getHandlerState(tabId)!;
const contextParams: ISqlContextParams = {
contextId: state.contextId,
...connectionCatalogSchema,
};
try {
const newContextParams = connectionCatalogSchema.connectionId === state.connectionId
? await this.updateSqlContext(contextParams)
: await this.changeConnection(contextParams, state.connectionId);
state.connectionId = newContextParams.connectionId;
state.schemaId = newContextParams.schemaId;
state.catalogId = newContextParams.catalogId;
state.contextId = newContextParams.contextId;
const tab = this.navigationTabsService.getTab(tabId)!;
tab.name = this.generateTabName(state);
await this.sqlDialectInfoService.loadSqlDialectInfo(newContextParams.connectionId);
return newContextParams;
} catch (e) {
this.notificationService.logError({ title: 'Failed to change SQL-editor schema' });
return connectionCatalogSchema;
}
}
private async changeConnection(params: ISqlContextParams, oldConnectionId: string): Promise<ISqlContextParams> {
// try to create new context first
const newContextParams = await this.createSqlContext(params);
// when new context created - destroy old one silently
await this.destroySqlContext(oldConnectionId, params.contextId);
return newContextParams;
}
private generateTabName(state: ISqlEditorTabState): string {
const connection = this.connectionsManagerService.getConnectionById(state.connectionId);
return `sql-${state.order} (${connection?.name || ''})`;
}
}
function findMinimalFree(array: number[], base: number): number {
return array
.sort((a, b) => b - a)
.reduceRight((prev, cur) => (prev === cur ? prev + 1 : prev), base);
}
@@ -0,0 +1,245 @@
/*
* 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 {
NavigationTabsService,
ITab,
ConnectionsManagerService,
INavigator,
NavigationService,
IContextProvider,
ITabOptions,
NodesManagerService,
INodeNavigationData,
NavigationType,
} from '@dbeaver/core/app';
import { injectable } from '@dbeaver/core/di';
import { NotificationService } from '@dbeaver/core/eventsLog';
import { GraphQLService } from '@dbeaver/core/sdk';
import { ISqlEditorTabState, ISqlContextParams } from './ISqlEditorTabState';
import { SqlDialectInfoService } from './SqlDialectInfoService';
import { sqlEditorTabHandlerKey } from './sqlEditorTabHandlerKey';
import { SqlExecutionState } from './SqlExecutionState';
enum SQLEditorNavigationAction {
create,
select,
close
}
export interface SQLEditorActionContext {
type: SQLEditorNavigationAction;
}
export interface SQLCreateAction extends SQLEditorActionContext {
type: SQLEditorNavigationAction.create;
connectionId?: string;
catalogId?: string;
schemaId?: string;
}
export interface SQLEditorAction extends SQLEditorActionContext {
type: SQLEditorNavigationAction.close | SQLEditorNavigationAction.select;
editorId: string;
resultId: string;
}
@injectable()
export class SqlEditorNavigatorService {
private readonly navigator: INavigator<SQLCreateAction | SQLEditorAction>;
constructor(private navigationTabsService: NavigationTabsService,
private connectionsManagerService: ConnectionsManagerService,
private notificationService: NotificationService,
private gql: GraphQLService,
private sqlDialectInfoService: SqlDialectInfoService,
private nodesManagerService: NodesManagerService,
private navigationService: NavigationService) {
this.navigator = this.navigationService.createNavigator<SQLCreateAction | SQLEditorAction>(
null,
this.navigateHandler.bind(this)
);
this.nodesManagerService.navigator.addHandler(this.nodeNavigationHandler.bind(this));
}
registerTabHandler() {
}
openNewEditor(connectionId?: string) {
this.navigator.navigateTo({
type: SQLEditorNavigationAction.create,
connectionId,
});
}
openEditorResult(editorId: string, resultId: string) {
this.navigator.navigateTo({
type: SQLEditorNavigationAction.select,
editorId,
resultId,
});
}
closeEditorResult(editorId: string, resultId: string) {
this.navigator.navigateTo({
type: SQLEditorNavigationAction.close,
editorId,
resultId,
});
}
private async nodeNavigationHandler(contexts: IContextProvider<INodeNavigationData>) {
try {
const nodeInfo = await contexts.getContext(this.nodesManagerService.navigationNodeContext);
if (nodeInfo.type === NavigationType.closeConnection) {
for (const tab of this.navigationTabsService.findTabs(
isSQLEditorTab(tab => tab.handlerState.connectionId.includes(nodeInfo.nodeId))
)) {
await this.navigationTabsService.closeTab(tab.id);
}
return;
}
} catch (exception) {
this.notificationService.logException(exception, 'Error in Object Viewer while processing action with database node');
}
}
private async navigateHandler(
contexts: IContextProvider<SQLCreateAction | SQLEditorAction>,
data: SQLCreateAction | SQLEditorAction
) {
try {
const tabInfo = await contexts.getContext(this.navigationTabsService.navigationTabContext);
if (data.type === SQLEditorNavigationAction.create) {
const tabOptions = await this.createNewEditor(data.connectionId);
if (tabOptions) {
tabInfo.openNewTab(tabOptions);
} else {
this.notificationService.logError({
title: `Failed to create editor for ${data.connectionId} connection`,
});
}
return;
}
const tab = this.navigationTabsService.findTab(isSQLEditorTab(tab => tab.id === data.editorId));
if (!tab) {
return;
}
if (data.type === SQLEditorNavigationAction.select) {
tab.handlerState.currentResultTabId = data.resultId;
} else if (data.type === SQLEditorNavigationAction.close) {
tab.handlerState.resultTabs.splice(
tab.handlerState.resultTabs.findIndex(result => result.resultTabId === data.resultId),
1
);
if (tab.handlerState.currentResultTabId === data.resultId) {
tab.handlerState.currentResultTabId = tab.handlerState.resultTabs[0]?.resultTabId || '';
}
}
this.navigationTabsService.selectTab(tab.id);
} catch (exception) {
this.notificationService.logException(exception, 'Error in SQL Editor while processing action with editor');
}
}
private async createNewEditor(connectionId?: string): Promise<ITabOptions<ISqlEditorTabState> | null> {
const order = this.getFreeEditorId();
const connection = connectionId
? this.connectionsManagerService.getConnectionById(connectionId)
: this.connectionsManagerService.connections[0];
if (!connection) {
return null;
}
await this.sqlDialectInfoService.loadSqlDialectInfo(connection.id);
const context = await this.createSqlContext(connection.id);
return {
handlerId: sqlEditorTabHandlerKey,
handlerState: {
query: '',
order,
contextId: context.contextId,
connectionId: connection.id,
objectCatalogId: context.objectCatalogId,
objectSchemaId: context.objectSchemaId,
sqlExecutionState: new SqlExecutionState(),
resultTabs: [],
},
};
}
private getFreeEditorId() {
const editorTabs = this.navigationTabsService.findTabs(isSQLEditorTab);
const ordered = Array.from(editorTabs).map(tab => tab.handlerState.order);
return findMinimalFree(ordered, 1);
}
/**
* 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<ISqlContextParams> {
const response = await this.gql.gql.sqlContextCreate({
connectionId,
defaultCatalog,
defaultSchema,
});
return {
contextId: response.context.id,
connectionId,
objectCatalogId: response.context.defaultCatalog,
objectSchemaId: response.context.defaultSchema,
};
}
}
function findMinimalFree(array: number[], base: number): number {
return array
.sort((a, b) => b - a)
.reduceRight((prev, cur) => (prev === cur ? prev + 1 : prev), base);
}
export function isSQLEditorTab(tab: ITab): tab is ITab<ISqlEditorTabState>;
export function isSQLEditorTab(
predicate: (tab: ITab<ISqlEditorTabState>) => boolean
): (tab: ITab) => tab is ITab<ISqlEditorTabState>
export function isSQLEditorTab(
tab: ITab | ((tab: ITab<ISqlEditorTabState>) => boolean)
): boolean | ((tab: ITab) => tab is ITab<ISqlEditorTabState>) {
if (typeof tab === 'function') {
const predicate = tab;
return (tab: ITab): tab is ITab<ISqlEditorTabState> => {
const sqlEditorTab = tab.handlerId === sqlEditorTabHandlerKey;
if (!predicate || !sqlEditorTab) {
return sqlEditorTab;
}
return predicate(tab);
};
}
return tab.handlerId === sqlEditorTabHandlerKey;
}
@@ -0,0 +1,44 @@
/*
* 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 styled, { css } from 'reshadow';
import { TabHandlerPanelProps } from '@dbeaver/core/app';
import {
splitStyles, Split, ResizerControls, Pane, splitHorizontalStyles,
} from '@dbeaver/core/blocks';
import { useStyles } from '@dbeaver/core/theming';
import { ISqlEditorTabState } from './ISqlEditorTabState';
import { SqlEditor } from './SqlEditor';
import { SqlResultTabs } from './SqlResultTabs/SqlResultTabs';
const viewerStyles = css`
Pane {
composes: theme-typography--body2 from global;
display: flex;
}
SqlEditor {
composes: theme-typography--body1 from global;
}
`;
export function SqlEditorPanel({ tab }: TabHandlerPanelProps<ISqlEditorTabState>) {
return styled(useStyles(splitStyles, splitHorizontalStyles, viewerStyles))(
<Split split="horizontal" sticky={30}>
<Pane>
<SqlEditor tabId={tab.id}/>
</Pane>
<ResizerControls />
<Pane main={true}>
<SqlResultTabs tab={tab}/>
</Pane>
</Split>
);
}

Some files were not shown because too many files have changed in this diff Show More