From da8891a43abeb169d9a100a0562606fbf04e772f Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 22 Apr 2020 18:30:03 +0300 Subject: [PATCH 1/7] fix(object-viewer-plugin): show empty properties dbeaver-corp/dbeaver-web#632 --- webapp/packages/object-viewer-plugin/src/tools.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/webapp/packages/object-viewer-plugin/src/tools.ts b/webapp/packages/object-viewer-plugin/src/tools.ts index 26e3d40681..410d276ca4 100644 --- a/webapp/packages/object-viewer-plugin/src/tools.ts +++ b/webapp/packages/object-viewer-plugin/src/tools.ts @@ -37,9 +37,5 @@ export function additionalProps(property: ObjectPropertyInfo) { } export function filterProperty(property: ObjectPropertyInfo) { - if (property.value === null) { - return false; - } - return true; } From ee3ca9de12a23620736230c4ef14b62cd380fc59 Mon Sep 17 00:00:00 2001 From: Wroud Date: Sun, 26 Apr 2020 19:59:44 +0300 Subject: [PATCH 2/7] chore: cleanup --- webapp/packages/ag-grid-plugin/src/styles/styles.ts | 1 - webapp/yarn.lock | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/webapp/packages/ag-grid-plugin/src/styles/styles.ts b/webapp/packages/ag-grid-plugin/src/styles/styles.ts index ce342f7829..57c2492941 100644 --- a/webapp/packages/ag-grid-plugin/src/styles/styles.ts +++ b/webapp/packages/ag-grid-plugin/src/styles/styles.ts @@ -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]; }; diff --git a/webapp/yarn.lock b/webapp/yarn.lock index f3d6c23839..9b93aa3dbd 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -10799,10 +10799,10 @@ preact-ssr-prepass@^1.0.1: resolved "https://registry.yarnpkg.com/preact-ssr-prepass/-/preact-ssr-prepass-1.0.1.tgz#15ce9306edacc1095fcefe3e6a4e461f3af805cc" integrity sha512-pUCh/X/KJd45GVCfQAqmIdL6eLSj/EIrCPBU81s95wEYNM9f1omsZWrxusIwJP3oVKQyt29//9dLBjY2P6iyYg== -preact@^10.4.0: - version "10.4.0" - resolved "https://registry.yarnpkg.com/preact/-/preact-10.4.0.tgz#90e10264a221690484a56344437a353ffac08600" - integrity sha512-34iqY2qPWKAmsi+tNNwYCstta93P+zF1f4DLtsOUPh32uYImNzJY7h7EymCva+6RoJL01v3W3phSRD8jE0sFLg== +preact@^10.4.1: + version "10.4.1" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.4.1.tgz#9b3ba020547673a231c6cf16f0fbaef0e8863431" + integrity sha512-WKrRpCSwL2t3tpOOGhf2WfTpcmbpxaWtDbdJdKdjd0aEiTkvOmS4NBkG6kzlaAHI9AkQ3iVqbFWM3Ei7mZ4o1Q== prelude-ls@~1.1.2: version "1.1.2" From 38fe42fa10d7578ca4315990757937cfcaef9fec Mon Sep 17 00:00:00 2001 From: Wroud Date: Sun, 26 Apr 2020 20:01:00 +0300 Subject: [PATCH 3/7] feat(extensions): implementation --- webapp/packages/core/extensions/package.json | 8 +++++ .../core/src/extensions/IExtension.ts | 15 ++++++++++ .../packages/core/src/extensions/constants.ts | 10 +++++++ .../core/src/extensions/createExtension.ts | 30 +++++++++++++++++++ webapp/packages/core/src/extensions/index.ts | 2 ++ 5 files changed, 65 insertions(+) create mode 100644 webapp/packages/core/extensions/package.json create mode 100644 webapp/packages/core/src/extensions/IExtension.ts create mode 100644 webapp/packages/core/src/extensions/constants.ts create mode 100644 webapp/packages/core/src/extensions/createExtension.ts create mode 100644 webapp/packages/core/src/extensions/index.ts diff --git a/webapp/packages/core/extensions/package.json b/webapp/packages/core/extensions/package.json new file mode 100644 index 0000000000..3b62caed89 --- /dev/null +++ b/webapp/packages/core/extensions/package.json @@ -0,0 +1,8 @@ +{ + "name": "@dbeaver/core/extensions", + "private": true, + "sideEffects": false, + "main": "../dist/extensions", + "module": "../dist/extensions", + "types": "../dist/types/extensions" +} diff --git a/webapp/packages/core/src/extensions/IExtension.ts b/webapp/packages/core/src/extensions/IExtension.ts new file mode 100644 index 0000000000..b11e5de0e1 --- /dev/null +++ b/webapp/packages/core/src/extensions/IExtension.ts @@ -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 { + [EXTENSION_SYMBOL]: symbol; + [EXTENSION_TYPE_SYMBOL]?: T; +} diff --git a/webapp/packages/core/src/extensions/constants.ts b/webapp/packages/core/src/extensions/constants.ts new file mode 100644 index 0000000000..9e98216e22 --- /dev/null +++ b/webapp/packages/core/src/extensions/constants.ts @@ -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'); diff --git a/webapp/packages/core/src/extensions/createExtension.ts b/webapp/packages/core/src/extensions/createExtension.ts new file mode 100644 index 0000000000..2b3c21ce5c --- /dev/null +++ b/webapp/packages/core/src/extensions/createExtension.ts @@ -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( + extension: object, + key: symbol +): IExtension { + return Object.assign(extension, { [EXTENSION_SYMBOL]: key }); +} + +export function isExtension( + obj: any, + key?: symbol, +): obj is IExtension { + if (!(EXTENSION_SYMBOL in obj)) { + return false; + } + if (key && obj[EXTENSION_SYMBOL] !== key) { + return false; + } + return true; +} diff --git a/webapp/packages/core/src/extensions/index.ts b/webapp/packages/core/src/extensions/index.ts new file mode 100644 index 0000000000..4a845dbddb --- /dev/null +++ b/webapp/packages/core/src/extensions/index.ts @@ -0,0 +1,2 @@ +export * from './IExtension'; +export * from './createExtension'; From 54c272265d230358f189d732ef2d3ffbc852b64b Mon Sep 17 00:00:00 2001 From: Wroud Date: Sun, 26 Apr 2020 20:02:21 +0300 Subject: [PATCH 4/7] chore(core): sdk update --- .../queries/navGetStructContainers.gql | 2 ++ webapp/packages/core/src/sdk/sdk.ts | 16 ++++++---------- webapp/packages/dbeaver/package.json | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/queries/navGetStructContainers.gql b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/queries/navGetStructContainers.gql index 2efac2e811..f88cb4ff99 100644 --- a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/queries/navGetStructContainers.gql +++ b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/queries/navGetStructContainers.gql @@ -4,11 +4,13 @@ query navGetStructContainers($connectionId: ID!, $catalogId: ID) { name description type + features } schemaList { name description type + features } } } diff --git a/webapp/packages/core/src/sdk/sdk.ts b/webapp/packages/core/src/sdk/sdk.ts index e25e51538a..c4098afa50 100644 --- a/webapp/packages/core/src/sdk/sdk.ts +++ b/webapp/packages/core/src/sdk/sdk.ts @@ -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; license?: Maybe; custom?: Maybe; + /** Driver score for ordering, biggest first */ promotedScore?: Maybe; connectionProperties?: Maybe; defaultConnectionProperties?: Maybe; @@ -427,6 +420,7 @@ export type ServerConfig = { supportsWorkspaces?: Maybe; supportedLanguages?: Maybe>>; services?: Maybe>>; + productConfiguration: Scalars["Object"]; }; export type ServerError = { @@ -554,10 +548,10 @@ export type NavGetStructContainersQueryVariables = { export type NavGetStructContainersQuery = { navGetStructContainers: { catalogList: Array< - Pick + Pick >; schemaList: Array< - Pick + Pick >; }; }; @@ -1143,11 +1137,13 @@ export const NavGetStructContainersDocument = gql` name description type + features } schemaList { name description type + features } } } diff --git a/webapp/packages/dbeaver/package.json b/webapp/packages/dbeaver/package.json index ccfb59159a..c75856cb72 100644 --- a/webapp/packages/dbeaver/package.json +++ b/webapp/packages/dbeaver/package.json @@ -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", From b1c1f8ebb5c993684700a1f22f9d7b413b925607 Mon Sep 17 00:00:00 2001 From: Wroud Date: Sun, 26 Apr 2020 22:04:46 +0300 Subject: [PATCH 5/7] feat(core): extensions --- .../extensions/IConnectionProvider.ts | 23 +++++++++++++++++++ .../extensions/IConnectionSetter.ts | 23 +++++++++++++++++++ .../extensions/IObjectCatalogProvider.ts | 23 +++++++++++++++++++ .../extensions/IObjectCatalogSetter.ts | 23 +++++++++++++++++++ .../extensions/IObjectSchemaProvider.ts | 23 +++++++++++++++++++ .../extensions/IObjectSchemaSetter.ts | 23 +++++++++++++++++++ 6 files changed, 138 insertions(+) create mode 100644 webapp/packages/core/src/app/shared/ConnectionsManager/extensions/IConnectionProvider.ts create mode 100644 webapp/packages/core/src/app/shared/ConnectionsManager/extensions/IConnectionSetter.ts create mode 100644 webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectCatalogProvider.ts create mode 100644 webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectCatalogSetter.ts create mode 100644 webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectSchemaProvider.ts create mode 100644 webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectSchemaSetter.ts diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/extensions/IConnectionProvider.ts b/webapp/packages/core/src/app/shared/ConnectionsManager/extensions/IConnectionProvider.ts new file mode 100644 index 0000000000..d470d472de --- /dev/null +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/extensions/IConnectionProvider.ts @@ -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 { + (context: T): string; +} + +export function connectionProvider(provider: IConnectionProvider) { + return createExtension(provider, connectionProviderSymbol); +} + +export function isConnectionProvider(obj: IExtension): obj is IConnectionProvider & IExtension { + return isExtension(obj, connectionProviderSymbol); +} diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/extensions/IConnectionSetter.ts b/webapp/packages/core/src/app/shared/ConnectionsManager/extensions/IConnectionSetter.ts new file mode 100644 index 0000000000..d21b5ab4e4 --- /dev/null +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/extensions/IConnectionSetter.ts @@ -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 { + (connectionId: string, context: T): Promise | boolean; +} + +export function connectionSetter(setter: IConnectionSetter) { + return createExtension(setter, connectionSetterSymbol); +} + +export function isConnectionSetter(obj: IExtension): obj is IConnectionSetter & IExtension { + return isExtension(obj, connectionSetterSymbol); +} diff --git a/webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectCatalogProvider.ts b/webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectCatalogProvider.ts new file mode 100644 index 0000000000..c4c8155fa4 --- /dev/null +++ b/webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectCatalogProvider.ts @@ -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 { + (context: T): string | undefined; +} + +export function objectCatalogProvider(provider: IObjectCatalogProvider) { + return createExtension(provider, objectCatalogProviderSymbol); +} + +export function isObjectCatalogProvider(obj: IExtension): obj is IObjectCatalogProvider & IExtension { + return isExtension(obj, objectCatalogProviderSymbol); +} diff --git a/webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectCatalogSetter.ts b/webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectCatalogSetter.ts new file mode 100644 index 0000000000..32b9a1aac2 --- /dev/null +++ b/webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectCatalogSetter.ts @@ -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 { + (catalogId: string, context: T): Promise | boolean; +} + +export function objectCatalogSetter(provider: IObjectCatalogSetter) { + return createExtension(provider, objectCatalogSetterSymbol); +} + +export function isObjectCatalogSetter(obj: IExtension): obj is IObjectCatalogSetter & IExtension { + return isExtension(obj, objectCatalogSetterSymbol) as boolean; +} diff --git a/webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectSchemaProvider.ts b/webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectSchemaProvider.ts new file mode 100644 index 0000000000..50d4358fd5 --- /dev/null +++ b/webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectSchemaProvider.ts @@ -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 { + (context: T): string | undefined; +} + +export function objectSchemaProvider(provider: IObjectSchemaProvider) { + return createExtension(provider, objectSchemaProviderSymbol); +} + +export function isObjectSchemaProvider(obj: IExtension): obj is IObjectSchemaProvider & IExtension { + return isExtension(obj, objectSchemaProviderSymbol); +} diff --git a/webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectSchemaSetter.ts b/webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectSchemaSetter.ts new file mode 100644 index 0000000000..37b5efe2ee --- /dev/null +++ b/webapp/packages/core/src/app/shared/NodesManager/extensions/IObjectSchemaSetter.ts @@ -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 { + (schemaId: string, context: T): Promise | boolean; +} + +export function objectSchemaSetter(provider: IObjectSchemaSetter) { + return createExtension(provider, objectSchemaSetterSymbol); +} + +export function isObjectSchemaSetter(obj: IExtension): obj is IObjectSchemaSetter & IExtension { + return isExtension(obj, objectSchemaSetterSymbol) as boolean; +} From 075e6f8eaea3e8be3bdb7aafaeb10f65d869f22d Mon Sep 17 00:00:00 2001 From: Wroud Date: Sun, 26 Apr 2020 22:52:04 +0300 Subject: [PATCH 6/7] refactor: navigation tabs & navigation --- .../ag-grid-plugin/src/styles/styles.ts | 2 +- webapp/packages/core/src/AppBootstrap.ts | 6 +- .../ConnectionSchemaManagerService.ts | 344 ++++++++---------- .../ConnectionSchemaStore.ts | 77 ---- .../ConnectionSelector/ConnectionSelector.tsx | 6 +- .../ConnectionSelectorController.ts | 147 +++++--- .../IConnectionCatalogSchema.ts | 19 - webapp/packages/core/src/app/index.ts | 11 +- .../ConnectionsManagerService.ts | 77 ++-- .../src/app/shared/NavigationTabs/ITab.ts | 19 + .../NavigationTabsBar/NavigationTabsBar.tsx | 35 +- .../NavigationTabsBar/Tabs/HandlerContent.tsx | 30 -- .../NavigationTabsBar/Tabs/HandlerData.tsx | 29 -- .../NavigationTabsBar/Tabs/TabContent.tsx | 31 -- .../NavigationTabsBar/Tabs/TabData.tsx | 63 ---- .../Tabs/TabHandlerPanel.tsx | 24 ++ .../NavigationTabsBar/Tabs/TabHandlerTab.tsx | 31 ++ .../NavigationTabsBar/Tabs/useTabHandlers.ts | 21 -- .../NavigationTabsBar/useNavigationTabs.ts | 24 -- .../NavigationTabs/NavigationTabsService.ts | 141 ++++--- .../core/src/app/shared/NavigationTabs/Tab.ts | 59 --- .../app/shared/NavigationTabs/TabHandler.ts | 78 ++-- .../NavigationTabs/TabNavigationContext.ts | 36 +- .../NavigationTabs/useTabHandlerState.ts | 24 -- .../NodesManager/NodesManagerService.ts | 121 +++--- .../core/src/blocks/Tabs/TabsBox/TabsBox.tsx | 1 - .../packages/core/src/sdk/CachedResource.ts | 59 ++- .../packages/data-viewer-plugin/package.json | 1 + .../DataViewerPanel.tsx} | 16 +- .../src/DataViewerPage/DataViewerTab.tsx | 33 ++ .../src/DataViewerTabService.ts | 118 +++--- .../src/DdlViewer/DdlViewerController.ts | 2 +- .../src/DdlViewerTabService.ts | 35 +- .../src/IObjectViewerTabContext.ts | 20 + .../src/IObjectViewerTabState.ts | 14 + .../src/ObjectPage/DBObjectPagePanel.tsx | 19 + .../src/ObjectPage/DBObjectPageService.ts | 84 +++++ .../src/ObjectPage/DBObjectPageTab.tsx | 32 ++ .../src/ObjectPage/ObjectPage.ts | 67 ++++ .../FolderContext.ts | 0 .../ObjectFolders.tsx | 13 +- .../ObjectFoldersController.ts | 35 +- .../ObjectFoldersService.ts | 0 .../ObjectFoldersTab/ObjectFolderTabModel.ts | 0 .../ObjectFoldersTabService.ts | 0 .../NodePropertiesMixin.ts | 0 .../ObjectFoldersTabContainer.ts | 0 .../ObjectInfoTab/ObjectInfoTab.tsx | 0 .../ObjectInfoTab/ObjectInfoTabModel.ts | 0 .../ObjectInfoTab/ObjectInfoTabService.ts | 2 +- .../ObjectInfoTab/ObjectProperties.tsx | 0 .../ObjectInfoTab/ObjectProperty.tsx | 2 +- .../ObjectPropertiesPagePanel.tsx} | 15 +- .../ObjectPropertiesPageService.ts | 70 ++++ .../ObjectPropertiesPageTab.tsx | 27 ++ .../ObjectPropertyTable/Header.tsx | 0 .../ObjectPropertyTable/Item.tsx | 2 +- .../ObjectChildrenPropertyTable.tsx | 0 .../ObjectPropertyTable.tsx | 2 +- .../VirtualFolderTab/VirtualFolderTab.tsx | 0 .../VirtualFolderTab/VirtualFolderTabData.tsx | 0 .../VirtualFolderTabMixin.tsx | 0 .../VirtualFolderTabModel.tsx | 0 .../VirtualFolderTabService.tsx | 0 .../formControls/ObjectPropertyCheckbox.tsx | 2 +- .../formControls/ObjectPropertyInput.tsx | 2 +- .../formControls/ObjectPropertyProps.ts | 0 .../formControls/index.ts | 0 .../helpers.ts} | 0 .../src/ObjectViewerBootstrap.ts | 7 +- .../src/ObjectViewerPanel.tsx | 71 ++++ .../src/ObjectViewerService.ts | 86 ----- .../src/ObjectViewerTab.tsx | 29 ++ .../src/ObjectViewerTabService.ts | 258 ++++++++++--- .../object-viewer-plugin/src/Viewer.tsx | 20 - .../object-viewer-plugin/src/index.ts | 7 +- .../object-viewer-plugin/src/manifest.ts | 14 +- .../src/useObjectFolder.ts | 6 +- .../sql-editor/src/ISqlEditorTabState.ts | 9 +- .../src/SqlEditor/SqlEditorController.tsx | 42 +-- .../src/SqlEditor/SqlEditorService.ts | 22 +- .../sql-editor/src/SqlEditorBootstrap.ts | 8 +- .../sql-editor/src/SqlEditorManagerService.ts | 330 ----------------- .../src/SqlEditorNavigatorService.ts | 245 +++++++++++++ .../sql-editor/src/SqlEditorPanel.tsx | 44 +++ .../packages/sql-editor/src/SqlEditorTab.tsx | 48 +-- .../sql-editor/src/SqlEditorTabService.ts | 201 +++++++++- .../sql-editor/src/SqlExecutionState.ts | 2 +- .../src/SqlResultTabs/SqlResultTabs.tsx | 36 +- .../src/SqlResultTabs/SqlResultTabsService.ts | 4 +- webapp/packages/sql-editor/src/manifest.ts | 6 +- 91 files changed, 2006 insertions(+), 1617 deletions(-) delete mode 100644 webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSchemaStore.ts delete mode 100644 webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/IConnectionCatalogSchema.ts create mode 100644 webapp/packages/core/src/app/shared/NavigationTabs/ITab.ts delete mode 100644 webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/HandlerContent.tsx delete mode 100644 webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/HandlerData.tsx delete mode 100644 webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabContent.tsx delete mode 100644 webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabData.tsx create mode 100644 webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabHandlerPanel.tsx create mode 100644 webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabHandlerTab.tsx delete mode 100644 webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/useTabHandlers.ts delete mode 100644 webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/useNavigationTabs.ts delete mode 100644 webapp/packages/core/src/app/shared/NavigationTabs/Tab.ts delete mode 100644 webapp/packages/core/src/app/shared/NavigationTabs/useTabHandlerState.ts rename webapp/packages/data-viewer-plugin/src/{DataViewerTab/DataViewer.tsx => DataViewerPage/DataViewerPanel.tsx} (52%) create mode 100644 webapp/packages/data-viewer-plugin/src/DataViewerPage/DataViewerTab.tsx create mode 100644 webapp/packages/object-viewer-plugin/src/IObjectViewerTabContext.ts create mode 100644 webapp/packages/object-viewer-plugin/src/IObjectViewerTabState.ts create mode 100644 webapp/packages/object-viewer-plugin/src/ObjectPage/DBObjectPagePanel.tsx create mode 100644 webapp/packages/object-viewer-plugin/src/ObjectPage/DBObjectPageService.ts create mode 100644 webapp/packages/object-viewer-plugin/src/ObjectPage/DBObjectPageTab.tsx create mode 100644 webapp/packages/object-viewer-plugin/src/ObjectPage/ObjectPage.ts rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/FolderContext.ts (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectFolders.tsx (64%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectFoldersController.ts (59%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectFoldersService.ts (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectFoldersTab/ObjectFolderTabModel.ts (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectFoldersTab/ObjectFoldersTabService.ts (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectFoldersTabsContainer/NodePropertiesMixin.ts (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectFoldersTabsContainer/ObjectFoldersTabContainer.ts (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectInfoTab/ObjectInfoTab.tsx (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectInfoTab/ObjectInfoTabModel.ts (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectInfoTab/ObjectInfoTabService.ts (90%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectInfoTab/ObjectProperties.tsx (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectInfoTab/ObjectProperty.tsx (95%) rename webapp/packages/object-viewer-plugin/src/{ObjectViewer.tsx => ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx} (76%) create mode 100644 webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPageService.ts create mode 100644 webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPageTab.tsx rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectPropertyTable/Header.tsx (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectPropertyTable/Item.tsx (98%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectPropertyTable/ObjectChildrenPropertyTable.tsx (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/ObjectPropertyTable/ObjectPropertyTable.tsx (95%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/VirtualFolderTab/VirtualFolderTab.tsx (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/VirtualFolderTab/VirtualFolderTabData.tsx (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/VirtualFolderTab/VirtualFolderTabMixin.tsx (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/VirtualFolderTab/VirtualFolderTabModel.tsx (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/VirtualFolderTab/VirtualFolderTabService.tsx (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/formControls/ObjectPropertyCheckbox.tsx (95%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/formControls/ObjectPropertyInput.tsx (95%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/formControls/ObjectPropertyProps.ts (100%) rename webapp/packages/object-viewer-plugin/src/{ => ObjectPropertiesPage}/formControls/index.ts (100%) rename webapp/packages/object-viewer-plugin/src/{tools.ts => ObjectPropertiesPage/helpers.ts} (100%) create mode 100644 webapp/packages/object-viewer-plugin/src/ObjectViewerPanel.tsx delete mode 100644 webapp/packages/object-viewer-plugin/src/ObjectViewerService.ts create mode 100644 webapp/packages/object-viewer-plugin/src/ObjectViewerTab.tsx delete mode 100644 webapp/packages/object-viewer-plugin/src/Viewer.tsx delete mode 100644 webapp/packages/sql-editor/src/SqlEditorManagerService.ts create mode 100644 webapp/packages/sql-editor/src/SqlEditorNavigatorService.ts create mode 100644 webapp/packages/sql-editor/src/SqlEditorPanel.tsx diff --git a/webapp/packages/ag-grid-plugin/src/styles/styles.ts b/webapp/packages/ag-grid-plugin/src/styles/styles.ts index 57c2492941..a3be9a48cc 100644 --- a/webapp/packages/ag-grid-plugin/src/styles/styles.ts +++ b/webapp/packages/ag-grid-plugin/src/styles/styles.ts @@ -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'); diff --git a/webapp/packages/core/src/AppBootstrap.ts b/webapp/packages/core/src/AppBootstrap.ts index 015dd6a91d..aa41c02045 100644 --- a/webapp/packages/core/src/AppBootstrap.ts +++ b/webapp/packages/core/src/AppBootstrap.ts @@ -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(); diff --git a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts index 8299f8f372..a07c43eec8 100644 --- a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts +++ b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts @@ -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('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 | null = null; + @observable private getCurrentSchemaId: IObjectSchemaProvider | null = null; + @observable private getCurrentCatalogId: IObjectCatalogProvider | null = null; + @observable private changeConnectionId: IConnectionSetter | null = null; + @observable private changeCatalogId: IObjectCatalogSetter | null = null; + @observable private changeSchemaId: IObjectSchemaSetter | 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 { + 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 { - 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}`); + } } } diff --git a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSchemaStore.ts b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSchemaStore.ts deleted file mode 100644 index 1d0ca1cb70..0000000000 --- a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSchemaStore.ts +++ /dev/null @@ -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(c => c.id); - private schemaMap = new OrderedMap(s => s.id); - private catalogsMap = new OrderedMap(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); - } -} diff --git a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelector.tsx b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelector.tsx index 6fab54d2d6..254d22c01a 100644 --- a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelector.tsx +++ b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelector.tsx @@ -52,10 +52,10 @@ export const ConnectionSelector = observer(function ConnectionSelector() { const SchemaOrCatalogMenu = TopMenuItem; return styled(useStyles(style))( - + - {controller.isSchemaSelectorVisible && ( - + {controller.isObjectContainerSelectorVisible && ( + )} ); diff --git a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelectorController.ts b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelectorController.ts index c7fadf3a3a..5810624628 100644 --- a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelectorController.ts +++ b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelectorController.ts @@ -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; + }); } } diff --git a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/IConnectionCatalogSchema.ts b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/IConnectionCatalogSchema.ts deleted file mode 100644 index 0aaf3ffb7d..0000000000 --- a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/IConnectionCatalogSchema.ts +++ /dev/null @@ -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; -} diff --git a/webapp/packages/core/src/app/index.ts b/webapp/packages/core/src/app/index.ts index ff551cbe27..f468e13079 100644 --- a/webapp/packages/core/src/app/index.ts +++ b/webapp/packages/core/src/app/index.ts @@ -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'; diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionsManagerService.ts b/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionsManagerService.ts index 38581fc15c..c68afdcb8e 100644 --- a/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionsManagerService.ts +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionsManagerService.ts @@ -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 export type Connection = Pick - -export interface ISchema { - id: string; -} - -export interface ICatalogsAndSchemas { - catalogs: ISchema[]; - schemas: ISchema[]; -} +export type ObjectContainer = Pick @injectable() export class ConnectionsManagerService { @observable private connectionsMap: Map = 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(); 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 { - 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 { - 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 { - const drivers = await this.graphQLService.gql.getDriverById({ driverId }); - return drivers.driverList[0] || null; + async loadObjectContainer(connectionId: string, catalogId?: string): Promise { + 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, + refresh: boolean, + connectionId: string, + catalogId?: string, + ): Promise> { + 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): Promise> { 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 */ diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/ITab.ts b/webapp/packages/core/src/app/shared/NavigationTabs/ITab.ts new file mode 100644 index 0000000000..4f223a430b --- /dev/null +++ b/webapp/packages/core/src/app/shared/NavigationTabs/ITab.ts @@ -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 { + id?: string; + handlerId: string; + handlerState: T; + name?: string; + icon?: string; +} + +export interface ITab extends ITabOptions { + id: string; +} diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx index ae58e43bd3..5bcb47cb1d 100644 --- a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx +++ b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/NavigationTabsBar.tsx @@ -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 ( - + tabs={navigation.tabIdList.map(tabId => ( + + ))} + style={stylesArray} + > + {navigation.tabIdList.map(tabId => ( + + + + ))} + ); }); diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/HandlerContent.tsx b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/HandlerContent.tsx deleted file mode 100644 index 66c11f2848..0000000000 --- a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/HandlerContent.tsx +++ /dev/null @@ -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 ( - <> - - - - ); -}); diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/HandlerData.tsx b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/HandlerData.tsx deleted file mode 100644 index b9cd259d78..0000000000 --- a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/HandlerData.tsx +++ /dev/null @@ -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 ( - - ); -}); diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabContent.tsx b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabContent.tsx deleted file mode 100644 index aa57431fe2..0000000000 --- a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabContent.tsx +++ /dev/null @@ -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 ( - <> - - - - ); -}); diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabData.tsx b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabData.tsx deleted file mode 100644 index c3fe3b1aab..0000000000 --- a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabData.tsx +++ /dev/null @@ -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))( - 1 && handlers.map(handlerId => ( - - - - ))} - style={[styles]} - > - {handlers.map(handlerId => ( - - - - ))} - - ); -}); diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabHandlerPanel.tsx b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabHandlerPanel.tsx new file mode 100644 index 0000000000..a06795c411 --- /dev/null +++ b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabHandlerPanel.tsx @@ -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 ; +}); diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabHandlerTab.tsx b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabHandlerTab.tsx new file mode 100644 index 0000000000..92df5a8ce8 --- /dev/null +++ b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/TabHandlerTab.tsx @@ -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 ; +}); diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/useTabHandlers.ts b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/useTabHandlers.ts deleted file mode 100644 index c5411eaf6d..0000000000 --- a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/Tabs/useTabHandlers.ts +++ /dev/null @@ -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; -} diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/useNavigationTabs.ts b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/useNavigationTabs.ts deleted file mode 100644 index 191f8c7a5d..0000000000 --- a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsBar/useNavigationTabs.ts +++ /dev/null @@ -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), - }; -} diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsService.ts b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsService.ts index caeeb8cd50..502bb6e3f0 100644 --- a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsService.ts +++ b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsService.ts @@ -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(); - @observable tabsMap = new Map(); + @observable tabsMap = new Map(); @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(); + onTabSelect = new Subject(); + onTabClose = new Subject(); constructor(private notificationService: NotificationService, private autoSaveService: LocalStorageSaveService) { this.autoSaveService.withAutoSave( this.tabsMap, `${NAVIGATION_TABS_BASE_KEY}_tab_map`, - (json): IKeyValueMap => { - const map: IKeyValueMap = {}; - for (const [key, value] of Object.entries(json as IKeyValueMap)) { + (json): IKeyValueMap => { + const map: IKeyValueMap = {}; + for (const [key, value] of Object.entries(json as IKeyValueMap)) { 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( + options: TabHandlerOptions, + ): TabHandler { + 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(tabId: string, state: TabHandlerState) { + @action updateHandlerState(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(tabId: string, handlerId: string): T | undefined { + getHandlerState(tabId: string): T | undefined { const tab = this.tabsMap.get(tabId); if (!tab) { return; } - return tab.getHandlerState(handlerId); + return tab.handlerState; } - getTab(tabId: string): Tab | undefined { + getTab(tabId: string): ITab | undefined { return this.tabsMap.get(tabId); } + findTab(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; + findTabs(predicate: (tab: ITab) => tab is ITab): Generator>; + * findTabs(predicate: (tab: ITab) => boolean): Generator { + 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 { + private async restoreTab(tab: ITab, removedTabs: string[]): Promise { 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; - } } diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/Tab.ts b/webapp/packages/core/src/app/shared/NavigationTabs/Tab.ts deleted file mode 100644 index 831d73aaf1..0000000000 --- a/webapp/packages/core/src/app/shared/NavigationTabs/Tab.ts +++ /dev/null @@ -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 = { - handlerId: string; - state: T; -} - -export type TabOptions = { - nodeId: string; - handlerId: string; - handlerState: Map>; - name?: string; - icon?: string; -} - -export class Tab { - @observable nodeId: string - @observable handlerId: string - @observable handlerState: Map> - @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(state: TabHandlerState) { - 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(handlerId: string): T | undefined { - return this.handlerState.get(handlerId)?.state; - } -} diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/TabHandler.ts b/webapp/packages/core/src/app/shared/NavigationTabs/TabHandler.ts index 8bfefe40a2..247899e583 100644 --- a/webapp/packages/core/src/app/shared/NavigationTabs/TabHandler.ts +++ b/webapp/packages/core/src/app/shared/NavigationTabs/TabHandler.ts @@ -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 = { + tab: ITab; + handler: TabHandler; + onSelect(tabId: string): void; + onClose?(tabId: string): void; + style: Style[]; +} +export type TabHandlerTabComponent = React.FunctionComponent> -export type TabHandlerEvent = (tabId: string, handlerId: string) => void -export type TabRestoreEvent = ( - tabId: string, - handlerId: string, -) => Promise | boolean +export type TabHandlerPanelProps = { + tab: ITab; + handler: TabHandler; +} +export type TabHandlerPanelComponent = React.FunctionComponent> -export type TabHandlerFilter = (nodeId: string) => boolean +export type TabHandlerEvent = (tab: ITab) => Promise | void +export type TabRestoreEvent = (tab: ITab) => Promise | boolean -export type TabHandlerOptions = { +export type TabHandlerOptions = { 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; + getPanelComponent(): TabHandlerPanelComponent; + onSelect?: TabHandlerEvent; + onClose?: TabHandlerEvent; + onRestore?: TabRestoreEvent; + extensions?: IExtension>[]; } -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 { + key: string + navigatorId: string + getTabComponent: () => TabHandlerTabComponent; + getPanelComponent: () => TabHandlerPanelComponent; + onSelect?: TabHandlerEvent + onClose?: TabHandlerEvent + onRestore?: TabRestoreEvent + extensions?: IExtension>[]; - constructor(options: TabHandlerOptions) { + constructor(options: TabHandlerOptions) { 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; } } diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/TabNavigationContext.ts b/webapp/packages/core/src/app/shared/NavigationTabs/TabNavigationContext.ts index 45ee6ce15c..7bf1807251 100644 --- a/webapp/packages/core/src/app/shared/NavigationTabs/TabNavigationContext.ts +++ b/webapp/packages/core/src/app/shared/NavigationTabs/TabNavigationContext.ts @@ -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(options: ITabOptions): ITab; + 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(options: ITabOptions): ITab { + 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; } } diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/useTabHandlerState.ts b/webapp/packages/core/src/app/shared/NavigationTabs/useTabHandlerState.ts deleted file mode 100644 index 480d152395..0000000000 --- a/webapp/packages/core/src/app/shared/NavigationTabs/useTabHandlerState.ts +++ /dev/null @@ -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(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(handlerId); - - return state; -} diff --git a/webapp/packages/core/src/app/shared/NodesManager/NodesManagerService.ts b/webapp/packages/core/src/app/shared/NodesManager/NodesManagerService.ts index d62822199a..4cc0148719 100644 --- a/webapp/packages/core/src/app/shared/NodesManager/NodesManagerService.ts +++ b/webapp/packages/core/src/app/shared/NodesManager/NodesManagerService.ts @@ -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; 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( 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 { @@ -133,12 +155,21 @@ export class NodesManagerService { } async loadNodeInfo(nodeId: string): Promise { - 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 { @@ -179,11 +210,11 @@ export class NodesManagerService { return this.nodesStore.getNode(node.parentId); } - getConnectionCatalogSchema(nodeId: string): Partial { - const initial: Partial = {}; + getNodeContainerInfo(nodeId: string): INodeContainerInfo { + const initial: INodeContainerInfo = {}; - const scanParents = (res: Partial, - nodeId?: string): Partial => { + 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 => { 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) { - 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 || ''}`; } diff --git a/webapp/packages/core/src/blocks/Tabs/TabsBox/TabsBox.tsx b/webapp/packages/core/src/blocks/Tabs/TabsBox/TabsBox.tsx index e4012c6d64..b13f3b9265 100644 --- a/webapp/packages/core/src/blocks/Tabs/TabsBox/TabsBox.tsx +++ b/webapp/packages/core/src/blocks/Tabs/TabsBox/TabsBox.tsx @@ -44,7 +44,6 @@ export function TabsBox({ {tabs && ( {tabs} - )} diff --git a/webapp/packages/core/src/sdk/CachedResource.ts b/webapp/packages/core/src/sdk/CachedResource.ts index cde07dec89..bef342ba51 100644 --- a/webapp/packages/core/src/sdk/CachedResource.ts +++ b/webapp/packages/core/src/sdk/CachedResource.ts @@ -8,14 +8,15 @@ import { observable } from 'mobx'; -export type Loader = (current: TData, ...args: TArgs) => Promise +export type Loader = (current: TData, update: boolean, ...args: TArgs) => Promise export class CachedResource { @observable data: TData; @observable private loaded = false; @observable private loading = false; - private promise: Promise | null = null; + private refreshPromise: Promise | null = null; + private singleElementPromise: Promise | null = null; private loader: Loader; constructor(defaultValue: TData, loader: Loader) { @@ -32,36 +33,64 @@ export class CachedResource { } async refresh(...args: TArgs): Promise { - 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 { 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 { - 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 { + 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; } } diff --git a/webapp/packages/data-viewer-plugin/package.json b/webapp/packages/data-viewer-plugin/package.json index 9677fec3cf..085ae47e01 100644 --- a/webapp/packages/data-viewer-plugin/package.json +++ b/webapp/packages/data-viewer-plugin/package.json @@ -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", diff --git a/webapp/packages/data-viewer-plugin/src/DataViewerTab/DataViewer.tsx b/webapp/packages/data-viewer-plugin/src/DataViewerPage/DataViewerPanel.tsx similarity index 52% rename from webapp/packages/data-viewer-plugin/src/DataViewerTab/DataViewer.tsx rename to webapp/packages/data-viewer-plugin/src/DataViewerPage/DataViewerPanel.tsx index 8dbbc15146..a336f705c2 100644 --- a/webapp/packages/data-viewer-plugin/src/DataViewerTab/DataViewer.tsx +++ b/webapp/packages/data-viewer-plugin/src/DataViewerPage/DataViewerPanel.tsx @@ -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 ; + return ; }; diff --git a/webapp/packages/data-viewer-plugin/src/DataViewerPage/DataViewerTab.tsx b/webapp/packages/data-viewer-plugin/src/DataViewerPage/DataViewerTab.tsx new file mode 100644 index 0000000000..c1a122ddfa --- /dev/null +++ b/webapp/packages/data-viewer-plugin/src/DataViewerPage/DataViewerTab.tsx @@ -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))( + + + + + ); +}); diff --git a/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts b/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts index e9b49e199a..e51802d3dc 100644 --- a/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts +++ b/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts @@ -7,116 +7,96 @@ */ 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) { 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) { - return; - } - this.dataViewerTableService.createTableModelIfNotExists(tableId); + private handleTabSelect(tab: ITab) { + 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) { + // 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) { + this.dataViewerTableService.removeTableModel(tab.handlerState.objectId); } } diff --git a/webapp/packages/ddl-viewer-plugin/src/DdlViewer/DdlViewerController.ts b/webapp/packages/ddl-viewer-plugin/src/DdlViewer/DdlViewerController.ts index 1e9f152a9d..6008061b0c 100644 --- a/webapp/packages/ddl-viewer-plugin/src/DdlViewer/DdlViewerController.ts +++ b/webapp/packages/ddl-viewer-plugin/src/DdlViewer/DdlViewerController.ts @@ -55,7 +55,7 @@ export class DdlViewerController implements IInitializableController, IDestructi } private async loadDialect(nodeId: string): Promise { - const { connectionId } = this.nodesManagerService.getConnectionCatalogSchema(nodeId); + const { connectionId } = this.nodesManagerService.getNodeContainerInfo(nodeId); if (!connectionId) { return; } diff --git a/webapp/packages/ddl-viewer-plugin/src/DdlViewerTabService.ts b/webapp/packages/ddl-viewer-plugin/src/DdlViewerTabService.ts index a67c24d8ef..8441f5edcd 100644 --- a/webapp/packages/ddl-viewer-plugin/src/DdlViewerTabService.ts +++ b/webapp/packages/ddl-viewer-plugin/src/DdlViewerTabService.ts @@ -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) { + 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); } } diff --git a/webapp/packages/object-viewer-plugin/src/IObjectViewerTabContext.ts b/webapp/packages/object-viewer-plugin/src/IObjectViewerTabContext.ts new file mode 100644 index 0000000000..1bf5554dbf --- /dev/null +++ b/webapp/packages/object-viewer-plugin/src/IObjectViewerTabContext.ts @@ -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 | null; + page?: ObjectPage; + trySwitchPage(page: ObjectPage): boolean; + tabInfo: ITabNavigationContext; + nodeInfo: INodeNavigationContext; +} diff --git a/webapp/packages/object-viewer-plugin/src/IObjectViewerTabState.ts b/webapp/packages/object-viewer-plugin/src/IObjectViewerTabState.ts new file mode 100644 index 0000000000..7e07f2a27c --- /dev/null +++ b/webapp/packages/object-viewer-plugin/src/IObjectViewerTabState.ts @@ -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; +} diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPage/DBObjectPagePanel.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPage/DBObjectPagePanel.tsx new file mode 100644 index 0000000000..bba67cf086 --- /dev/null +++ b/webapp/packages/object-viewer-plugin/src/ObjectPage/DBObjectPagePanel.tsx @@ -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 ; +}); diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPage/DBObjectPageService.ts b/webapp/packages/object-viewer-plugin/src/ObjectPage/DBObjectPageService.ts new file mode 100644 index 0000000000..05fbee05ad --- /dev/null +++ b/webapp/packages/object-viewer-plugin/src/ObjectPage/DBObjectPageService.ts @@ -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(); + + @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, 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, page: ObjectPage) => { + tab.handlerState.pageId = page.key; + await this.callHandlerCallback(tab, page => page.onSelect); + } + + async restorePages(tab: ITab): Promise { + for (const page of this.pages.values()) { + if (page.onRestore && !page.onRestore(tab)) { + return false; + } + } + return true; + } + + async closePages(tab: ITab) { + await this.callHandlerCallback(tab, page => page.onClose); + } + + private async callHandlerCallback( + tab: ITab, + 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; + } +} diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPage/DBObjectPageTab.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPage/DBObjectPageTab.tsx new file mode 100644 index 0000000000..46d25860c1 --- /dev/null +++ b/webapp/packages/object-viewer-plugin/src/ObjectPage/DBObjectPageTab.tsx @@ -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; + page: ObjectPage; + onSelect(tab: ITab, 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 ; +}); diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPage/ObjectPage.ts b/webapp/packages/object-viewer-plugin/src/ObjectPage/ObjectPage.ts new file mode 100644 index 0000000000..d7d2116781 --- /dev/null +++ b/webapp/packages/object-viewer-plugin/src/ObjectPage/ObjectPage.ts @@ -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; + page: ObjectPage; + onSelect(): void; + style: Style[]; +} +export type ObjectPageTabComponent = React.FunctionComponent + +export type ObjectPagePanelProps = { + tab: ITab; + page: ObjectPage; +} +export type ObjectPagePanelComponent = React.FunctionComponent + +export type ObjectPageCallback = (tab: ITab) => Promise | void +export type ObjectPageRestoreCallback = (tab: ITab) => Promise | 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; + } +} diff --git a/webapp/packages/object-viewer-plugin/src/FolderContext.ts b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/FolderContext.ts similarity index 100% rename from webapp/packages/object-viewer-plugin/src/FolderContext.ts rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/FolderContext.ts diff --git a/webapp/packages/object-viewer-plugin/src/ObjectFolders.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFolders.tsx similarity index 64% rename from webapp/packages/object-viewer-plugin/src/ObjectFolders.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFolders.tsx index 6d19ae93df..ddbc93209f 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectFolders.tsx +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFolders.tsx @@ -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; } -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))( ); -} +}); diff --git a/webapp/packages/object-viewer-plugin/src/ObjectFoldersController.ts b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersController.ts similarity index 59% rename from webapp/packages/object-viewer-plugin/src/ObjectFoldersController.ts rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersController.ts index 2d33dfb97b..36da2ae885 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectFoldersController.ts +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersController.ts @@ -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 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): 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(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); } } diff --git a/webapp/packages/object-viewer-plugin/src/ObjectFoldersService.ts b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersService.ts similarity index 100% rename from webapp/packages/object-viewer-plugin/src/ObjectFoldersService.ts rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersService.ts diff --git a/webapp/packages/object-viewer-plugin/src/ObjectFoldersTab/ObjectFolderTabModel.ts b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersTab/ObjectFolderTabModel.ts similarity index 100% rename from webapp/packages/object-viewer-plugin/src/ObjectFoldersTab/ObjectFolderTabModel.ts rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersTab/ObjectFolderTabModel.ts diff --git a/webapp/packages/object-viewer-plugin/src/ObjectFoldersTab/ObjectFoldersTabService.ts b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersTab/ObjectFoldersTabService.ts similarity index 100% rename from webapp/packages/object-viewer-plugin/src/ObjectFoldersTab/ObjectFoldersTabService.ts rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersTab/ObjectFoldersTabService.ts diff --git a/webapp/packages/object-viewer-plugin/src/ObjectFoldersTabsContainer/NodePropertiesMixin.ts b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersTabsContainer/NodePropertiesMixin.ts similarity index 100% rename from webapp/packages/object-viewer-plugin/src/ObjectFoldersTabsContainer/NodePropertiesMixin.ts rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersTabsContainer/NodePropertiesMixin.ts diff --git a/webapp/packages/object-viewer-plugin/src/ObjectFoldersTabsContainer/ObjectFoldersTabContainer.ts b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersTabsContainer/ObjectFoldersTabContainer.ts similarity index 100% rename from webapp/packages/object-viewer-plugin/src/ObjectFoldersTabsContainer/ObjectFoldersTabContainer.ts rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersTabsContainer/ObjectFoldersTabContainer.ts diff --git a/webapp/packages/object-viewer-plugin/src/ObjectInfoTab/ObjectInfoTab.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectInfoTab/ObjectInfoTab.tsx similarity index 100% rename from webapp/packages/object-viewer-plugin/src/ObjectInfoTab/ObjectInfoTab.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectInfoTab/ObjectInfoTab.tsx diff --git a/webapp/packages/object-viewer-plugin/src/ObjectInfoTab/ObjectInfoTabModel.ts b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectInfoTab/ObjectInfoTabModel.ts similarity index 100% rename from webapp/packages/object-viewer-plugin/src/ObjectInfoTab/ObjectInfoTabModel.ts rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectInfoTab/ObjectInfoTabModel.ts diff --git a/webapp/packages/object-viewer-plugin/src/ObjectInfoTab/ObjectInfoTabService.ts b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectInfoTab/ObjectInfoTabService.ts similarity index 90% rename from webapp/packages/object-viewer-plugin/src/ObjectInfoTab/ObjectInfoTabService.ts rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectInfoTab/ObjectInfoTabService.ts index f090b55117..3172da2f63 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectInfoTab/ObjectInfoTabService.ts +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectInfoTab/ObjectInfoTabService.ts @@ -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'; diff --git a/webapp/packages/object-viewer-plugin/src/ObjectInfoTab/ObjectProperties.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx similarity index 100% rename from webapp/packages/object-viewer-plugin/src/ObjectInfoTab/ObjectProperties.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx diff --git a/webapp/packages/object-viewer-plugin/src/ObjectInfoTab/ObjectProperty.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperty.tsx similarity index 95% rename from webapp/packages/object-viewer-plugin/src/ObjectInfoTab/ObjectProperty.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperty.tsx index c675e46350..9ef835975b 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectInfoTab/ObjectProperty.tsx +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperty.tsx @@ -14,7 +14,7 @@ import { ObjectPropertyInput, ObjectPropertyProps, } from '../formControls'; -import { filterProperty, matchType } from '../tools'; +import { filterProperty, matchType } from '../helpers'; const propertyStyles = css` property { diff --git a/webapp/packages/object-viewer-plugin/src/ObjectViewer.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx similarity index 76% rename from webapp/packages/object-viewer-plugin/src/ObjectViewer.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx index ce82062a97..9472677a9d 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectViewer.tsx +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx @@ -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 ; @@ -46,7 +43,7 @@ export const ObjectViewer = observer(function ObjectViewer({ return styled(useStyles(viewerStyles))( - + ); }); diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPageService.ts b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPageService.ts new file mode 100644 index 0000000000..fc15cd9a47 --- /dev/null +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPageService.ts @@ -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) { + // 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) { + 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'); + } + } +} diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPageTab.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPageTab.tsx new file mode 100644 index 0000000000..1d27710589 --- /dev/null +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPageTab.tsx @@ -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))( + + + + + ); +}); diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPropertyTable/Header.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/Header.tsx similarity index 100% rename from webapp/packages/object-viewer-plugin/src/ObjectPropertyTable/Header.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/Header.tsx diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPropertyTable/Item.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/Item.tsx similarity index 98% rename from webapp/packages/object-viewer-plugin/src/ObjectPropertyTable/Item.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/Item.tsx index dcc1733d0a..f70f7fa368 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectPropertyTable/Item.tsx +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/Item.tsx @@ -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 { diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPropertyTable/ObjectChildrenPropertyTable.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectChildrenPropertyTable.tsx similarity index 100% rename from webapp/packages/object-viewer-plugin/src/ObjectPropertyTable/ObjectChildrenPropertyTable.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectChildrenPropertyTable.tsx diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPropertyTable/ObjectPropertyTable.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx similarity index 95% rename from webapp/packages/object-viewer-plugin/src/ObjectPropertyTable/ObjectPropertyTable.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx index a801e4d353..3a51618e29 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectPropertyTable/ObjectPropertyTable.tsx +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx @@ -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` diff --git a/webapp/packages/object-viewer-plugin/src/VirtualFolderTab/VirtualFolderTab.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTab.tsx similarity index 100% rename from webapp/packages/object-viewer-plugin/src/VirtualFolderTab/VirtualFolderTab.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTab.tsx diff --git a/webapp/packages/object-viewer-plugin/src/VirtualFolderTab/VirtualFolderTabData.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabData.tsx similarity index 100% rename from webapp/packages/object-viewer-plugin/src/VirtualFolderTab/VirtualFolderTabData.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabData.tsx diff --git a/webapp/packages/object-viewer-plugin/src/VirtualFolderTab/VirtualFolderTabMixin.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabMixin.tsx similarity index 100% rename from webapp/packages/object-viewer-plugin/src/VirtualFolderTab/VirtualFolderTabMixin.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabMixin.tsx diff --git a/webapp/packages/object-viewer-plugin/src/VirtualFolderTab/VirtualFolderTabModel.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabModel.tsx similarity index 100% rename from webapp/packages/object-viewer-plugin/src/VirtualFolderTab/VirtualFolderTabModel.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabModel.tsx diff --git a/webapp/packages/object-viewer-plugin/src/VirtualFolderTab/VirtualFolderTabService.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabService.tsx similarity index 100% rename from webapp/packages/object-viewer-plugin/src/VirtualFolderTab/VirtualFolderTabService.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabService.tsx diff --git a/webapp/packages/object-viewer-plugin/src/formControls/ObjectPropertyCheckbox.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/formControls/ObjectPropertyCheckbox.tsx similarity index 95% rename from webapp/packages/object-viewer-plugin/src/formControls/ObjectPropertyCheckbox.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/formControls/ObjectPropertyCheckbox.tsx index 366a52db8b..5725a6dfb2 100644 --- a/webapp/packages/object-viewer-plugin/src/formControls/ObjectPropertyCheckbox.tsx +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/formControls/ObjectPropertyCheckbox.tsx @@ -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` diff --git a/webapp/packages/object-viewer-plugin/src/formControls/ObjectPropertyInput.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/formControls/ObjectPropertyInput.tsx similarity index 95% rename from webapp/packages/object-viewer-plugin/src/formControls/ObjectPropertyInput.tsx rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/formControls/ObjectPropertyInput.tsx index b6b8ec582b..e1dd9822ed 100644 --- a/webapp/packages/object-viewer-plugin/src/formControls/ObjectPropertyInput.tsx +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/formControls/ObjectPropertyInput.tsx @@ -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` diff --git a/webapp/packages/object-viewer-plugin/src/formControls/ObjectPropertyProps.ts b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/formControls/ObjectPropertyProps.ts similarity index 100% rename from webapp/packages/object-viewer-plugin/src/formControls/ObjectPropertyProps.ts rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/formControls/ObjectPropertyProps.ts diff --git a/webapp/packages/object-viewer-plugin/src/formControls/index.ts b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/formControls/index.ts similarity index 100% rename from webapp/packages/object-viewer-plugin/src/formControls/index.ts rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/formControls/index.ts diff --git a/webapp/packages/object-viewer-plugin/src/tools.ts b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/helpers.ts similarity index 100% rename from webapp/packages/object-viewer-plugin/src/tools.ts rename to webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/helpers.ts diff --git a/webapp/packages/object-viewer-plugin/src/ObjectViewerBootstrap.ts b/webapp/packages/object-viewer-plugin/src/ObjectViewerBootstrap.ts index 40af6c33cf..4c317a6ba4 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectViewerBootstrap.ts +++ b/webapp/packages/object-viewer-plugin/src/ObjectViewerBootstrap.ts @@ -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(); } } diff --git a/webapp/packages/object-viewer-plugin/src/ObjectViewerPanel.tsx b/webapp/packages/object-viewer-plugin/src/ObjectViewerPanel.tsx new file mode 100644 index 0000000000..68efa898fb --- /dev/null +++ b/webapp/packages/object-viewer-plugin/src/ObjectViewerPanel.tsx @@ -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) { + const children = useChildren(tab.handlerState.objectId); + const dbObjectPagesService = useService(DBObjectPageService); + const pages = dbObjectPagesService.orderedPages; + + if (!children?.isLoaded) { + return ; + } + + return styled(useStyles(styles))( + ( + + ))} + style={stylesArray} + > + {pages.map(page => ( + + + + ))} + + ); +}); diff --git a/webapp/packages/object-viewer-plugin/src/ObjectViewerService.ts b/webapp/packages/object-viewer-plugin/src/ObjectViewerService.ts deleted file mode 100644 index 15fa0affd3..0000000000 --- a/webapp/packages/object-viewer-plugin/src/ObjectViewerService.ts +++ /dev/null @@ -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 = 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(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; - } -} diff --git a/webapp/packages/object-viewer-plugin/src/ObjectViewerTab.tsx b/webapp/packages/object-viewer-plugin/src/ObjectViewerTab.tsx new file mode 100644 index 0000000000..df804af32f --- /dev/null +++ b/webapp/packages/object-viewer-plugin/src/ObjectViewerTab.tsx @@ -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) { + const node = useNode(tab.handlerState.objectId); + + return styled(useStyles(...style))( + + + + + ); +}); diff --git a/webapp/packages/object-viewer-plugin/src/ObjectViewerTabService.ts b/webapp/packages/object-viewer-plugin/src/ObjectViewerTabService.ts index 4b59f1a868..3f6e6be54a 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectViewerTabService.ts +++ b/webapp/packages/object-viewer-plugin/src/ObjectViewerTabService.ts @@ -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 = new Map(); + readonly tabHandler: TabHandler 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({ + 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) { + const nodeInfo = this.nodesManagerService + .getNodeContainerInfo(context.handlerState.objectId); + + if (nodeInfo.connectionId) { + // connection node id differs from connection id + return NodeManagerUtils.connectionNodeIdToConnectionId(nodeInfo.connectionId); + } + return ''; + } + + getDBObjectCatalog(context: ITab) { + const nodeInfo = this.nodesManagerService + .getNodeContainerInfo(context.handlerState.objectId); + + if (nodeInfo.catalogId) { + return nodeInfo.catalogId; + } + return ''; + } + + getDBObjectSchema(context: ITab) { + const nodeInfo = this.nodesManagerService + .getNodeContainerInfo(context.handlerState.objectId); + + if (nodeInfo.schemaId) { + return nodeInfo.schemaId; + } + return ''; } 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) { + 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) { + 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) { + await this.dbObjectPageService.closePages(tab); + } + + objectViewerTabContext = async ( + contexts: IContextProvider, + data: INodeNavigationData + ): Promise => { + 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({ + 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) { 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; +export function isObjectViewerTab( + predicate: (tab: ITab) => boolean +): (tab: ITab) => tab is ITab +export function isObjectViewerTab( + tab: ITab | ((tab: ITab) => boolean) +): boolean | ((tab: ITab) => tab is ITab) { + if (typeof tab === 'function') { + const predicate = tab; + return (tab: ITab): tab is ITab => { + const objectViewerTab = tab.handlerId === objectViewerTabHandlerKey; + if (!predicate || !objectViewerTab) { + return objectViewerTab; + } + return predicate(tab); + }; + } + return tab.handlerId === objectViewerTabHandlerKey; +} diff --git a/webapp/packages/object-viewer-plugin/src/Viewer.tsx b/webapp/packages/object-viewer-plugin/src/Viewer.tsx deleted file mode 100644 index 70f8dc06c4..0000000000 --- a/webapp/packages/object-viewer-plugin/src/Viewer.tsx +++ /dev/null @@ -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 ; -} diff --git a/webapp/packages/object-viewer-plugin/src/index.ts b/webapp/packages/object-viewer-plugin/src/index.ts index 893dc6cccd..d230141875 100644 --- a/webapp/packages/object-viewer-plugin/src/index.ts +++ b/webapp/packages/object-viewer-plugin/src/index.ts @@ -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'; diff --git a/webapp/packages/object-viewer-plugin/src/manifest.ts b/webapp/packages/object-viewer-plugin/src/manifest.ts index 5d29a30456..d0ccb6bfa4 100644 --- a/webapp/packages/object-viewer-plugin/src/manifest.ts +++ b/webapp/packages/object-viewer-plugin/src/manifest.ts @@ -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) { diff --git a/webapp/packages/object-viewer-plugin/src/useObjectFolder.ts b/webapp/packages/object-viewer-plugin/src/useObjectFolder.ts index 8110bf8f24..d2e5cc3e47 100644 --- a/webapp/packages/object-viewer-plugin/src/useObjectFolder.ts +++ b/webapp/packages/object-viewer-plugin/src/useObjectFolder.ts @@ -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 }; } diff --git a/webapp/packages/sql-editor/src/ISqlEditorTabState.ts b/webapp/packages/sql-editor/src/ISqlEditorTabState.ts index 4917af7c1e..8afe4a20a4 100644 --- a/webapp/packages/sql-editor/src/ISqlEditorTabState.ts +++ b/webapp/packages/sql-editor/src/ISqlEditorTabState.ts @@ -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[]; diff --git a/webapp/packages/sql-editor/src/SqlEditor/SqlEditorController.tsx b/webapp/packages/sql-editor/src/SqlEditor/SqlEditorController.tsx index 1a6adb4fc7..9f40db0568 100644 --- a/webapp/packages/sql-editor/src/SqlEditor/SqlEditorController.tsx +++ b/webapp/packages/sql-editor/src/SqlEditor/SqlEditorController.tsx @@ -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(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(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(sqlEditorTabHandlerKey)!; - return state.query; + return this.tab.handlerState.query; } - private tab!: Tab; - private editorId!: string; + private tab!: ITab; private editor?: Editor; - constructor(private sqlEditorManager: SqlEditorManagerService, + constructor(private sqlResultTabsService: SqlResultTabsService, private sqlDialectInfoService: SqlDialectInfoService, private sqlEditorService: SqlEditorService) { } - init(tab: Tab) { + init(tab: ITab) { this.tab = tab; - this.editorId = tab.nodeId; } private getExecutingQuery(): string { if (!this.editor) { - const state = this.tab.getHandlerState(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(sqlEditorTabHandlerKey)!; - state.query = query; + this.tab.handlerState.query = query; } } diff --git a/webapp/packages/sql-editor/src/SqlEditor/SqlEditorService.ts b/webapp/packages/sql-editor/src/SqlEditor/SqlEditorService.ts index 5541283495..5202ac5fdc 100644 --- a/webapp/packages/sql-editor/src/SqlEditor/SqlEditorService.ts +++ b/webapp/packages/sql-editor/src/SqlEditor/SqlEditorService.ts @@ -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 { - const state = this.sqlEditorManagerService.getHandlerState(tabId); - if (!state) { - return null; - } + async getAutocomplete( + connectionId: string, + contextId: string, + query: string, + cursor: number + ): Promise { const result = await this.gql.gql.querySqlCompletionProposals({ - connectionId: state.connectionId, - contextId: state.contextId, - query: state.query, + connectionId, + contextId, + query, position: cursor, }); diff --git a/webapp/packages/sql-editor/src/SqlEditorBootstrap.ts b/webapp/packages/sql-editor/src/SqlEditorBootstrap.ts index f506ed5b74..9898a3b684 100644 --- a/webapp/packages/sql-editor/src/SqlEditorBootstrap.ts +++ b/webapp/packages/sql-editor/src/SqlEditorBootstrap.ts @@ -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) => { const node = context.data; const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(node.id); - this.sqlEditorManagerService.openNewEditor(connectionId); + this.sqlEditorNavigatorService.openNewEditor(connectionId); }, }; this.contextMenuService.addMenuItem(this.contextMenuService.getRootMenuToken(), openSqlEditor); diff --git a/webapp/packages/sql-editor/src/SqlEditorManagerService.ts b/webapp/packages/sql-editor/src/SqlEditorManagerService.ts deleted file mode 100644 index 0786225dcc..0000000000 --- a/webapp/packages/sql-editor/src/SqlEditorManagerService.ts +++ /dev/null @@ -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; - - 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( - 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, 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 { - 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 { - 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 { - - 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 { - 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(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 { - - 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 { - // 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); -} diff --git a/webapp/packages/sql-editor/src/SqlEditorNavigatorService.ts b/webapp/packages/sql-editor/src/SqlEditorNavigatorService.ts new file mode 100644 index 0000000000..e0e9818383 --- /dev/null +++ b/webapp/packages/sql-editor/src/SqlEditorNavigatorService.ts @@ -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; + 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( + 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) { + 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, + 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 | 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 { + + 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; +export function isSQLEditorTab( + predicate: (tab: ITab) => boolean +): (tab: ITab) => tab is ITab +export function isSQLEditorTab( + tab: ITab | ((tab: ITab) => boolean) +): boolean | ((tab: ITab) => tab is ITab) { + if (typeof tab === 'function') { + const predicate = tab; + return (tab: ITab): tab is ITab => { + const sqlEditorTab = tab.handlerId === sqlEditorTabHandlerKey; + if (!predicate || !sqlEditorTab) { + return sqlEditorTab; + } + return predicate(tab); + }; + } + return tab.handlerId === sqlEditorTabHandlerKey; +} diff --git a/webapp/packages/sql-editor/src/SqlEditorPanel.tsx b/webapp/packages/sql-editor/src/SqlEditorPanel.tsx new file mode 100644 index 0000000000..4cc7e818a6 --- /dev/null +++ b/webapp/packages/sql-editor/src/SqlEditorPanel.tsx @@ -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) { + return styled(useStyles(splitStyles, splitHorizontalStyles, viewerStyles))( + + + + + + + + + + ); +} diff --git a/webapp/packages/sql-editor/src/SqlEditorTab.tsx b/webapp/packages/sql-editor/src/SqlEditorTab.tsx index 1be01a9a65..05b8e9f2a8 100644 --- a/webapp/packages/sql-editor/src/SqlEditorTab.tsx +++ b/webapp/packages/sql-editor/src/SqlEditorTab.tsx @@ -6,42 +6,26 @@ * you may not use this file except in compliance with the License. */ -import styled, { css } from 'reshadow'; +import styled from 'reshadow'; -import { - splitStyles, Split, ResizerControls, Pane, splitHorizontalStyles, -} from '@dbeaver/core/blocks'; +import { TabHandlerTabProps, ConnectionsManagerService } 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 { SqlEditor } from './SqlEditor'; -import { SqlResultTabs } from './SqlResultTabs/SqlResultTabs'; +import { ISqlEditorTabState } from './ISqlEditorTabState'; +export function SqlEditorTab({ + tab, handler, onSelect, onClose, style, +}: TabHandlerTabProps) { + const connectionsManagerService = useService(ConnectionsManagerService); + const connection = connectionsManagerService.getConnectionById(tab.handlerState.connectionId); + const name = `sql-${tab.handlerState.order}${connection ? ` (${connection.name})` : ''}`; -const viewerStyles = css` - Pane { - composes: theme-typography--body2 from global; - display: flex; - } - SqlEditor { - composes: theme-typography--body1 from global; - } -`; - -type SqlEditorTabProps = { - tabId: string; - handlerId: string; -} - -export function SqlEditorTab({ tabId }: SqlEditorTabProps) { - return styled(useStyles(splitStyles, splitHorizontalStyles, viewerStyles))( - - - - - - - - - + return styled(useStyles(...style))( + + + + ); } diff --git a/webapp/packages/sql-editor/src/SqlEditorTabService.ts b/webapp/packages/sql-editor/src/SqlEditorTabService.ts index afa83cfcac..d2c0e5a62b 100644 --- a/webapp/packages/sql-editor/src/SqlEditorTabService.ts +++ b/webapp/packages/sql-editor/src/SqlEditorTabService.ts @@ -6,34 +6,203 @@ * you may not use this file except in compliance with the License. */ -import { NavigationTabsService, TabHandlerOptions } from '@dbeaver/core/app'; +import { + NavigationTabsService, + TabHandler, + ITab, + ConnectionsManagerService, + connectionProvider, + connectionSetter, + objectSchemaProvider, + objectCatalogProvider, + objectCatalogSetter, + objectSchemaSetter, +} from '@dbeaver/core/app'; import { injectable } from '@dbeaver/core/di'; +import { NotificationService } from '@dbeaver/core/eventsLog'; +import { GraphQLService } from '@dbeaver/core/sdk'; -import { SqlEditorManagerService } from './SqlEditorManagerService'; +import { ISqlEditorTabState, ISqlContextParams } from './ISqlEditorTabState'; +import { SqlDialectInfoService } from './SqlDialectInfoService'; +import { SqlEditorPanel } from './SqlEditorPanel'; import { SqlEditorTab } from './SqlEditorTab'; import { sqlEditorTabHandlerKey } from './sqlEditorTabHandlerKey'; +import { SqlExecutionState } from './SqlExecutionState'; @injectable() export class SqlEditorTabService { + readonly tabHandler: TabHandler constructor(private navigationTabsService: NavigationTabsService, - private sqlEditorManagerService: SqlEditorManagerService) { + private connectionsManagerService: ConnectionsManagerService, + private notificationService: NotificationService, + private gql: GraphQLService, + private sqlDialectInfoService: SqlDialectInfoService) { + + this.tabHandler = this.navigationTabsService + .registerTabHandler({ + key: sqlEditorTabHandlerKey, + navigatorId: 'sql-editor', + priority: 1, + getTabComponent: () => SqlEditorTab, + getPanelComponent: () => SqlEditorPanel, + onRestore: this.handleTabRestore.bind(this), + onClose: this.handleTabClose.bind(this), + extensions: [ + connectionProvider(this.getConnectionId.bind(this)), + objectCatalogProvider(this.getObjectCatalogId.bind(this)), + objectSchemaProvider(this.getObjectSchemaId.bind(this)), + connectionSetter(this.setConnectionId.bind(this)), + objectCatalogSetter(this.setObjectCatalogId.bind(this)), + objectSchemaSetter(this.setObjectSchemaId.bind(this)), + ], + }); } registerTabHandler() { - const tabHandler: TabHandlerOptions = { - key: sqlEditorTabHandlerKey, - name: 'Sql Editor', - icon: '/icons/sql_script.png', - navigatorId: 'sql-editor', - order: 3, - priority: 1, - getTabHandlerComponent: () => SqlEditorTab, - isActive: this.sqlEditorManagerService.isSqlEditorEntity, - onRestore: this.sqlEditorManagerService.handleTabRestore.bind(this.sqlEditorManagerService), - onClose: this.sqlEditorManagerService.handleTabClose.bind(this.sqlEditorManagerService), - }; - this.navigationTabsService.registerTabHandler(tabHandler); } + private async handleTabRestore(tab: ITab): Promise { + + if (typeof tab.handlerState.query !== 'string' + || typeof tab.handlerState.connectionId !== 'string' + || typeof tab.handlerState.contextId !== 'string' + || typeof tab.handlerState.objectCatalogId !== 'string' + || typeof tab.handlerState.order !== 'number' + || !['string', 'undefined'].includes(typeof tab.handlerState.currentResultTabId) + || !Array.isArray(tab.handlerState.resultTabs) + ) { + return false; + } + + // the connection for this editor was closed + if (!this.connectionsManagerService.getConnectionById(tab.handlerState.connectionId)) { + return false; + } + + tab.handlerState.currentResultTabId = ''; + tab.handlerState.resultTabs = []; // clean old results + tab.handlerState.sqlExecutionState = new SqlExecutionState(); + + await this.sqlDialectInfoService.loadSqlDialectInfo(tab.handlerState.connectionId); + + return true; + } + + private getConnectionId(tab: ITab) { + return tab.handlerState.connectionId; + } + + private getObjectCatalogId(tab: ITab) { + return tab.handlerState.objectCatalogId; + } + + private getObjectSchemaId(tab: ITab) { + return tab.handlerState.objectSchemaId; + } + + private async setConnectionId(connectionId: string, tab: ITab) { + try { + // try to create new context first + const context = await this.createSqlContext(connectionId); + // when new context created - destroy old one silently + await this.destroySqlContext(tab.handlerState.connectionId, tab.handlerState.contextId); + + tab.handlerState.connectionId = context.connectionId; + tab.handlerState.contextId = context.contextId; + tab.handlerState.objectCatalogId = context.objectCatalogId; + tab.handlerState.objectSchemaId = context.objectSchemaId; + await this.sqlDialectInfoService.loadSqlDialectInfo(connectionId); + return true; + } catch (exception) { + this.notificationService.logError({ title: 'Failed to change SQL-editor connection' }); + return false; + } + } + + private async setObjectCatalogId(containerId: string, tab: ITab) { + try { + await this.updateSqlContext(tab.handlerState.connectionId, tab.handlerState.contextId, containerId); + tab.handlerState.objectCatalogId = containerId; + return true; + } catch (exception) { + this.notificationService.logError({ title: 'Failed to change SQL-editor schema' }); + return false; + } + } + + private async setObjectSchemaId(containerId: string, tab: ITab) { + try { + await this.updateSqlContext( + tab.handlerState.connectionId, + tab.handlerState.contextId, + tab.handlerState.objectCatalogId, + containerId + ); + tab.handlerState.objectSchemaId = containerId; + return true; + } catch (exception) { + this.notificationService.logError({ title: 'Failed to change SQL-editor schema' }); + return false; + } + } + + private async handleTabClose(tab: ITab) { + await this.destroySqlContext(tab.handlerState.connectionId, tab.handlerState.contextId); + } + + private async destroySqlContext(connectionId: string, contextId: string): Promise { + 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( + connectionId: string, + defaultCatalog?: string, + defaultSchema?: string + ): Promise { + + const response = await this.gql.gql.sqlContextCreate({ + connectionId, + defaultCatalog, + defaultSchema, + }); + return { + contextId: response.context.id, + connectionId, + objectCatalogId: response.context.defaultCatalog, + objectSchemaId: response.context.defaultSchema, + }; + } + + /** + * Update catalog and schema for the exiting sql context in the certain connection + */ + private async updateSqlContext( + connectionId: string, + contextId: string, + defaultCatalog?: string, + defaultSchema?: string + ) { + await this.gql.gql.sqlContextSetDefaults({ + connectionId, + contextId, + defaultCatalog, + defaultSchema, + }); + } } diff --git a/webapp/packages/sql-editor/src/SqlExecutionState.ts b/webapp/packages/sql-editor/src/SqlExecutionState.ts index 53861e3eaf..8dbd67c5ca 100644 --- a/webapp/packages/sql-editor/src/SqlExecutionState.ts +++ b/webapp/packages/sql-editor/src/SqlExecutionState.ts @@ -8,7 +8,7 @@ import { computed, observable } from 'mobx'; -import { EDeferredState } from '@dbeaver/core/src/utils'; +import { EDeferredState } from '@dbeaver/core/utils'; import { SQLQueryExecutionProcess } from './SqlResultTabs/SQLQueryExecutionProcess'; diff --git a/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultTabs.tsx b/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultTabs.tsx index 7d1beb66c4..00f7d21f82 100644 --- a/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultTabs.tsx +++ b/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultTabs.tsx @@ -10,16 +10,15 @@ import { observer } from 'mobx-react'; import { useCallback } from 'react'; import styled, { css } from 'reshadow'; -import { useTabHandlerState } from '@dbeaver/core/app'; +import { ITab as TabClass } from '@dbeaver/core/app'; import { - Tab, TabPanel, TabTitle, TabsBox, TextPlaceholder, Loader, + Tab, TabPanel, TabTitle, TabsBox, TextPlaceholder, Loader } from '@dbeaver/core/blocks'; import { useService } from '@dbeaver/core/di'; import { useStyles, composes } from '@dbeaver/core/theming'; import { ISqlEditorTabState } from '../ISqlEditorTabState'; -import { SqlEditorManagerService } from '../SqlEditorManagerService'; -import { sqlEditorTabHandlerKey } from '../sqlEditorTabHandlerKey'; +import { SqlEditorNavigatorService } from '../SqlEditorNavigatorService'; import { SqlResultPanel } from './SqlResultPanel/SqlResultPanel'; const styles = composes( @@ -45,48 +44,47 @@ const styles = composes( ); type SqlDataResultProps = { - tabId: string; + tab: TabClass; } -export const SqlResultTabs = observer(function SqlDataResult({ tabId: editorId }: SqlDataResultProps) { - const sqlEditorManager = useService(SqlEditorManagerService); - const handlerState = useTabHandlerState(editorId, sqlEditorTabHandlerKey); +export const SqlResultTabs = observer(function SqlDataResult({ tab }: SqlDataResultProps) { + const navigatorService = useService(SqlEditorNavigatorService); const handleOpen = useCallback( - (resultId: string) => sqlEditorManager.openEditorResult(editorId, resultId), - [editorId] + (resultId: string) => navigatorService.openEditorResult(tab.id, resultId), + [] ); const handleClose = useCallback( - (resultId: string) => sqlEditorManager.closeEditorResult(editorId, resultId), - [editorId] + (resultId: string) => navigatorService.closeEditorResult(tab.id, resultId), + [] ); - if (!handlerState?.resultTabs.length) { + if (!tab.handlerState.resultTabs.length) { return Execute query with Ctrl+Enter to see results; } - const currentId = handlerState.currentResultTabId || ''; + const currentId = tab.handlerState.currentResultTabId || ''; return styled(useStyles(styles))( ( + tabs={tab.handlerState.resultTabs.map(result => ( ))} style={[styles]} > - {handlerState?.resultTabs.map(result => ( + {tab.handlerState.resultTabs.map(result => ( ))} diff --git a/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultTabsService.ts b/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultTabsService.ts index 82ddd9cf35..8748ab411d 100644 --- a/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultTabsService.ts +++ b/webapp/packages/sql-editor/src/SqlResultTabs/SqlResultTabsService.ts @@ -27,8 +27,8 @@ export class SqlResultTabsService { const sqlQueryParams: ISqlQueryParams = { connectionId: editorState.connectionId, - catalogId: editorState.catalogId, - schemaId: editorState.schemaId, + objectCatalogId: editorState.objectCatalogId, + objectSchemaId: editorState.objectSchemaId, contextId: editorState.contextId, query, }; diff --git a/webapp/packages/sql-editor/src/manifest.ts b/webapp/packages/sql-editor/src/manifest.ts index b8e4ad245e..c6ed005f71 100644 --- a/webapp/packages/sql-editor/src/manifest.ts +++ b/webapp/packages/sql-editor/src/manifest.ts @@ -11,7 +11,7 @@ import { PluginManifest } from '@dbeaver/core/di'; import { SqlDialectInfoService } from './SqlDialectInfoService'; import { SqlEditorService } from './SqlEditor/SqlEditorService'; import { SqlEditorBootstrap } from './SqlEditorBootstrap'; -import { SqlEditorManagerService } from './SqlEditorManagerService'; +import { SqlEditorNavigatorService } from './SqlEditorNavigatorService'; import { SqlEditorTabService } from './SqlEditorTabService'; import { SqlResultService } from './SqlResultTabs/SqlResultService'; import { SqlResultTabsService } from './SqlResultTabs/SqlResultTabsService'; @@ -23,11 +23,11 @@ export const sqlEditorPluginManifest: PluginManifest = { providers: [ SqlDialectInfoService, - SqlEditorManagerService, + SqlEditorTabService, SqlResultTabsService, SqlResultService, - SqlEditorTabService, SqlEditorService, + SqlEditorNavigatorService, ], initialize(services): void { From 99a530722ba96a362ad7a35aacfee178c3323d91 Mon Sep 17 00:00:00 2001 From: Wroud Date: Sun, 26 Apr 2020 23:34:31 +0300 Subject: [PATCH 7/7] fix: some flaws --- .../extensions/IConnectionProvider.ts | 2 +- .../NodesManager/NodesManagerService.ts | 4 +-- .../src/DataViewerTabService.ts | 7 +++- .../src/DataViewerTableService.ts | 32 ++++++++++++------- .../src/ObjectViewerTabService.ts | 18 +++++------ 5 files changed, 38 insertions(+), 25 deletions(-) diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/extensions/IConnectionProvider.ts b/webapp/packages/core/src/app/shared/ConnectionsManager/extensions/IConnectionProvider.ts index d470d472de..185a54b939 100644 --- a/webapp/packages/core/src/app/shared/ConnectionsManager/extensions/IConnectionProvider.ts +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/extensions/IConnectionProvider.ts @@ -11,7 +11,7 @@ import { createExtension, isExtension, IExtension } from '../../../../extensions const connectionProviderSymbol = Symbol('@extension/ConnectionProvider'); export interface IConnectionProvider { - (context: T): string; + (context: T): string | undefined; } export function connectionProvider(provider: IConnectionProvider) { diff --git a/webapp/packages/core/src/app/shared/NodesManager/NodesManagerService.ts b/webapp/packages/core/src/app/shared/NodesManager/NodesManagerService.ts index 4cc0148719..fee1035547 100644 --- a/webapp/packages/core/src/app/shared/NodesManager/NodesManagerService.ts +++ b/webapp/packages/core/src/app/shared/NodesManager/NodesManagerService.ts @@ -113,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 }; @@ -142,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 => ({ diff --git a/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts b/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts index e51802d3dc..76ccb360c9 100644 --- a/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts +++ b/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts @@ -81,7 +81,12 @@ export class DataViewerTabService { } } - private handleTabSelect(tab: ITab) { + private async handleTabSelect(tab: ITab) { + const objectInfo = await this.nodesManagerService.loadDatabaseObjectInfo(tab.handlerState.objectId); + + if (!this.nodesManagerService.isNodeHasData(objectInfo)) { + return; + } this.dataViewerTableService.createTableModelIfNotExists(tab.handlerState.objectId); } diff --git a/webapp/packages/data-viewer-plugin/src/DataViewerTableService.ts b/webapp/packages/data-viewer-plugin/src/DataViewerTableService.ts index c8c7cfb5e7..dde604c1d9 100644 --- a/webapp/packages/data-viewer-plugin/src/DataViewerTableService.ts +++ b/webapp/packages/data-viewer-plugin/src/DataViewerTableService.ts @@ -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 { + private async createSqlContext( + connectionId: string, + defaultCatalog?: string, + defaultSchema?: string + ): Promise { - 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; } diff --git a/webapp/packages/object-viewer-plugin/src/ObjectViewerTabService.ts b/webapp/packages/object-viewer-plugin/src/ObjectViewerTabService.ts index 3f6e6be54a..463492b970 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectViewerTabService.ts +++ b/webapp/packages/object-viewer-plugin/src/ObjectViewerTabService.ts @@ -66,11 +66,11 @@ export class ObjectViewerTabService { const nodeInfo = this.nodesManagerService .getNodeContainerInfo(context.handlerState.objectId); - if (nodeInfo.connectionId) { - // connection node id differs from connection id - return NodeManagerUtils.connectionNodeIdToConnectionId(nodeInfo.connectionId); + if (!nodeInfo.connectionId) { + return; } - return ''; + // connection node id differs from connection id + return NodeManagerUtils.connectionNodeIdToConnectionId(nodeInfo.connectionId); } getDBObjectCatalog(context: ITab) { @@ -78,19 +78,19 @@ export class ObjectViewerTabService { .getNodeContainerInfo(context.handlerState.objectId); if (nodeInfo.catalogId) { - return nodeInfo.catalogId; + return; } - return ''; + return nodeInfo.catalogId; } getDBObjectSchema(context: ITab) { const nodeInfo = this.nodesManagerService .getNodeContainerInfo(context.handlerState.objectId); - if (nodeInfo.schemaId) { - return nodeInfo.schemaId; + if (!nodeInfo.schemaId) { + return; } - return ''; + return nodeInfo.schemaId; } registerTabHandler() {