mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge pull request #17 from dbeaver/chore-deps-update
refactor: navigation
This commit is contained in:
@@ -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"
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
+160
-184
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-77
@@ -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);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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>
|
||||
);
|
||||
|
||||
+88
-59
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
-19
@@ -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>;
|
||||
}
|
||||
+2
@@ -4,11 +4,13 @@ query navGetStructContainers($connectionId: ID!, $catalogId: ID) {
|
||||
name
|
||||
description
|
||||
type
|
||||
features
|
||||
}
|
||||
schemaList {
|
||||
name
|
||||
description
|
||||
type
|
||||
features
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
+37
-40
@@ -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
|
||||
*/
|
||||
|
||||
+23
@@ -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);
|
||||
}
|
||||
+23
@@ -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;
|
||||
}
|
||||
+21
-14
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
-30
@@ -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}/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
-29
@@ -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} />
|
||||
);
|
||||
});
|
||||
-31
@@ -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>
|
||||
);
|
||||
});
|
||||
+24
@@ -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} />;
|
||||
});
|
||||
+31
@@ -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} />;
|
||||
});
|
||||
-21
@@ -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;
|
||||
}
|
||||
-24
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
+5
-11
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+8
-5
@@ -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>
|
||||
);
|
||||
}
|
||||
});
|
||||
+12
-23
@@ -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);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import {
|
||||
ObjectPropertyInput,
|
||||
ObjectPropertyProps,
|
||||
} from '../formControls';
|
||||
import { filterProperty, matchType } from '../tools';
|
||||
import { filterProperty, matchType } from '../helpers';
|
||||
|
||||
const propertyStyles = css`
|
||||
property {
|
||||
+6
-9
@@ -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>
|
||||
);
|
||||
});
|
||||
+70
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -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>
|
||||
);
|
||||
});
|
||||
+1
-1
@@ -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 {
|
||||
+1
-1
@@ -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`
|
||||
+1
-1
@@ -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`
|
||||
+1
-1
@@ -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`
|
||||
-4
@@ -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
Reference in New Issue
Block a user