From 6ab152db94017aca06e8140d849a17556fa2750d Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 8 Jun 2020 19:01:19 +0300 Subject: [PATCH] feat(core): refresh metadata CB-43 --- .../src/Administration/UsersManagerService.ts | 4 +- .../src/app/NavigationTree/NavigationTree.tsx | 5 +- .../NavigationTreeContextMenuService.ts | 78 +-- .../NavigationNode/NavigationNode.tsx | 67 +-- .../NavigationNodeChildren.tsx | 13 +- .../NavigationTreeNode/NavigationTreeNode.tsx | 10 +- .../TreeNodeMenu/TreeNodeMenu.tsx | 11 +- .../NavigationTreeNode/useNavigationTree.ts | 47 +- .../NavigationTree/NavigationTreeService.ts | 17 +- webapp/packages/core/src/app/index.ts | 14 +- .../ConnectionDialogsService.ts | 10 +- .../ConnectionsManagerService.ts | 16 +- .../src/app/shared/NavigationTabs/ITab.ts | 1 + .../NavigationTabs/NavigationTabsService.ts | 11 +- .../NavigationTabs/TabNavigationContext.ts | 1 + .../shared/NodesManager/DBObjectService.ts | 88 ++++ .../app/shared/NodesManager/EntityTypes.ts | 20 + .../NodesManager/NavNodeManagerService.ts | 489 ++++++++++++++++++ .../app/shared/NodesManager/NodeWithParent.ts | 29 -- .../NodesManager/NodesManagerService.ts | 314 ----------- .../src/app/shared/NodesManager/NodesStore.ts | 90 ---- ...ctInfo.gql => getChildrenDBObjectInfo.gql} | 6 +- ...baseObjectInfo.gql => getDBObjectInfo.gql} | 5 +- .../NodesManager/useDatabaseObjectInfo.ts | 8 +- .../src/app/shared/NodesManager/useNode.ts | 17 +- .../core/src/app/shared/useChildren.ts | 15 +- webapp/packages/core/src/manifest.ts | 5 +- .../core/src/root/PermissionsService.ts | 2 +- .../packages/core/src/root/SessionService.ts | 2 +- .../packages/core/src/sdk/CachedResource.ts | 53 +- webapp/packages/core/src/sdk/sdk.ts | 201 ++++--- .../src/DataExportMenuService.ts | 6 +- .../ExportNotificationController.ts | 6 +- .../src/DataViewerPage/DataViewerTab.tsx | 6 +- .../src/DataViewerTabService.ts | 42 +- .../src/DdlViewer/DdlViewerController.ts | 15 +- .../src/DdlViewerTabService.ts | 24 +- .../src/IObjectViewerTabState.ts | 2 + .../ObjectFoldersTabService.ts | 12 +- .../ObjectPropertiesPagePanel.tsx | 4 +- .../ObjectPropertiesPageService.ts | 6 +- .../ObjectPropertyTable/Item.tsx | 12 +- .../ObjectPropertyTable.tsx | 13 +- .../VirtualFolderTabMixin.tsx | 11 +- .../VirtualFolderTabService.tsx | 11 +- .../src/ObjectViewerPanel.tsx | 4 +- .../src/ObjectViewerTab.tsx | 2 +- .../src/ObjectViewerTabService.ts | 221 ++++---- .../src/useObjectFolder.ts | 12 +- .../sql-editor/src/SqlEditorBootstrap.ts | 22 +- .../src/SqlEditorNavigatorService.ts | 22 +- 51 files changed, 1184 insertions(+), 918 deletions(-) create mode 100644 webapp/packages/core/src/app/shared/NodesManager/DBObjectService.ts create mode 100644 webapp/packages/core/src/app/shared/NodesManager/EntityTypes.ts create mode 100644 webapp/packages/core/src/app/shared/NodesManager/NavNodeManagerService.ts delete mode 100644 webapp/packages/core/src/app/shared/NodesManager/NodeWithParent.ts delete mode 100644 webapp/packages/core/src/app/shared/NodesManager/NodesManagerService.ts delete mode 100644 webapp/packages/core/src/app/shared/NodesManager/NodesStore.ts rename webapp/packages/core/src/app/shared/NodesManager/queries/{queryChildrenDatabaseObjectInfo.gql => getChildrenDBObjectInfo.gql} (65%) rename webapp/packages/core/src/app/shared/NodesManager/queries/{queryDatabaseObjectInfo.gql => getDBObjectInfo.gql} (60%) diff --git a/webapp/packages/authentication/src/Administration/UsersManagerService.ts b/webapp/packages/authentication/src/Administration/UsersManagerService.ts index 4ff6569245..2d8896e110 100644 --- a/webapp/packages/authentication/src/Administration/UsersManagerService.ts +++ b/webapp/packages/authentication/src/Administration/UsersManagerService.ts @@ -39,7 +39,7 @@ export class UsersManagerService { async grantRole(userId: string, roleId: string, update?: boolean) { await this.graphQLService.gql.grantUserRole({ userId, roleId }); if (update) { - await this.users.refresh(userId); + await this.users.refresh(true, userId); } } @@ -50,7 +50,7 @@ export class UsersManagerService { await this.graphQLService.gql.deleteUser({ userId }); if (update) { - await this.users.refresh(userId); + await this.users.refresh(true, userId); } } diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTree.tsx b/webapp/packages/core/src/app/NavigationTree/NavigationTree.tsx index b7ec6f1113..619c414574 100644 --- a/webapp/packages/core/src/app/NavigationTree/NavigationTree.tsx +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTree.tsx @@ -11,6 +11,7 @@ import styled, { css } from 'reshadow'; import { usePermission, EPermission } from '@dbeaver/core/root'; +import { ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeManagerService'; import { useChildren } from '../shared/useChildren'; import { NavigationTreeNode } from './NavigationTreeNode/NavigationTreeNode'; @@ -46,7 +47,7 @@ export const NavigationTree = observer(function NavigationTree() { return null; } - if (!nodeChildren || nodeChildren.children.length === 0) { + if (!nodeChildren.children || nodeChildren.children.length === 0) { return styled(navigationTreeStyles)(
@@ -62,7 +63,7 @@ export const NavigationTree = observer(function NavigationTree() { return styled(navigationTreeStyles)( {nodeChildren.children.map(id => ( - + ))} ); diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeContextMenuService.ts b/webapp/packages/core/src/app/NavigationTree/NavigationTreeContextMenuService.ts index 34624f99b6..a4ce669e59 100644 --- a/webapp/packages/core/src/app/NavigationTree/NavigationTreeContextMenuService.ts +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeContextMenuService.ts @@ -7,63 +7,65 @@ */ import { injectable } from '@dbeaver/core/di'; -import { - ContextMenuService, IContextMenuItem, IMenuContext, IMenuPanel, -} from '@dbeaver/core/dialogs'; +import { ContextMenuService, IMenuPanel } from '@dbeaver/core/dialogs'; -import { NodesManagerService } from '../shared/NodesManager/NodesManagerService'; -import { NodeWithParent } from '../shared/NodesManager/NodeWithParent'; +import { NavNode } from '../shared/NodesManager/EntityTypes'; +import { NavNodeManagerService } from '../shared/NodesManager/NavNodeManagerService'; @injectable() export class NavigationTreeContextMenuService { static nodeContextType = 'NodeWithParent'; private static menuToken = 'navTreeMenu'; - constructor(private contextMenuService: ContextMenuService, - private nodesManagerService: NodesManagerService) { - } + constructor( + private contextMenuService: ContextMenuService, + private navNodeManagerService: NavNodeManagerService + ) { } getMenuToken() { return NavigationTreeContextMenuService.menuToken; } - constructMenuWithContext(node: NodeWithParent): IMenuPanel { - const context: IMenuContext = { + constructMenuWithContext(node: NavNode): IMenuPanel { + return this.contextMenuService.createContextMenu({ menuId: this.getMenuToken(), contextId: node.id, contextType: NavigationTreeContextMenuService.nodeContextType, data: node, - }; - return this.contextMenuService.createContextMenu(context); + }); } registerMenuItems() { - const openNodeTab: IContextMenuItem = { - id: 'openNodeTab', - isPresent(context) { - return context.contextType === NavigationTreeContextMenuService.nodeContextType; - }, - order: 1, - title: 'app_navigationTree_openNodeTab', - onClick: (context: IMenuContext) => { - const node = context.data; - this.nodesManagerService.navToNode(node.id); - }, - }; - this.contextMenuService.addMenuItem(this.contextMenuService.getRootMenuToken(), openNodeTab); + this.contextMenuService.addMenuItem( + this.contextMenuService.getRootMenuToken(), + { + id: 'openNodeTab', + isPresent(context) { + return context.contextType === NavigationTreeContextMenuService.nodeContextType; + }, + order: 1, + title: 'app_navigationTree_openNodeTab', + onClick: (context) => { + const node = context.data; + this.navNodeManagerService.navToNode(node.id, node.parentId); + }, + } + ); - const refreshNode: IContextMenuItem = { - id: 'refreshNode', - isPresent(context) { - return context.contextType === NavigationTreeContextMenuService.nodeContextType; - }, - order: Number.MAX_SAFE_INTEGER, - title: 'app_navigationTree_refreshNode', - onClick: (context: IMenuContext) => { - const node = context.data; - this.nodesManagerService.refreshNode(node.id); - }, - }; - this.contextMenuService.addMenuItem(this.contextMenuService.getRootMenuToken(), refreshNode); + this.contextMenuService.addMenuItem( + this.contextMenuService.getRootMenuToken(), + { + id: 'refreshNode', + isPresent(context) { + return context.contextType === NavigationTreeContextMenuService.nodeContextType; + }, + order: Number.MAX_SAFE_INTEGER, + title: 'app_navigationTree_refreshNode', + onClick: (context) => { + const node = context.data; + this.navNodeManagerService.refresh(node.id); + }, + } + ); } } diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNode.tsx b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNode.tsx index 9c99fae994..787a9d7311 100644 --- a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNode.tsx +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNode.tsx @@ -21,13 +21,13 @@ type NodeProps = PropsWithChildren<{ title?: string; icon?: string; isExpanded: boolean; - isLoaded?: boolean; + isLoading: boolean; isSelected: boolean; portal?: JSX.Element; isExpandable?: boolean; - onExpand?: (id: string) => void; - onDoubleClick?: (id: string) => void; - onClick?: (id: string, isMultiple?: boolean) => void; + onExpand?: () => void; + onDoubleClick?: () => void; + onClick?: (isMultiple?: boolean) => void; }> const KEYCODE = { @@ -36,23 +36,19 @@ const KEYCODE = { DOWN: 40, }; -export function NavigationNode( - props: NodeProps -) { - const { - id, - title, - icon, - children, - isLoaded, - isExpanded, - isSelected, - portal, - isExpandable, - onExpand, - onDoubleClick, - onClick, - } = props; +export function NavigationNode({ + title, + icon, + children, + isLoading, + isExpanded, + isSelected, + portal, + isExpandable, + onExpand, + onDoubleClick, + onClick, +}: NodeProps) { const styles = useStyles(navigationNodeStyles); @@ -60,28 +56,19 @@ export function NavigationNode( (e: MouseEvent) => { e.stopPropagation(); if (onExpand) { - onExpand(id); + onExpand(); } }, - [id, onExpand] - ); - - const handleDoubleClick = useCallback( - (e: MouseEvent) => { - if (onDoubleClick) { - onDoubleClick(id); - } - }, - [id, onDoubleClick] + [onExpand] ); const handleClick = useCallback( (e: MouseEvent) => { if (onClick) { - onClick(id, e.ctrlKey); + onClick(e.ctrlKey); } }, - [id, onClick] + [onClick] ); const handleEnter = useCallback( @@ -89,29 +76,29 @@ export function NavigationNode( switch (event.keyCode) { case KEYCODE.ENTER: if (onClick) { - onClick(id, event.ctrlKey); + onClick(event.ctrlKey); } break; } return true; }, - [id, onClick] + [onClick] ); return styled(styles)( <> - + {title} diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNodeChildren.tsx b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNodeChildren.tsx index ade15af012..30c6396598 100644 --- a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNodeChildren.tsx +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNodeChildren.tsx @@ -13,24 +13,27 @@ import { useChildren } from '../../shared/useChildren'; type NavigationNodeChildrenProps = PropsWithChildren<{ parentId: string; - component: React.ElementType<{id: string}>; + component: React.ElementType<{ + id: string; + parentId: string; + }>; }> export const NavigationNodeChildren = observer(function NavigationNodeChildren({ parentId, component, }: NavigationNodeChildrenProps) { - const nodeChildren = useChildren(parentId); + const { children } = useChildren(parentId); - if (!nodeChildren?.isLoaded) { + if (!children) { return null; } const NavigationNodeChild = component; return ( <> - {nodeChildren.children!.map(id => ( - + {children.map(id => ( + ))} ); diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationTreeNode.tsx b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationTreeNode.tsx index 5f0e962a62..593a7c3d73 100644 --- a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationTreeNode.tsx +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationTreeNode.tsx @@ -15,12 +15,14 @@ import { useNavigationTree } from './useNavigationTree'; type NavigationTreeNodeProps = { id: string; + parentId: string; } export const NavigationTreeNode = observer(function NavigationTreeNodeFn({ id, + parentId, }: NavigationTreeNodeProps) { - const node = useNavigationTree(id); + const node = useNavigationTree(id, parentId); if (!node) { return null; @@ -33,12 +35,12 @@ export const NavigationTreeNode = observer(function NavigationTreeNodeFn({ icon={node.icon} isSelected={node.isSelected} isExpanded={node.isExpanded} - isLoaded={node.isLoaded} - isExpandable={node.hasChildren && node.isExpandable} + isLoading={node.isLoading} + isExpandable={node.isExpandable} onExpand={node.handleExpand} onClick={node.handleSelect} onDoubleClick={node.handleDoubleClick} - portal={} + portal={} > navigationTreeContextMenuService.constructMenuWithContext(node), - [nodeId] + [node] ); const isHidden = useMemo( () => computed(() => !menuPanel.menuItems.length || menuPanel.menuItems.every(item => item.isHidden)), - [nodeId] + [menuPanel] ); if (isHidden.get()) { diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/useNavigationTree.ts b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/useNavigationTree.ts index 12cae46442..47637a330a 100644 --- a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/useNavigationTree.ts +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/useNavigationTree.ts @@ -10,49 +10,52 @@ import { useState, useCallback, useEffect } from 'react'; import { useService } from '@dbeaver/core/di'; +import { NavNode } from '../../shared/NodesManager/EntityTypes'; import { EObjectFeature } from '../../shared/NodesManager/EObjectFeature'; -import { NodesManagerService } from '../../shared/NodesManager/NodesManagerService'; -import { NodeWithParent } from '../../shared/NodesManager/NodeWithParent'; +import { NavNodeManagerService } from '../../shared/NodesManager/NavNodeManagerService'; import { useNode } from '../../shared/NodesManager/useNode'; import { useChildren } from '../../shared/useChildren'; import { NavigationTreeService } from '../NavigationTreeService'; -export function useNavigationTree(nodeId: string) { +export function useNavigationTree(nodeId: string, parentId: string) { const navigationTreeService = useService(NavigationTreeService); - const nodesManagerService = useService(NodesManagerService); + const navNodeManagerService = useService(NavNodeManagerService); const [isExpanded, switchExpand] = useState(false); const [isSelected, switchSelect] = useState(false); - const node = useNode(nodeId); + const { node, isLoaded: nodeLoaded } = useNode(nodeId); const children = useChildren(nodeId); if (!node) { return undefined; } - const isLoaded = children?.isLoaded; - const isExpandable = isExpandableFilter(node) && (!children?.isLoaded || children.children.length > 0); + const isLoaded = children.isLoaded; + const isExpandable = isExpandableFilter(node) && (!isLoaded || children.children!.length > 0); const handleDoubleClick = useCallback( - (nodeId: string) => nodesManagerService.navToNode(nodeId), - [nodesManagerService] + () => navNodeManagerService.navToNode(nodeId, parentId), + [navNodeManagerService, nodeId, parentId] ); const handleExpand = useCallback( - (id: string) => { + async () => { if (!isExpanded) { - navigationTreeService.loadNestedNodes(id).then(state => !state && switchExpand(false)); + const state = await navigationTreeService.loadNestedNodes(nodeId); + if (!state) { + switchExpand(false); + } } switchExpand(!isExpanded); }, - [isExpanded] + [isExpanded, nodeId] ); const handleSelect = useCallback( - (id: string, isMultiple?: boolean) => { - switchSelect(navigationTreeService.selectNode(id, isMultiple)); + (isMultiple?: boolean) => { + switchSelect(navigationTreeService.selectNode(nodeId, isMultiple)); }, - [isSelected] + [isSelected, nodeId] ); const { @@ -65,17 +68,25 @@ export function useNavigationTree(nodeId: string) { } }, [isExpandable && hasChildren]); + useEffect(() => { + if (isExpanded && !children.isLoaded && !children.isLoading && !!children.children && nodeLoaded) { + navigationTreeService.loadNestedNodes(nodeId); + } + }, [isExpanded, children.isLoaded, children.isLoading, children.children, nodeLoaded, nodeId]); + // Here we subscribe to selected nodes if current node selected (mobx) - if (isSelected && !navigationTreeService.selectedNodes.includes(node.id)) { + if (isSelected && !navigationTreeService.selectedNodes.includes(nodeId)) { switchSelect(false); } return { name, + node, nodeType, icon, isExpanded, isLoaded, + isLoading: children.isLoading, isExpandable, isSelected, hasChildren, @@ -85,6 +96,6 @@ export function useNavigationTree(nodeId: string) { }; } -export function isExpandableFilter(node: NodeWithParent) { - return !node.object?.features?.find(feature => feature === EObjectFeature.entity); +export function isExpandableFilter(node: NavNode) { + return node.hasChildren && !node.objectFeatures.includes(EObjectFeature.entity); } diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeService.ts b/webapp/packages/core/src/app/NavigationTree/NavigationTreeService.ts index b5563effe6..c68fb25492 100644 --- a/webapp/packages/core/src/app/NavigationTree/NavigationTreeService.ts +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeService.ts @@ -11,23 +11,22 @@ import { observable } from 'mobx'; import { injectable } from '@dbeaver/core/di'; import { NotificationService } from '@dbeaver/core/eventsLog'; -import { NodesManagerService } from '../shared/NodesManager/NodesManagerService'; +import { NavNodeManagerService, ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeManagerService'; @injectable() export class NavigationTreeService { selectedNodes = observable.array([]); - constructor(private nodesManagerService: NodesManagerService, - private notificationService: NotificationService) { - } + constructor( + private NavNodeManagerService: NavNodeManagerService, + private notificationService: NotificationService + ) { } - async loadNestedNodes(id = '/') { + async loadNestedNodes(id = ROOT_NODE_PATH) { try { - const children = await this.nodesManagerService.loadChildren(id); - if (children) { - return true; - } + await this.NavNodeManagerService.loadTree(id); + return true; } catch (exception) { this.notificationService.logException(exception, `Can't load tree node: ${id}`); } diff --git a/webapp/packages/core/src/app/index.ts b/webapp/packages/core/src/app/index.ts index 306e139230..e3d6387a31 100644 --- a/webapp/packages/core/src/app/index.ts +++ b/webapp/packages/core/src/app/index.ts @@ -15,15 +15,16 @@ 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/NodesManager/DBObjectService'; +export * from './shared/NodesManager/NavNodeManagerService'; export * from './shared/NodesManager/NodeManagerUtils'; 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'; export * from './shared/ToolsPanel/LogViewTab/LogViewerMenuService'; export * from './shared/ToolsPanel/LogViewTab/LogViewerService'; +export * from './NavigationTree/NavigationTreeService'; +export * from './NavigationTree/NavigationTreeContextMenuService'; export * from './Notifications/NotificationsItem/Snackbar/styles'; export * from './Notifications/NotificationsItem/Snackbar/NotificationMark'; @@ -33,7 +34,8 @@ export * from './shared/NavigationTabs/TabHandler'; export * from './shared/Navigation/NavigationContext'; export * from './shared/TabEntity/TabEntity'; // Enums -// export * from ''; +export * from './shared/NodesManager/ENodeFeature'; +export * from './shared/NodesManager/EObjectFeature'; // hooks export * from './shared/useChildren'; @@ -50,10 +52,8 @@ export * from './TopNavBar/Logo'; // Interfaces export * from './shared/Navigation/INavigator'; -export * from './shared/NodesManager/ENodeFeature'; -export * from './shared/NodesManager/EObjectFeature'; export * from './shared/NodesManager/NodeManagerUtils'; -export * from './shared/NodesManager/NodeWithParent'; +export * from './shared/NodesManager/EntityTypes'; // tab entity export * from './shared/TabEntity/TabContainerEntity'; diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionDialogsService.ts b/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionDialogsService.ts index aa760a237f..baeb175ad1 100644 --- a/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionDialogsService.ts +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionDialogsService.ts @@ -15,9 +15,9 @@ import { import { NavigationTreeContextMenuService } from '../../NavigationTree/NavigationTreeContextMenuService'; import { EMainMenu, MainMenuService } from '../../TopNavBar/MainMenu/MainMenuService'; +import { NavNode } from '../NodesManager/EntityTypes'; import { EObjectFeature } from '../NodesManager/EObjectFeature'; import { NodeManagerUtils } from '../NodesManager/NodeManagerUtils'; -import { NodeWithParent } from '../NodesManager/NodeWithParent'; import { ConnectionsManagerService } from './ConnectionsManagerService'; @@ -52,20 +52,20 @@ export class ConnectionDialogsService { } ); - const closeConnection: IContextMenuItem = { + const closeConnection: IContextMenuItem = { id: 'closeConnection', isPresent(context) { return context.contextType === NavigationTreeContextMenuService.nodeContextType - && !!context.data.object?.features?.includes(EObjectFeature.dataSource); + && !!context.data.objectFeatures.includes(EObjectFeature.dataSource); }, title: 'Disconnect', - onClick: (context: IMenuContext) => { + onClick: (context: IMenuContext) => { const node = context.data; const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(node.id); this.connectionsManagerService.closeConnectionAsync(connectionId); }, }; - this.contextMenuService.addMenuItem(this.contextMenuService.getRootMenuToken(), closeConnection); + this.contextMenuService.addMenuItem(this.contextMenuService.getRootMenuToken(), closeConnection); } } diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionsManagerService.ts b/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionsManagerService.ts index 41b55991ed..1f1fe2208d 100644 --- a/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionsManagerService.ts +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionsManagerService.ts @@ -20,7 +20,7 @@ import { DatabaseObjectInfo, } from '@dbeaver/core/sdk'; -import { NodesManagerService } from '../NodesManager/NodesManagerService'; +import { NavNodeManagerService } from '../NodesManager/NavNodeManagerService'; export type DBDriver = Pick< DriverInfo, @@ -67,7 +67,7 @@ export class ConnectionsManagerService { constructor( private graphQLService: GraphQLService, - private nodesManagerService: NodesManagerService, + private navNodeManagerService: NavNodeManagerService, private sessionService: SessionService ) { this.sessionService.onUpdate.subscribe(this.restoreConnections.bind(this)); @@ -81,10 +81,10 @@ export class ConnectionsManagerService { return this.dbDrivers.load(); } - addOpenedConnection(connection: Connection) { + async addOpenedConnection(connection: Connection) { this.connectionsMap.set(connection.id, connection); this.onOpenConnection.next(connection); - this.nodesManagerService.updateRootChildren(); // Update connections list, probably here we must also request node info and add it to nodes manager + await this.navNodeManagerService.updateRootChildren(); // Update connections list, probably here we must also request node info and add it to nodes manager } getConnectionById(connectionId: string): Connection | undefined { @@ -113,7 +113,7 @@ export class ConnectionsManagerService { for (const connection of this.connections) { await this.closeConnectionAsync(connection.id, true); } - await this.nodesManagerService.updateRootChildren(); + await this.navNodeManagerService.updateRootChildren(); } async closeConnectionAsync(id: string, skipNodesRefresh?: boolean): Promise { @@ -122,7 +122,7 @@ export class ConnectionsManagerService { this.connectionsMap.delete(id); if (!skipNodesRefresh) { - await this.nodesManagerService.updateRootChildren(); // Update connections list, probably here we must just remove nodes from nodes manager + await this.navNodeManagerService.updateRootChildren(); // Update connections list, probably here we must just remove nodes from nodes manager } } @@ -132,7 +132,7 @@ export class ConnectionsManagerService { } private async afterConnectionClose(id: string) { - await this.nodesManagerService.closeConnection(id); + await this.navNodeManagerService.remove(id); this.onCloseConnection.next(id); } @@ -154,7 +154,7 @@ export class ConnectionsManagerService { this.connectionsMap.delete(connection.id); } - await this.nodesManagerService.updateRootChildren(); + await this.navNodeManagerService.updateRootChildren(); } private isObjectContainersLoaded( diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/ITab.ts b/webapp/packages/core/src/app/shared/NavigationTabs/ITab.ts index d336f9f112..47429dbac7 100644 --- a/webapp/packages/core/src/app/shared/NavigationTabs/ITab.ts +++ b/webapp/packages/core/src/app/shared/NavigationTabs/ITab.ts @@ -14,4 +14,5 @@ export interface ITabOptions { export interface ITab extends ITabOptions { id: string; + restored: boolean; } diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsService.ts b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsService.ts index 85d5fb9562..0a64fec389 100644 --- a/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsService.ts +++ b/webapp/packages/core/src/app/shared/NavigationTabs/NavigationTabsService.ts @@ -67,6 +67,7 @@ export class NavigationTabsService { typeof value.id === 'string' && typeof value.handlerId === 'string' ) { + value.restored = false; map[key] = value; } } @@ -86,7 +87,7 @@ export class NavigationTabsService { } } - @action selectTab(tabId: string, skipHandlers?: boolean) { + @action async selectTab(tabId: string, skipHandlers?: boolean) { const tab = this.tabsMap.get(tabId); if (!tab) { return; @@ -99,7 +100,7 @@ export class NavigationTabsService { } if (!skipHandlers) { - this.callHandlerCallback(tab, handler => handler.onSelect); + await this.callHandlerCallback(tab, handler => handler.onSelect); } this.tabSelectSubject.next(tab); @@ -177,7 +178,6 @@ export class NavigationTabsService { // must be executed with low priority, because this call runs many requests to backend and blocks others async restoreTabs() { const removedTabs: string[] = []; - const restoreTasks: Promise[] = []; const session = await this.sessionService.session.load(); for (const tabId of this.state.tabs) { @@ -192,10 +192,9 @@ export class NavigationTabsService { continue; } - restoreTasks.push(this.restoreTab(tab, removedTabs)); + await this.restoreTab(tab, removedTabs); } - await Promise.all(restoreTasks); if (removedTabs.length > 0) { this.notificationService.logError({ title: 'Some tabs cannot be restored properly', isSilent: true }); @@ -237,6 +236,8 @@ export class NavigationTabsService { if (restoreFail) { removedTabs.push(tab.id); + } else { + tab.restored = true; } } } diff --git a/webapp/packages/core/src/app/shared/NavigationTabs/TabNavigationContext.ts b/webapp/packages/core/src/app/shared/NavigationTabs/TabNavigationContext.ts index 7bf1807251..1e9fa6e8f5 100644 --- a/webapp/packages/core/src/app/shared/NavigationTabs/TabNavigationContext.ts +++ b/webapp/packages/core/src/app/shared/NavigationTabs/TabNavigationContext.ts @@ -40,6 +40,7 @@ export class TabNavigationContext implements ITabNavigationContext { openNewTab(options: ITabOptions): ITab { this._tab = observable({ id: uuid(), + restored: true, ...options, }); this._isNewlyCreated = true; diff --git a/webapp/packages/core/src/app/shared/NodesManager/DBObjectService.ts b/webapp/packages/core/src/app/shared/NodesManager/DBObjectService.ts new file mode 100644 index 0000000000..96cd3d1e0e --- /dev/null +++ b/webapp/packages/core/src/app/shared/NodesManager/DBObjectService.ts @@ -0,0 +1,88 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { injectable } from '@dbeaver/core/di'; +import { GraphQLService, CachedResource } from '@dbeaver/core/sdk'; +import { MetadataMap } from '@dbeaver/core/utils'; + +import { DBObject } from './EntityTypes'; + +export interface IDBObjectParams { + navNodeId: string[]; + remove?: boolean; +} + +interface IDBObjectMetadata { + loaded: boolean; + loading: boolean; +} + +@injectable() +export class DBObjectService { + readonly dbObject = new CachedResource( + new Map(), + this.loadDBObject.bind(this), + (_, metadata, { navNodeId }) => navNodeId.every(navNodeId => metadata.get(navNodeId).loaded), + new MetadataMap(() => ({ loaded: false, loading: false })), + (_, metadata, { navNodeId }) => navNodeId.some(navNodeId => metadata.get(navNodeId).loading) + ) + + constructor(private graphQLService: GraphQLService) { } + + getDBObject(navNodeId: string) { + return this.dbObject.data.get(navNodeId); + } + + async load(navNodeId: string): Promise< DBObject> + async load(navNodeId: string[]): Promise + async load(navNodeId: string | string[]): Promise { + const dbObject = await this.dbObject.load({ navNodeId: Array.isArray(navNodeId) ? navNodeId : [navNodeId] }); + + if (!Array.isArray(navNodeId)) { + return dbObject.get(navNodeId)!; + } + return navNodeId.map(navNodeId => dbObject.get(navNodeId)!); + } + + async remove(navNodeId: string[]) { + await this.dbObject.refresh(true, { navNodeId, remove: true }); + } + + private async loadDBObject( + dbObject: Map, + metadata: MetadataMap, + load: boolean, + data: IDBObjectParams + ) { + for (const navNodeId of data.navNodeId) { + if (data.remove) { + dbObject.delete(navNodeId); + metadata.delete(navNodeId); + } else { + const itemMetadata = metadata.get(navNodeId); + + itemMetadata.loaded = false; + if (load) { + itemMetadata.loading = true; + + try { + const { objectInfo: { object } } = await this.graphQLService.gql.getDBObjectInfo({ + navNodeId, + }); + + dbObject.set(navNodeId, { navNodeId, ...object } as DBObject); + itemMetadata.loaded = true; + } finally { + itemMetadata.loading = false; + } + } + } + } + return dbObject; + } +} diff --git a/webapp/packages/core/src/app/shared/NodesManager/EntityTypes.ts b/webapp/packages/core/src/app/shared/NodesManager/EntityTypes.ts new file mode 100644 index 0000000000..9e82e43168 --- /dev/null +++ b/webapp/packages/core/src/app/shared/NodesManager/EntityTypes.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 { DatabaseObjectInfo, NavigatorNodeInfo } from '@dbeaver/core/sdk'; + +export type NavNodeInfo = NavigatorNodeInfo; + +export type NavNode = Omit & { + parentId: string; + objectFeatures: string[]; +} + +export type DBObject = DatabaseObjectInfo & { + navNodeId: string; +} diff --git a/webapp/packages/core/src/app/shared/NodesManager/NavNodeManagerService.ts b/webapp/packages/core/src/app/shared/NodesManager/NavNodeManagerService.ts new file mode 100644 index 0000000000..f94115a74f --- /dev/null +++ b/webapp/packages/core/src/app/shared/NodesManager/NavNodeManagerService.ts @@ -0,0 +1,489 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { injectable } from '@dbeaver/core/di'; +import { PermissionsService, EPermission } from '@dbeaver/core/root'; +import { GraphQLService, CachedResource } from '@dbeaver/core/sdk'; +import { MetadataMap } from '@dbeaver/core/utils'; + +import { INavigator } from '../Navigation/INavigator'; +import { IContextProvider } from '../Navigation/NavigationContext'; +import { NavigationService } from '../Navigation/NavigationService'; +import { DBObjectService } from './DBObjectService'; +import { ENodeFeature } from './ENodeFeature'; +import { NavNodeInfo, NavNode } from './EntityTypes'; +import { EObjectFeature } from './EObjectFeature'; +import { NodeManagerUtils } from './NodeManagerUtils'; + +export enum NavigationType { + open, + closeConnection +} + +export interface NavNodeKey { + nodeId: string; + parentId: string; +} + +export interface NavNodeValue { + node: NavNodeInfo; + parentId: string; +} + +export interface INodeContainerInfo { + connectionId?: string; + catalogId?: string; + schemaId?: string; +} + +export interface INavNodePath { + nodes: NavNodeKey[]; + remove?: boolean; + navNodeId?: never; + nodesValue?: never; +} + +export interface INavNodeData { + nodesValue: NavNodeValue[]; + navNodeId?: never; + nodes?: never; + remove?: never; +} + +export interface INavNodeId { + navNodeId: string[]; + remove?: boolean; + nodes?: never; + nodesValue?: never; +} + +export interface INodeNavigationContext { + type: NavigationType; + nodeId: string; + parentId: string; + folderId: string; + name?: string; + icon?: string; + getParents: () => string[]; + loadParents: (parents: string[]) => Promise; +} + +export interface INodeNavigationData { + type: NavigationType; + nodeId: string; + parentId: string; + folderId?: string; +} + +interface INavNodeMetadata { + loaded: boolean; + loading: boolean; +} + +interface INavTreeMetadata { + loaded: boolean; + loading: boolean; +} + +export const ROOT_NODE_PATH = '/'; + +@injectable() +export class NavNodeManagerService { + readonly navNode = new CachedResource( + new Map(), + this.loadNavNodeInfo.bind(this), + (_, metadata, data) => { + if (data.nodesValue) { + return false; + } + + const nodesId = data.navNodeId + ? data.navNodeId + : data.nodes.map(node => node.nodeId); + + return nodesId.every(navNodeId => metadata.get(navNodeId).loaded); + }, + new MetadataMap(() => ({ loaded: false, loading: false })), + (_, metadata, data) => { + if (data.nodesValue) { + return false; + } + + const nodesId = data.navNodeId + ? data.navNodeId + : data.nodes.map(node => node.nodeId); + + return nodesId.some(navNodeId => metadata.get(navNodeId).loading); + } + ); + readonly navTree = new CachedResource( + new Map(), + this.loadNavTree.bind(this), + (_, metadata, parentId) => metadata.get(parentId).loaded, + new MetadataMap(() => ({ loaded: false, loading: false })), + (_, metadata, parentId) => metadata.get(parentId).loading + ) + readonly navigator!: INavigator; + + constructor( + private graphQLService: GraphQLService, + private navigationService: NavigationService, + private permissionsService: PermissionsService, + private dbObjectService: DBObjectService, + ) { + + this.navigator = this.navigationService.createNavigator( + data => data.nodeId, + this.navigateHandler.bind(this), + { + type: NavigationType.open, + nodeId: ROOT_NODE_PATH, + parentId: ROOT_NODE_PATH, + } + ); + } + + async navToNode(nodeId: string, parentId: string, folderId?: string) { + await this.navigator.navigateTo({ + type: NavigationType.open, + nodeId, + parentId, + folderId, + }); + } + + async refresh(navNodeId: string) { + await this.graphQLService.gql.navRefreshNode({ + nodePath: navNodeId, + }); + await this.navTree.refresh(true, navNodeId, false); + } + + async updateRootChildren() { + if (!await this.permissionsService.hasAsync(EPermission.public)) { + return; + } + await this.navTree.refresh(true, ROOT_NODE_PATH, false); + } + + getTree(navNodeId: string): string[] | undefined + getTree(navNodeKey: NavNodeKey): string[] | undefined + getTree(navNodeKey: NavNodeKey[]): (string[] | undefined)[] + getTree(navNodeId: string | NavNodeKey | NavNodeKey[]) { + if (typeof navNodeId === 'string') { + return this.navTree.data.get(navNodeId); + } + + if (Array.isArray(navNodeId)) { + return navNodeId.map(node => this.navTree.data.get(node.nodeId)); + } + + return this.navTree.data.get(navNodeId.nodeId); + } + + async loadTree(navNodeId: string) { + await this.navTree.load(navNodeId, false); + return this.getTree(navNodeId)!; + } + + getNode(navNodeId: string): NavNode | undefined + getNode(navNodeKey: NavNodeKey): NavNode | undefined + getNode(navNodeKey: NavNodeKey[]): (NavNode | undefined)[] + getNode(navNodeId: string | NavNodeKey | NavNodeKey[]) { + if (typeof navNodeId === 'string') { + return this.navNode.data.get(navNodeId); + } + + if (Array.isArray(navNodeId)) { + return navNodeId.map(node => this.navNode.data.get(node.nodeId)); + } + + return this.navNode.data.get(navNodeId.nodeId); + } + + getNestedChildren(navNode: string | string[]) { + const nestedChildren: string[] = []; + let prevChildren: string[]; + if (Array.isArray(navNode)) { + prevChildren = navNode.concat(); + nestedChildren.push(...navNode); + } else { + prevChildren = (this.getTree(navNode)?.concat() || []); + nestedChildren.push(...prevChildren); + } + + while (prevChildren.length) { + const nodeKey = prevChildren.shift()!; + const children = this.getTree(nodeKey) || []; + prevChildren.push(...children); + nestedChildren.push(...children); + } + + return nestedChildren; + } + + async loadNode(node: NavNodeKey): Promise + async loadNode(...nodes: NavNodeKey[]): Promise + async loadNode(...nodes: NavNodeKey[]) { + await this.navNode.load({ nodes }); + + if (nodes.length === 1) { + return this.getNode(nodes[0])!; + } + + return this.getNode(nodes); + } + + getParent(node: NavNode) { + return this.navNode.data.get(node.parentId); + } + + isNodeHasData(node?: string | NavNode) { + if (typeof node === 'string') { + node = this.getNode(node); + } + + if (!node || !node.objectFeatures) { + return false; + } + + return node.objectFeatures.includes(ENodeFeature.dataContainer) + || node.objectFeatures.includes(ENodeFeature.container); + } + + getNodeContainerInfo(nodeId: string): INodeContainerInfo { + const initial: INodeContainerInfo = {}; + + const scanParents = (res: INodeContainerInfo, + nodeId?: string): INodeContainerInfo => { + if (!nodeId) { + return res; + } + const object = this.getNode(nodeId); + if (!object) { + return res; + } + if (object.objectFeatures.includes(EObjectFeature.dataSource)) { + res.connectionId = object.id; + } + if (object.objectFeatures.includes(EObjectFeature.catalog)) { + res.catalogId = object.name; // note that catalogId is node name + } + if (object.objectFeatures.includes(EObjectFeature.schema)) { + res.schemaId = object.name; // note that schemaId is node name + } + return scanParents(res, object.parentId); + }; + + return scanParents(initial, nodeId); + } + + async remove(path = ROOT_NODE_PATH) { + await this.navTree.refresh(true, path, true); + } + + navigationNavNodeContext = async ( + contexts: IContextProvider, + data: INodeNavigationData + ): Promise => { + let nodeId = data.nodeId; + let parentId = data.parentId; + let folderId = ''; + let name: string | undefined; + let icon: string | undefined; + + if (NodeManagerUtils.isDatabaseObject(nodeId) && data.type !== NavigationType.closeConnection) { + const node = await this.loadNode({ nodeId, parentId }); + + name = node.name; + icon = node.icon; + + if (node.folder) { + const parent = this.getNode(node.parentId); + folderId = nodeId; + if (parent && !parent.folder) { + nodeId = parent.id; + parentId = parent.parentId; + name = parent.name; + icon = parent.icon; + } + } + + if (data.folderId) { + folderId = data.folderId; + } + } + + const getParents = () => { + const parents: string[] = []; + let parent = this.getNode(nodeId); + + while (parent && parent.parentId !== ROOT_NODE_PATH) { + parents.unshift(parent.parentId); + parent = this.getNode(parent.parentId); + } + + return parents; + }; + + const loadParents = async (parents: string[]) => { + let parentId = ROOT_NODE_PATH; + + await this.navNode.load({ + nodes: parents.map((nodeId) => { + try { + return { nodeId, parentId }; + } finally { + parentId = nodeId; + } + }), + }); + }; + + return { + type: data.type, + nodeId, + parentId, + folderId, + name, + icon, + getParents, + loadParents, + }; + } + + private async navigateHandler(contexts: IContextProvider) { + } + + private async loadNavNodeInfo( + navNode: Map, + metadata: MetadataMap, + load: boolean, + data: INavNodePath | INavNodeData | INavNodeId, + ) { + if (data.nodesValue) { + for (const nodeValue of data.nodesValue) { + const itemMetadata = metadata.get(nodeValue.node.id); + navNode.set(nodeValue.node.id, { + ...nodeValue.node, + objectFeatures: nodeValue.node.object?.features || [], + parentId: nodeValue.parentId, + }); + itemMetadata.loaded = true; + } + return navNode; + } + + const nodesId = data.navNodeId + ? data.navNodeId + : data.nodes.map(node => node.nodeId); + + if (data.remove) { + for (const navNodeId of nodesId) { + navNode.delete(navNodeId); + metadata.delete(navNodeId); + } + + await this.dbObjectService.remove(nodesId); + return navNode; + } + + for (const navNodeId of nodesId) { + const itemMetadata = metadata.get(navNodeId); + + try { + itemMetadata.loaded = false; + if (load) { + itemMetadata.loading = true; + const { navNodeInfo } = await this.graphQLService.gql.navNodeInfo({ + nodePath: navNodeId, + }); + + let parentId = ROOT_NODE_PATH; + if (data.navNodeId) { + parentId = navNode.get(navNodeId)?.parentId || parentId; + } else { + parentId = data.nodes.find(node => node.nodeId === navNodeId)?.parentId || parentId; + } + + navNode.set(navNodeId, { + ...navNodeInfo, + objectFeatures: navNodeInfo.object?.features || [], + parentId, + }); + itemMetadata.loaded = true; + } + } finally { + itemMetadata.loading = false; + } + } + + await this.dbObjectService.dbObject.refresh(false, { navNodeId: nodesId }); + + return navNode; + } + + private async loadNavTree( + navTree: Map, + metadata: MetadataMap, + load: boolean, + parentId: string, + remove: boolean + ) { + const itemMetadata = metadata.get(parentId); + let childrenToRemove = navTree.get(parentId)?.concat() || []; + + if (remove) { + childrenToRemove = [parentId]; + } else { + try { + itemMetadata.loaded = false; + if (load) { + itemMetadata.loading = true; + + const { navNodeChildren } = await this.graphQLService.gql.navNodeChildren({ + parentPath: parentId, + }); + + await this.navNode.refresh(true, { + nodesValue: navNodeChildren.map(node => ({ node, parentId })), + }); + navTree.set(parentId, navNodeChildren.map(node => node.id)); + itemMetadata.loaded = true; + } + } finally { + itemMetadata.loading = false; + } + + const newChildren = navTree.get(parentId); + const nestedChildren = this.getNestedChildren(parentId); + + await this.navNode.refresh(false, { + navNodeId: nestedChildren.filter(navNodeId => !newChildren || !newChildren.includes(navNodeId)), + }); + + for (const navNodeId of nestedChildren) { + const itemMetadata = metadata.get(navNodeId); + itemMetadata.loaded = false; + } + + if (newChildren) { + childrenToRemove = childrenToRemove.filter(navNodeId => !newChildren.includes(navNodeId)); + } + } + + const nestedChildren = this.getNestedChildren(childrenToRemove); + await this.navNode.refresh(true, { navNodeId: nestedChildren, remove: true }); + for (const navNodeId of nestedChildren) { + navTree.delete(navNodeId); + metadata.delete(navNodeId); + } + + return navTree; + } +} diff --git a/webapp/packages/core/src/app/shared/NodesManager/NodeWithParent.ts b/webapp/packages/core/src/app/shared/NodesManager/NodeWithParent.ts deleted file mode 100644 index d463b53fdc..0000000000 --- a/webapp/packages/core/src/app/shared/NodesManager/NodeWithParent.ts +++ /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 { DatabaseObjectInfo, NavigatorNodeInfo } from '@dbeaver/core/sdk'; - -// Node Info without ObjectProperties -export type NavigatorNodeInfoLight = Omit & { - object?: { - features?: string[]; // EObjectFeature - }; -} - -export type NodeWithParent = NavigatorNodeInfoLight & { - parentId: string; -} - -export type DatabaseObjectInfoWithId = DatabaseObjectInfo & { - id: string; -} - -export type NodeChildren = { - children: string[]; - isLoaded: boolean; -} diff --git a/webapp/packages/core/src/app/shared/NodesManager/NodesManagerService.ts b/webapp/packages/core/src/app/shared/NodesManager/NodesManagerService.ts deleted file mode 100644 index f0e56a9096..0000000000 --- a/webapp/packages/core/src/app/shared/NodesManager/NodesManagerService.ts +++ /dev/null @@ -1,314 +0,0 @@ -/* - * cloudbeaver - Cloud Database Manager - * Copyright (C) 2020 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ - -import { injectable } from '@dbeaver/core/di'; -import { PermissionsService, EPermission } from '@dbeaver/core/root'; -import { DatabaseObjectInfo, GraphQLService } from '@dbeaver/core/sdk'; - -import { INavigator } from '../Navigation/INavigator'; -import { IContextProvider } from '../Navigation/NavigationContext'; -import { NavigationService } from '../Navigation/NavigationService'; -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; - 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 permissionsService: PermissionsService) { - - this.navigator = this.navigationService.createNavigator( - data => data.nodeId, - this.navigateHandler.bind(this), - { - type: NavigationType.open, - nodeId: ROOT_NODE_PATH, - } - ); - } - - 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, - }); - } - - async refreshNode(nodeId: string) { - await this.purgeServerCache(nodeId); - this.nodesStore.removeNode(nodeId); - await this.updateNodeInfo(nodeId); - } - - getDatabaseObjectInfo(nodeId: string): DatabaseObjectInfoWithId | undefined { - return this.nodesStore.getDatabaseObjectInfo(nodeId); - } - - getNode(nodeId: string): NodeWithParent | undefined { - return this.nodesStore.getNode(nodeId); - } - - getChildren(nodeId: string): NodeChildren | undefined { - return this.nodesStore.getChildren(nodeId); - } - - async updateNodeInfo(nodeId: string) { - const { navNodeInfo } = await this.graphQLService.gql.navNodeInfo({ - nodePath: nodeId, - }); - - let parentId = nodeId.match(/^(.*)\/.*?$/)![1]; - if (parentId === 'database:/') { - parentId = ROOT_NODE_PATH; - } - - const newNode = { ...navNodeInfo, parentId }; - this.nodesStore.updateNodeInfo(newNode); - return newNode; - } - - async updateDatabaseObjectInfo(nodeId: string) { - const node = this.nodesStore.getNode(nodeId); - if (!node) { - await this.updateNodeInfo(nodeId); - } - - const { objectInfo } = await this.graphQLService.gql.queryDatabaseObjectInfo({ - nodeId, - // filter: { features: ['viewable', 'editPossible'] }, - }); - - const info = { ...objectInfo.object, id: objectInfo.id }; - this.nodesStore.updateDatabaseObjectInfo(info); - return info; - } - - async purgeServerCache(nodeId: string) { - await this.graphQLService.gql.navRefreshNode({ - nodePath: nodeId, - }); - } - - async updateChildren(parentId: string) { - const { navNodeChildren } = await this.graphQLService.gql.navNodeChildren({ - parentPath: parentId, - }); - const children = navNodeChildren.map(child => child.id); - this.nodesStore.updateChildren(parentId, children); - for (const child of navNodeChildren) { - this.nodesStore.updateNodeInfo({ ...child, parentId }); - } - return children; - } - - async updateRootChildren() { - if (!await this.permissionsService.hasAsync(EPermission.public)) { - return; - } - this.updateChildren(ROOT_NODE_PATH); - } - - async updateChildrenDatabaseObjectInfo(nodeId: string) { - const { - childrenDatabaseObjectInfo, - } = await this.graphQLService.gql.queryChildrenDatabaseObjectInfo({ - nodePath: nodeId, - // filter: { features: ['viewable', 'editPossible'] }, // filter properties for view - }); - - const children = childrenDatabaseObjectInfo.map(children => ({ - ...children.object, - id: children.id, - })); - this.nodesStore.updateChildrenDatabaseObjectInfo(children); - - return children; - } - - async loadNodeInfo(nodeId: string): Promise { - let node = this.nodesStore.getNode(nodeId); - if (node) { - if (node.parentId !== ROOT_NODE_PATH) { - await this.loadNodeInfo(node.parentId); - } - return node; - } - - 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 { - const nodeChildren = this.nodesStore.getChildren(parentId); - if (nodeChildren?.isLoaded) { - return nodeChildren.children; - } - - return this.updateChildren(parentId); - } - - async loadDatabaseObjectInfo( - nodeId: string - ): Promise { - const databaseObjectInfo = this.nodesStore.getDatabaseObjectInfo(nodeId); - if (databaseObjectInfo) { - return databaseObjectInfo; - } - - return this.updateDatabaseObjectInfo(nodeId); - } - - async loadChildrenDatabaseObjectInfo(parentId: string): Promise { - const children = await this.loadChildren(parentId); - const childrenDatabaseObjectInfo: DatabaseObjectInfoWithId[] = []; - for (const child of children) { - const databaseObjectInfo = this.nodesStore.getDatabaseObjectInfo(child); - if (!databaseObjectInfo) { - return this.updateChildrenDatabaseObjectInfo(parentId); - } - childrenDatabaseObjectInfo.push(databaseObjectInfo); - } - - return childrenDatabaseObjectInfo; - } - - getParent(node: NodeWithParent) { - return this.nodesStore.getNode(node.parentId); - } - - getNodeContainerInfo(nodeId: string): INodeContainerInfo { - const initial: INodeContainerInfo = {}; - - const scanParents = (res: INodeContainerInfo, - nodeId?: string): INodeContainerInfo => { - if (!nodeId) { - return res; - } - const node = this.getNode(nodeId); - if (!node) { - return res; - } - if (node?.object?.features?.includes(EObjectFeature.dataSource)) { - res.connectionId = node.id; - } - if (node?.object?.features?.includes(EObjectFeature.catalog)) { - 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); - }; - - return scanParents(initial, nodeId); - } - - removeNodes(path = ROOT_NODE_PATH) { - this.nodesStore.removeNode(path); - } - - navigationNodeContext = async ( - contexts: IContextProvider, - data: INodeNavigationData - ): Promise => { - let nodeId = data.nodeId; - let folderId = ''; - let name: string | undefined; - let icon: string | undefined; - - 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, - folderId, - name, - icon, - }; - } - - async navigateHandler(contexts: IContextProvider) { - } - - isNodeHasData(info?: string | DatabaseObjectInfo) { - if (typeof info === 'string') { - info = this.nodesStore.getDatabaseObjectInfo(info); - } - - if (!info || !info.features) { - return false; - } - - return info.features.includes(ENodeFeature.dataContainer) - || info.features.includes(ENodeFeature.container); - } -} diff --git a/webapp/packages/core/src/app/shared/NodesManager/NodesStore.ts b/webapp/packages/core/src/app/shared/NodesManager/NodesStore.ts deleted file mode 100644 index b9f6be1252..0000000000 --- a/webapp/packages/core/src/app/shared/NodesManager/NodesStore.ts +++ /dev/null @@ -1,90 +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, observable } from 'mobx'; - -import { - DatabaseObjectInfoWithId, - NodeChildren, - NodeWithParent, -} from './NodeWithParent'; - - -export class NodesStore { - @observable - private nodesMap: Map = new Map() - @observable - private databaseObjectInfoMap: Map = new Map() - @observable - private nodeChildrenMap: Map = new Map() - - getNode(nodeId: string): NodeWithParent | undefined { - return this.nodesMap.get(nodeId); - } - - getChildren(nodeId: string): NodeChildren | undefined { - return this.nodeChildrenMap.get(nodeId); - } - - getDatabaseObjectInfo(nodeId: string): DatabaseObjectInfoWithId | undefined { - return this.databaseObjectInfoMap.get(nodeId); - } - - @action - updateNodeInfo(node: NodeWithParent) { - this.nodesMap.set(node.id, node); - - const parent = this.nodeChildrenMap.get(node.parentId); - - if (!parent) { - this.nodeChildrenMap.set(node.parentId, { - children: [node.id], - isLoaded: false, - }); - } else if (!parent.children.includes(node.id)) { - parent.children.push(node.id); - } - } - - @action - updateDatabaseObjectInfo(databaseObjectInfo: DatabaseObjectInfoWithId) { - this.databaseObjectInfoMap.set(databaseObjectInfo.id, databaseObjectInfo); - } - - @action - updateChildren(parentId: string, children: string[]) { - this.nodeChildrenMap.set(parentId, { - children, - isLoaded: true, - }); - } - - @action - updateChildrenDatabaseObjectInfo(children: DatabaseObjectInfoWithId[]) { - for (const child of children) { - this.databaseObjectInfoMap.set(child.id, child); - } - } - - @action - removeNode(nodeId: string) { - const node = this.nodesMap.get(nodeId); - if (!node) { - return; - } - const parent = this.nodeChildrenMap.get(node.id); - if (parent) { - for (const children of parent.children) { - this.removeNode(children); - } - this.nodeChildrenMap.delete(node.id); - } - this.databaseObjectInfoMap.delete(nodeId); - this.nodesMap.delete(nodeId); - } -} diff --git a/webapp/packages/core/src/app/shared/NodesManager/queries/queryChildrenDatabaseObjectInfo.gql b/webapp/packages/core/src/app/shared/NodesManager/queries/getChildrenDBObjectInfo.gql similarity index 65% rename from webapp/packages/core/src/app/shared/NodesManager/queries/queryChildrenDatabaseObjectInfo.gql rename to webapp/packages/core/src/app/shared/NodesManager/queries/getChildrenDBObjectInfo.gql index b06b2b61f3..52b53e4f6c 100644 --- a/webapp/packages/core/src/app/shared/NodesManager/queries/queryChildrenDatabaseObjectInfo.gql +++ b/webapp/packages/core/src/app/shared/NodesManager/queries/getChildrenDBObjectInfo.gql @@ -1,8 +1,8 @@ -query queryChildrenDatabaseObjectInfo( - $nodePath: ID! +query getChildrenDBObjectInfo( + $navNodeId: ID! $filter: ObjectPropertyFilter ) { - childrenDatabaseObjectInfo: navNodeChildren(parentPath: $nodePath) { + dbObjects: navNodeChildren(parentPath: $navNodeId) { id object { features diff --git a/webapp/packages/core/src/app/shared/NodesManager/queries/queryDatabaseObjectInfo.gql b/webapp/packages/core/src/app/shared/NodesManager/queries/getDBObjectInfo.gql similarity index 60% rename from webapp/packages/core/src/app/shared/NodesManager/queries/queryDatabaseObjectInfo.gql rename to webapp/packages/core/src/app/shared/NodesManager/queries/getDBObjectInfo.gql index 54854ee4ca..753adf711d 100644 --- a/webapp/packages/core/src/app/shared/NodesManager/queries/queryDatabaseObjectInfo.gql +++ b/webapp/packages/core/src/app/shared/NodesManager/queries/getDBObjectInfo.gql @@ -1,6 +1,5 @@ -query queryDatabaseObjectInfo($nodeId: ID!, $filter: ObjectPropertyFilter) { - objectInfo: navNodeInfo(nodePath: $nodeId) { - id +query getDBObjectInfo($navNodeId: ID!, $filter: ObjectPropertyFilter) { + objectInfo: navNodeInfo(nodePath: $navNodeId) { object { features properties(filter: $filter) { diff --git a/webapp/packages/core/src/app/shared/NodesManager/useDatabaseObjectInfo.ts b/webapp/packages/core/src/app/shared/NodesManager/useDatabaseObjectInfo.ts index f1b759ec74..8beba2a904 100644 --- a/webapp/packages/core/src/app/shared/NodesManager/useDatabaseObjectInfo.ts +++ b/webapp/packages/core/src/app/shared/NodesManager/useDatabaseObjectInfo.ts @@ -8,10 +8,10 @@ import { useService } from '@dbeaver/core/di'; -import { NodesManagerService } from './NodesManagerService'; +import { DBObjectService } from './DBObjectService'; -export function useDatabaseObjectInfo(nodeId: string) { - const nodesManagerService = useService(NodesManagerService); +export function useDatabaseObjectInfo(navNodeId: string) { + const dbObjectService = useService(DBObjectService); - return nodesManagerService.getDatabaseObjectInfo(nodeId); + return dbObjectService.getDBObject(navNodeId); } diff --git a/webapp/packages/core/src/app/shared/NodesManager/useNode.ts b/webapp/packages/core/src/app/shared/NodesManager/useNode.ts index bc3121d5d6..8960355d85 100644 --- a/webapp/packages/core/src/app/shared/NodesManager/useNode.ts +++ b/webapp/packages/core/src/app/shared/NodesManager/useNode.ts @@ -8,10 +8,19 @@ import { useService } from '@dbeaver/core/di'; -import { NodesManagerService } from './NodesManagerService'; +import { NavNodeManagerService } from './NavNodeManagerService'; -export function useNode(nodeId: string) { - const nodesManagerService = useService(NodesManagerService); +export function useNode(navNodeId: string) { + const navNodeManagerService = useService(NavNodeManagerService); + const node = navNodeManagerService.getNode(navNodeId); + const isLoading = navNodeManagerService.navNode.isDataLoading({ + navNodeId: [navNodeId], + }); + const isLoaded = navNodeManagerService.navNode.isLoaded({ + navNodeId: [navNodeId], + }); - return nodesManagerService.getNode(nodeId); + return { + node, isLoading, isLoaded, + }; } diff --git a/webapp/packages/core/src/app/shared/useChildren.ts b/webapp/packages/core/src/app/shared/useChildren.ts index 4bf58116ee..c4b943826e 100644 --- a/webapp/packages/core/src/app/shared/useChildren.ts +++ b/webapp/packages/core/src/app/shared/useChildren.ts @@ -8,10 +8,17 @@ import { useService } from '@dbeaver/core/di'; -import { NodesManagerService } from './NodesManager/NodesManagerService'; +import { NavNodeManagerService, ROOT_NODE_PATH } from './NodesManager/NavNodeManagerService'; -export function useChildren(parentId = '/') { - const nodesManagerService = useService(NodesManagerService); +export function useChildren(navNodeId = ROOT_NODE_PATH) { + const navNodeManagerService = useService(NavNodeManagerService); + const children = navNodeManagerService.getTree(navNodeId); + const isLoading = navNodeManagerService.navTree.isDataLoading(navNodeId, false); + const isLoaded = navNodeManagerService.navTree.isLoaded(navNodeId, false); - return nodesManagerService.getChildren(parentId); + return { + children, + isLoaded, + isLoading, + }; } diff --git a/webapp/packages/core/src/manifest.ts b/webapp/packages/core/src/manifest.ts index f84c873448..868006d856 100644 --- a/webapp/packages/core/src/manifest.ts +++ b/webapp/packages/core/src/manifest.ts @@ -8,7 +8,7 @@ import { ConnectionDialogsService, ConnectionsManagerService, - MainMenuService, NavigationService, NavigationTreeService, NodesManagerService, + MainMenuService, NavigationService, NavigationTreeService, NavNodeManagerService, DBObjectService, ConnectionSchemaManagerService, NavigationTabsService, NavigationTreeContextMenuService, SettingsMenuService, LogViewerService, LogViewerMenuService, TopNavService, @@ -64,7 +64,8 @@ export const coreManifest: PluginManifest = { NavigationTabsService, NavigationTreeContextMenuService, NavigationTreeService, - NodesManagerService, + NavNodeManagerService, + DBObjectService, NotificationService, SessionService, SettingsMenuService, diff --git a/webapp/packages/core/src/root/PermissionsService.ts b/webapp/packages/core/src/root/PermissionsService.ts index 2500e2dbe1..574a37f3e2 100644 --- a/webapp/packages/core/src/root/PermissionsService.ts +++ b/webapp/packages/core/src/root/PermissionsService.ts @@ -52,7 +52,7 @@ export class PermissionsService { } async update() { - await this.permissions.refresh(); + await this.permissions.refresh(true); this.updateSubject.next(); } diff --git a/webapp/packages/core/src/root/SessionService.ts b/webapp/packages/core/src/root/SessionService.ts index 2ebc87b235..37d23a0900 100644 --- a/webapp/packages/core/src/root/SessionService.ts +++ b/webapp/packages/core/src/root/SessionService.ts @@ -33,7 +33,7 @@ export class SessionService { } async update() { - await this.session.refresh(); + await this.session.refresh(true); } private async refreshSessionStateAsync(data: SessionState | undefined): Promise { diff --git a/webapp/packages/core/src/sdk/CachedResource.ts b/webapp/packages/core/src/sdk/CachedResource.ts index 87cec251fe..d211c6b7ff 100644 --- a/webapp/packages/core/src/sdk/CachedResource.ts +++ b/webapp/packages/core/src/sdk/CachedResource.ts @@ -7,6 +7,7 @@ */ import { observable } from 'mobx'; +import { Subject, Observable } from 'rxjs'; export type Loader = ( current: TData, @@ -21,13 +22,23 @@ export type IsLoaded = ( ...args: TArgs ) => boolean +export type IsLoading = ( + current: TData, + metadata: TMetadata, + ...args: TArgs +) => boolean + export class CachedResource { @observable data: TData; @observable private loading = false; @observable private metadata: TMetadata; + + readonly onDataUpdate: Observable; + private dataSubject: Subject; private loader: Loader; private isLoadedCheck: IsLoaded; + private isLoadingCheck?: IsLoading; private activePromise: Promise | null = null; constructor( @@ -35,11 +46,15 @@ export class CachedResource { loader: Loader, isLoadedCheck: IsLoaded, metadata?: TMetadata, + isLoadingCheck?: IsLoading, ) { this.data = defaultValue; this.loader = loader; this.isLoadedCheck = isLoadedCheck; + this.isLoadingCheck = isLoadingCheck; this.metadata = metadata || {} as TMetadata; + this.dataSubject = new Subject(); + this.onDataUpdate = this.dataSubject.asObservable(); } isLoaded(...args: TArgs): boolean { @@ -49,36 +64,56 @@ export class CachedResource { isLoading(): boolean { return this.loading; } + isDataLoading(...args: TArgs): boolean { + if (this.isLoadingCheck) { + return this.loading && this.isLoadingCheck(this.data, this.metadata, ...args); + } + return this.loading; + } - async refresh(...args: TArgs): Promise { - return this.loadAll(true, args); + async refresh(load = false, ...args: TArgs): Promise { + return this.loadData(load, true, args); } async load(...args: TArgs): Promise { - return this.loadAll(false, args); + return this.loadData(true, false, args); } - private async loadAll(update: boolean, args: TArgs) { + async refreshUnblocked(load = false, ...args: TArgs): Promise { + return this.loadData(load, true, args, true); + } + + async loadUnblocked(...args: TArgs): Promise { + return this.loadData(true, false, args, true); + } + + private async loadData(load: boolean, update: boolean, args: TArgs, unblocked?: boolean) { + if (unblocked) { + return this.loadingTask(load, update, args); + } await this.waitActive(); - this.activePromise = this.loadingTask(update, args); + this.activePromise = this.loadingTask(load, update, args); try { return await this.activePromise; } finally { this.activePromise = null; + this.dataSubject.next(this.data); } } - private async loadingTask(update: boolean, args: TArgs): Promise { + private async loadingTask(load: boolean, update: boolean, args: TArgs): Promise { + const prevState = this.loading; this.loading = true; try { - if (this.isLoaded(...args) && !update) { + // don't load existed data & don't refresh doesn't loaded data + if ((this.isLoaded(...args) && !update) || (!this.isLoaded(...args) && update && !load)) { return this.data; } - this.data = await this.loader(this.data, this.metadata, update, ...args); + this.data = await this.loader(this.data, this.metadata, load, ...args); } finally { - this.loading = false; + this.loading = prevState; } return this.data; diff --git a/webapp/packages/core/src/sdk/sdk.ts b/webapp/packages/core/src/sdk/sdk.ts index f424d9d083..54e6bbfc0b 100644 --- a/webapp/packages/core/src/sdk/sdk.ts +++ b/webapp/packages/core/src/sdk/sdk.ts @@ -1,5 +1,6 @@ /* eslint-disable max-len */ import { GraphQLClient } from 'graphql-request'; + export type Maybe = T; /** All built-in and custom scalars, mapped to their actual values */ @@ -686,7 +687,7 @@ export type NavGetStructContainersQueryVariables = { }; -export type NavGetStructContainersQuery = { navGetStructContainers: { catalogList: Array>, schemaList: Array> } }; +export type NavGetStructContainersQuery = { navGetStructContainers: { catalogList: Array>; schemaList: Array> } }; export type CloseConnectionMutationVariables = { id: Scalars['ID']; @@ -727,7 +728,7 @@ export type DriverPropertiesQueryVariables = { export type DriverPropertiesQuery = { driver: Array<( Pick & { driverProperties?: Maybe>>> } - )> }; + )>; }; export type GetDriverByIdQueryVariables = { driverId: Scalars['ID']; @@ -750,6 +751,31 @@ export type TestConnectionMutationVariables = { export type TestConnectionMutation = { testConnection: Pick }; +export type GetChildrenDbObjectInfoQueryVariables = { + navNodeId: Scalars['ID']; + filter?: Maybe; +}; + + +export type GetChildrenDbObjectInfoQuery = { dbObjects: Array<( + Pick + & { object?: Maybe<( + Pick + & { properties?: Maybe>>> } + )>; } + )>; }; + +export type GetDbObjectInfoQueryVariables = { + navNodeId: Scalars['ID']; + filter?: Maybe; +}; + + +export type GetDbObjectInfoQuery = { objectInfo: { object?: Maybe<( + Pick + & { properties?: Maybe>>> } + )>; }; }; + export type NavNodeChildrenQueryVariables = { parentPath: Scalars['ID']; }; @@ -758,7 +784,7 @@ export type NavNodeChildrenQueryVariables = { export type NavNodeChildrenQuery = { navNodeChildren: Array<( Pick & { object?: Maybe> } - )> }; + )>; }; export type NavNodeInfoQueryVariables = { nodePath: Scalars['ID']; @@ -768,7 +794,7 @@ export type NavNodeInfoQueryVariables = { export type NavNodeInfoQuery = { navNodeInfo: ( Pick & { object?: Maybe> } - ) }; + ); }; export type NavRefreshNodeQueryVariables = { nodePath: Scalars['ID']; @@ -777,34 +803,6 @@ export type NavRefreshNodeQueryVariables = { export type NavRefreshNodeQuery = Pick; -export type QueryChildrenDatabaseObjectInfoQueryVariables = { - nodePath: Scalars['ID']; - filter?: Maybe; -}; - - -export type QueryChildrenDatabaseObjectInfoQuery = { childrenDatabaseObjectInfo: Array<( - Pick - & { object?: Maybe<( - Pick - & { properties?: Maybe>>> } - )> } - )> }; - -export type QueryDatabaseObjectInfoQueryVariables = { - nodeId: Scalars['ID']; - filter?: Maybe; -}; - - -export type QueryDatabaseObjectInfoQuery = { objectInfo: ( - Pick - & { object?: Maybe<( - Pick - & { properties?: Maybe>>> } - )> } - ) }; - export type ReadSessionLogQueryVariables = { maxEntries: Scalars['Int']; clearEntries: Scalars['Boolean']; @@ -891,7 +889,7 @@ export type GetAuthProvidersQueryVariables = {}; export type GetAuthProvidersQuery = { providers: Array<( Pick & { credentialParameters: Array> } - )> }; + )>; }; export type GetSessionUserQueryVariables = {}; @@ -906,7 +904,7 @@ export type AsyncExportTaskStatusMutationVariables = { export type AsyncExportTaskStatusMutation = { taskInfo: ( Pick & { error?: Maybe> } - ) }; + ); }; export type ExportDataFromContainerQueryVariables = { connectionId: Scalars['ID']; @@ -918,7 +916,7 @@ export type ExportDataFromContainerQueryVariables = { export type ExportDataFromContainerQuery = { taskInfo: ( Pick & { error?: Maybe> } - ) }; + ); }; export type ExportDataFromResultsQueryVariables = { connectionId: Scalars['ID']; @@ -931,7 +929,7 @@ export type ExportDataFromResultsQueryVariables = { export type ExportDataFromResultsQuery = { taskInfo: ( Pick & { error?: Maybe> } - ) }; + ); }; export type GetDataTransferProcessorsQueryVariables = {}; @@ -939,7 +937,7 @@ export type GetDataTransferProcessorsQueryVariables = {}; export type GetDataTransferProcessorsQuery = { processors: Array<( Pick & { properties?: Maybe>>> } - )> }; + )>; }; export type RemoveDataTransferFileQueryVariables = { dataFileId: Scalars['String']; @@ -965,10 +963,10 @@ export type AsyncSqlExecuteQueryMutation = { taskInfo: ( & { resultSet?: Maybe<( Pick & { columns?: Maybe>>> } - )> } - )> } - )>, error?: Maybe> } - ) }; + )>; } + )>; } + )>; error?: Maybe>; } + ); }; export type AsyncTaskCancelMutationVariables = { taskId: Scalars['String']; @@ -991,10 +989,10 @@ export type AsyncTaskStatusMutation = { taskInfo: ( & { resultSet?: Maybe<( Pick & { columns?: Maybe>>> } - )> } - )> } - )>, error?: Maybe> } - ) }; + )>; } + )>; } + )>; error?: Maybe>; } + ); }; export type ExecuteSqlQueryMutationVariables = { connectionId: Scalars['ID']; @@ -1011,9 +1009,9 @@ export type ExecuteSqlQueryMutation = { result?: Maybe<( & { resultSet?: Maybe<( Pick & { columns?: Maybe>>> } - )> } - )> } - )> }; + )>; } + )>; } + )>; }; export type MetadataGetNodeDdlQueryVariables = { nodeId: Scalars['ID']; @@ -1055,9 +1053,9 @@ export type ReadDataFromContainerMutation = { readDataFromContainer?: Maybe<( & { resultSet?: Maybe<( Pick & { columns?: Maybe>>> } - )> } - )> } - )> }; + )>; } + )>; } + )>; }; export type SqlContextCreateMutationVariables = { connectionId: Scalars['ID']; @@ -1109,8 +1107,8 @@ export type UpdateResultsDataMutation = { result?: Maybe<( & { results: Array<( Pick & { resultSet?: Maybe> } - )> } - )> }; + )>; } + )>; }; export type OpenSessionMutationVariables = {}; @@ -1118,7 +1116,7 @@ export type OpenSessionMutationVariables = {}; export type OpenSessionMutation = { session: ( Pick & { connections: Array> } - ) }; + ); }; export type ServerConfigQueryVariables = {}; @@ -1126,7 +1124,7 @@ export type ServerConfigQueryVariables = {}; export type ServerConfigQuery = { serverConfig: ( Pick & { supportedLanguages: Array> } - ) }; + ); }; export type SessionPermissionsQueryVariables = {}; @@ -1139,7 +1137,7 @@ export type SessionStateQueryVariables = {}; export type SessionStateQuery = { sessionState: ( Pick & { connections: Array> } - ) }; + ); }; export type TouchSessionMutationVariables = {}; @@ -1257,6 +1255,43 @@ export const TestConnectionDocument = ` } } `; +export const GetChildrenDbObjectInfoDocument = ` + query getChildrenDBObjectInfo($navNodeId: ID!, $filter: ObjectPropertyFilter) { + dbObjects: navNodeChildren(parentPath: $navNodeId) { + id + object { + features + properties(filter: $filter) { + id + category + dataType + description + displayName + features + value + } + } + } +} + `; +export const GetDbObjectInfoDocument = ` + query getDBObjectInfo($navNodeId: ID!, $filter: ObjectPropertyFilter) { + objectInfo: navNodeInfo(nodePath: $navNodeId) { + object { + features + properties(filter: $filter) { + id + category + dataType + description + displayName + features + value + } + } + } +} + `; export const NavNodeChildrenDocument = ` query navNodeChildren($parentPath: ID!) { navNodeChildren(parentPath: $parentPath) { @@ -1298,44 +1333,6 @@ export const NavRefreshNodeDocument = ` navRefreshNode(nodePath: $nodePath) } `; -export const QueryChildrenDatabaseObjectInfoDocument = ` - query queryChildrenDatabaseObjectInfo($nodePath: ID!, $filter: ObjectPropertyFilter) { - childrenDatabaseObjectInfo: navNodeChildren(parentPath: $nodePath) { - id - object { - features - properties(filter: $filter) { - id - category - dataType - description - displayName - features - value - } - } - } -} - `; -export const QueryDatabaseObjectInfoDocument = ` - query queryDatabaseObjectInfo($nodeId: ID!, $filter: ObjectPropertyFilter) { - objectInfo: navNodeInfo(nodePath: $nodeId) { - id - object { - features - properties(filter: $filter) { - id - category - dataType - description - displayName - features - value - } - } - } -} - `; export const ReadSessionLogDocument = ` query readSessionLog($maxEntries: Int!, $clearEntries: Boolean!) { log: readSessionLog(maxEntries: $maxEntries, clearEntries: $clearEntries) { @@ -1841,6 +1838,12 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = testConnection(variables: TestConnectionMutationVariables): Promise { return withWrapper(() => client.request(TestConnectionDocument, variables)); }, + getChildrenDBObjectInfo(variables: GetChildrenDbObjectInfoQueryVariables): Promise { + return withWrapper(() => client.request(GetChildrenDbObjectInfoDocument, variables)); + }, + getDBObjectInfo(variables: GetDbObjectInfoQueryVariables): Promise { + return withWrapper(() => client.request(GetDbObjectInfoDocument, variables)); + }, navNodeChildren(variables: NavNodeChildrenQueryVariables): Promise { return withWrapper(() => client.request(NavNodeChildrenDocument, variables)); }, @@ -1850,12 +1853,6 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = navRefreshNode(variables: NavRefreshNodeQueryVariables): Promise { return withWrapper(() => client.request(NavRefreshNodeDocument, variables)); }, - queryChildrenDatabaseObjectInfo(variables: QueryChildrenDatabaseObjectInfoQueryVariables): Promise { - return withWrapper(() => client.request(QueryChildrenDatabaseObjectInfoDocument, variables)); - }, - queryDatabaseObjectInfo(variables: QueryDatabaseObjectInfoQueryVariables): Promise { - return withWrapper(() => client.request(QueryDatabaseObjectInfoDocument, variables)); - }, readSessionLog(variables: ReadSessionLogQueryVariables): Promise { return withWrapper(() => client.request(ReadSessionLogDocument, variables)); }, @@ -1963,7 +1960,7 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = }, touchSession(variables?: TouchSessionMutationVariables): Promise { return withWrapper(() => client.request(TouchSessionDocument, variables)); - } + }, }; } -export type Sdk = ReturnType; \ No newline at end of file +export type Sdk = ReturnType; diff --git a/webapp/packages/data-export-plugin/src/DataExportMenuService.ts b/webapp/packages/data-export-plugin/src/DataExportMenuService.ts index ae6885370d..dbb87ca18c 100644 --- a/webapp/packages/data-export-plugin/src/DataExportMenuService.ts +++ b/webapp/packages/data-export-plugin/src/DataExportMenuService.ts @@ -7,7 +7,7 @@ */ import { - NavigationTreeContextMenuService, NodeManagerUtils, NodeWithParent, EObjectFeature + NavigationTreeContextMenuService, NodeManagerUtils, NavNode, EObjectFeature } from '@dbeaver/core/app'; import { injectable } from '@dbeaver/core/di'; import { @@ -39,13 +39,13 @@ export class DataExportMenuService { }; this.tableFooterMenuService.registerMenuItem(exportData); - this.contextMenuService.addMenuItem( + this.contextMenuService.addMenuItem( this.contextMenuService.getRootMenuToken(), { id: 'export', isPresent(context) { return context.contextType === NavigationTreeContextMenuService.nodeContextType - && !!context.data.object?.features?.includes(EObjectFeature.dataContainer); + && context.data.objectFeatures.includes(EObjectFeature.dataContainer); }, order: 2, title: 'Export', diff --git a/webapp/packages/data-export-plugin/src/ExportNotification/ExportNotificationController.ts b/webapp/packages/data-export-plugin/src/ExportNotification/ExportNotificationController.ts index 4c060299cf..4076ff9865 100644 --- a/webapp/packages/data-export-plugin/src/ExportNotification/ExportNotificationController.ts +++ b/webapp/packages/data-export-plugin/src/ExportNotification/ExportNotificationController.ts @@ -8,7 +8,7 @@ import { observable, computed } from 'mobx'; -import { ErrorDetailsDialog, NodesManagerService } from '@dbeaver/core/app'; +import { ErrorDetailsDialog, NavNodeManagerService } from '@dbeaver/core/app'; import { IInitializableController, injectable } from '@dbeaver/core/di'; import { CommonDialogService } from '@dbeaver/core/dialogs'; import { INotification } from '@dbeaver/core/eventsLog'; @@ -46,7 +46,7 @@ export class ExportNotificationController implements IInitializableController { return ''; } if (this.task.context.containerNodePath) { - const node = this.nodesManagerService.getNode(this.task.context.containerNodePath); + const node = this.navNodeManagerService.getNode(this.task.context.containerNodePath); return `${this.localization.translate('data_transfer_exporting_table')} ${node?.name}`; } @@ -76,7 +76,7 @@ export class ExportNotificationController implements IInitializableController { constructor( private commonDialogService: CommonDialogService, private dataExportProcessService: DataExportProcessService, - private nodesManagerService: NodesManagerService, + private navNodeManagerService: NavNodeManagerService, private localization: LocalizationService, ) {} diff --git a/webapp/packages/data-viewer-plugin/src/DataViewerPage/DataViewerTab.tsx b/webapp/packages/data-viewer-plugin/src/DataViewerPage/DataViewerTab.tsx index bd6ba0807a..5956c37f42 100644 --- a/webapp/packages/data-viewer-plugin/src/DataViewerPage/DataViewerTab.tsx +++ b/webapp/packages/data-viewer-plugin/src/DataViewerPage/DataViewerTab.tsx @@ -9,7 +9,7 @@ import { observer } from 'mobx-react'; import styled from 'reshadow'; -import { NodesManagerService } from '@dbeaver/core/app'; +import { NavNodeManagerService } from '@dbeaver/core/app'; import { TabIcon, Tab, TabTitle } from '@dbeaver/core/blocks'; import { useService } from '@dbeaver/core/di'; import { useStyles } from '@dbeaver/core/theming'; @@ -18,9 +18,9 @@ import { ObjectPageTabProps } from '@dbeaver/object-viewer-plugin'; export const DataViewerTab = observer(function DataViewerTab({ tab, page, onSelect, style, }: ObjectPageTabProps) { - const nodesManagerService = useService(NodesManagerService); + const navNodeManagerService = useService(NavNodeManagerService); - if (!nodesManagerService.isNodeHasData(tab.handlerState.objectId)) { + if (!navNodeManagerService.isNodeHasData(tab.handlerState.objectId)) { return null; } diff --git a/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts b/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts index 943969d663..395d88159b 100644 --- a/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts +++ b/webapp/packages/data-viewer-plugin/src/DataViewerTabService.ts @@ -7,7 +7,7 @@ */ import { - NodesManagerService, + NavNodeManagerService, INodeNavigationData, IContextProvider, ITab, @@ -29,11 +29,13 @@ import { DataViewerTableService } from './DataViewerTableService'; export class DataViewerTabService { page: ObjectPage; - constructor(private nodesManagerService: NodesManagerService, - private dataViewerTableService: DataViewerTableService, - private objectViewerTabService: ObjectViewerTabService, - private dbObjectPageService: DBObjectPageService, - private notificationService: NotificationService) { + constructor( + private navNodeManagerService: NavNodeManagerService, + private dataViewerTableService: DataViewerTableService, + private objectViewerTabService: ObjectViewerTabService, + private dbObjectPageService: DBObjectPageService, + private notificationService: NotificationService + ) { this.page = this.dbObjectPageService.register({ key: dataViewerHandlerKey, @@ -49,7 +51,7 @@ export class DataViewerTabService { } registerTabHandler() { - this.nodesManagerService.navigator.addHandler(this.navigationHandler.bind(this)); + this.navNodeManagerService.navigator.addHandler(this.navigationHandler.bind(this)); } private async navigationHandler(contexts: IContextProvider) { @@ -60,14 +62,12 @@ export class DataViewerTabService { 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); + const node = await this.navNodeManagerService.loadNode(nodeInfo); - if (!this.nodesManagerService.isNodeHasData(objectInfo)) { + if (!this.navNodeManagerService.isNodeHasData(node)) { return; } @@ -83,9 +83,12 @@ export class DataViewerTabService { } private async handleTabSelect(tab: ITab) { - const objectInfo = await this.nodesManagerService.loadDatabaseObjectInfo(tab.handlerState.objectId); + const node = await this.navNodeManagerService.loadNode({ + nodeId: tab.handlerState.objectId, + parentId: tab.handlerState.parentId, + }); - if (!this.nodesManagerService.isNodeHasData(objectInfo)) { + if (!this.navNodeManagerService.isNodeHasData(node)) { return; } @@ -93,7 +96,7 @@ export class DataViewerTabService { return; } - const nodeInfo = this.nodesManagerService + const nodeInfo = this.navNodeManagerService .getNodeContainerInfo(tab.handlerState.objectId); if (!nodeInfo.connectionId) { @@ -111,11 +114,12 @@ export class DataViewerTabService { // if (!this.nodesManagerService.isNodeHasData(tab.handlerState.objectId)) { // return; // } - const info = await this.nodesManagerService.loadDatabaseObjectInfo(tab.handlerState.objectId); - if (info) { - return true; - } - return false; + await this.navNodeManagerService.loadNode({ + nodeId: tab.handlerState.objectId, + parentId: tab.handlerState.parentId, + }); + // await this.dbObjectService.load(tab.handlerState.objectId); + return true; } private handleTabClose(tab: ITab) { diff --git a/webapp/packages/ddl-viewer-plugin/src/DdlViewer/DdlViewerController.ts b/webapp/packages/ddl-viewer-plugin/src/DdlViewer/DdlViewerController.ts index 6008061b0c..1afa8a28ce 100644 --- a/webapp/packages/ddl-viewer-plugin/src/DdlViewer/DdlViewerController.ts +++ b/webapp/packages/ddl-viewer-plugin/src/DdlViewer/DdlViewerController.ts @@ -8,7 +8,7 @@ import { observable, when } from 'mobx'; -import { NodeManagerUtils, NodesManagerService } from '@dbeaver/core/app'; +import { NodeManagerUtils, NavNodeManagerService } from '@dbeaver/core/app'; import { IDestructibleController, IInitializableController, injectable } from '@dbeaver/core/di'; import { NotificationService } from '@dbeaver/core/eventsLog'; import { SqlDialectInfo } from '@dbeaver/core/sdk'; @@ -25,11 +25,12 @@ export class DdlViewerController implements IInitializableController, IDestructi private nodeId!: string; - constructor(private ddlViewerService: DdlViewerService, - private nodesManagerService: NodesManagerService, - private sqlDialectInfoService: SqlDialectInfoService, - private notificationService: NotificationService) { - } + constructor( + private ddlViewerService: DdlViewerService, + private navNodeManagerService: NavNodeManagerService, + private sqlDialectInfoService: SqlDialectInfoService, + private notificationService: NotificationService + ) { } init(nodeId: string) { this.nodeId = nodeId; @@ -55,7 +56,7 @@ export class DdlViewerController implements IInitializableController, IDestructi } private async loadDialect(nodeId: string): Promise { - const { connectionId } = this.nodesManagerService.getNodeContainerInfo(nodeId); + const { connectionId } = this.navNodeManagerService.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 8441f5edcd..8aa6e8a0a6 100644 --- a/webapp/packages/ddl-viewer-plugin/src/DdlViewerTabService.ts +++ b/webapp/packages/ddl-viewer-plugin/src/DdlViewerTabService.ts @@ -7,7 +7,7 @@ */ import { - EObjectFeature, NodesManagerService, IContextProvider, INodeNavigationData + EObjectFeature, NavNodeManagerService, IContextProvider, INodeNavigationData } from '@dbeaver/core/app'; import { ITab } from '@dbeaver/core/blocks'; import { injectable } from '@dbeaver/core/di'; @@ -22,16 +22,18 @@ const ddlViewerTabId = 'ddl-viewer'; @injectable() export class DdlViewerTabService { - constructor(private nodesManagerService: NodesManagerService, - private objectViewerTabService: ObjectViewerTabService, - private notificationService: NotificationService, - private ddlViewerService: DdlViewerService) { - this.nodesManagerService.navigator.addHandler(this.navigationHandler.bind(this)); + constructor( + private navNodeManagerService: NavNodeManagerService, + private objectViewerTabService: ObjectViewerTabService, + private notificationService: NotificationService, + private ddlViewerService: DdlViewerService + ) { + this.navNodeManagerService.navigator.addHandler(this.navigationHandler.bind(this)); } buildTab(nodeId: string): ITab | null { - const node = this.nodesManagerService.getNode(nodeId); - const isDdlRequired = node?.object?.features?.includes(EObjectFeature.script); + const node = this.navNodeManagerService.getNode(nodeId); + const isDdlRequired = node?.objectFeatures.includes(EObjectFeature.script); if (!isDdlRequired) { return null; } @@ -39,7 +41,7 @@ export class DdlViewerTabService { tabId: ddlViewerTabId, title: 'DDL', icon: 'sql-text', - onActivate: () => this.activateDDLTab(nodeId), + onActivate: () => this.activateDDLTab(nodeId, node?.parentId!), panel: () => ddlViewer(nodeId), }; return ddlTab; @@ -57,7 +59,7 @@ export class DdlViewerTabService { } } - private activateDDLTab(nodeId: string) { - this.nodesManagerService.navToNode(nodeId, ddlViewerTabId); + private activateDDLTab(nodeId: string, parentId: string) { + this.navNodeManagerService.navToNode(nodeId, parentId, ddlViewerTabId); } } diff --git a/webapp/packages/object-viewer-plugin/src/IObjectViewerTabState.ts b/webapp/packages/object-viewer-plugin/src/IObjectViewerTabState.ts index df6d46a8a2..5333288be8 100644 --- a/webapp/packages/object-viewer-plugin/src/IObjectViewerTabState.ts +++ b/webapp/packages/object-viewer-plugin/src/IObjectViewerTabState.ts @@ -8,6 +8,8 @@ export interface IObjectViewerTabState { objectId: string; + parentId: string; + parents: string[]; folderId: string; tabIcon?: string; tabTitle?: string; diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersTab/ObjectFoldersTabService.ts b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersTab/ObjectFoldersTabService.ts index bb62545ca5..bf8d811ef0 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersTab/ObjectFoldersTabService.ts +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectFoldersTab/ObjectFoldersTabService.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { NodesManagerService, TabEntity } from '@dbeaver/core/app'; +import { NavNodeManagerService, TabEntity } from '@dbeaver/core/app'; import { ITab } from '@dbeaver/core/blocks'; import { injectable } from '@dbeaver/core/di'; @@ -15,7 +15,7 @@ import { ObjectFolderTabModel } from './ObjectFolderTabModel'; @injectable() export class ObjectFoldersTabService { - constructor(private nodesManagerService: NodesManagerService) { + constructor(private navNodeManagerService: NavNodeManagerService) { } createTabEntities(nodeId: string): TabEntity[] { @@ -24,22 +24,22 @@ export class ObjectFoldersTabService { private tabBuilder(nodeId: string): ITab[] { - const children = this.nodesManagerService.getChildren(nodeId)?.children || []; + const children = this.navNodeManagerService.getTree(nodeId) || []; const folderTabs = children.filter((nodeId) => { - const node = this.nodesManagerService.getNode(nodeId); + const node = this.navNodeManagerService.getNode(nodeId); return node ? node.folder : false; }); const tabList: ITab[] = folderTabs.map((nodeId) => { - const node = this.nodesManagerService.getNode(nodeId); + const node = this.navNodeManagerService.getNode(nodeId); if (!node) { throw Error(`node not found: ${nodeId}`); } return new ObjectFolderTabModel( node, - () => this.nodesManagerService.navToNode(node.id) + () => this.navNodeManagerService.navToNode(node.id, node.parentId) ); }); return tabList; diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx index 9472677a9d..3928607375 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx @@ -35,9 +35,9 @@ const viewerStyles = composes( export const ObjectPropertiesPagePanel = observer(function ObjectPropertiesPagePanel({ tab, }: ObjectPagePanelProps) { - const children = useChildren(tab.handlerState.objectId); + const { children, isLoading } = useChildren(tab.handlerState.objectId); - if (!children?.isLoaded) { + if (!children || isLoading) { return ; } diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPageService.ts b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPageService.ts index fc15cd9a47..129b2ea6e2 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPageService.ts +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertiesPageService.ts @@ -7,7 +7,7 @@ */ import { - NodesManagerService, ITab, IContextProvider, INodeNavigationData + NavNodeManagerService, ITab, IContextProvider, INodeNavigationData } from '@dbeaver/core/app'; import { injectable } from '@dbeaver/core/di'; import { NotificationService } from '@dbeaver/core/eventsLog'; @@ -24,7 +24,7 @@ export class ObjectPropertiesPageService { page?: ObjectPage; constructor( - private nodesManagerService: NodesManagerService, + private navNodeManagerService: NavNodeManagerService, private notificationService: NotificationService, private objectViewerTabService: ObjectViewerTabService, private dbObjectPageService: DBObjectPageService @@ -41,7 +41,7 @@ export class ObjectPropertiesPageService { getPanelComponent: () => ObjectPropertiesPagePanel, onSelect: this.handlePageSelect.bind(this), }); - this.nodesManagerService.navigator.addHandler(this.navigationHandler.bind(this)); + this.navNodeManagerService.navigator.addHandler(this.navigationHandler.bind(this)); } private async handlePageSelect(tab: ITab) { diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/Item.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/Item.tsx index 0786c8d21f..b654f82bf2 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/Item.tsx +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/Item.tsx @@ -10,7 +10,7 @@ import { observer } from 'mobx-react'; import { useCallback } from 'react'; import styled, { css } from 'reshadow'; -import { NodesManagerService, useDatabaseObjectInfo, useNode } from '@dbeaver/core/app'; +import { NavNodeManagerService, useDatabaseObjectInfo, useNode } from '@dbeaver/core/app'; import { StaticImage, TableItem, TableColumnValue, TableItemSelect } from '@dbeaver/core/blocks'; @@ -51,12 +51,12 @@ type ItemProps = { export const Item = observer(function Item({ objectId, columns, }: ItemProps) { - const nodesManagerService = useService(NodesManagerService); - const object = useNode(objectId); + const navNodeManagerService = useService(NavNodeManagerService); + const { node } = useNode(objectId); const databaseObjectInfo = useDatabaseObjectInfo(objectId); - const handleOpen = useCallback(() => nodesManagerService.navToNode(objectId), [objectId]); + const handleOpen = useCallback(() => navNodeManagerService.navToNode(node!.id, node!.parentId), [node]); - if (!object || !databaseObjectInfo?.properties) { + if (!node || !databaseObjectInfo?.properties) { return styled(useStyles(itemStyles))( @@ -81,7 +81,7 @@ export const Item = observer(function Item({ - + {databaseObjectInfo.properties.map(property => ( diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx index 3a51618e29..823622753d 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx @@ -22,15 +22,18 @@ const styles = css` `; type ObjectPropertyTableProps = { - parentId: string; objectId: string; + parentId: string; } -export const ObjectPropertyTable = observer(function ObjectPropertyTable(props: ObjectPropertyTableProps) { - const children = useChildren(props.objectId); - const { isLoading } = useObjectFolder(props.parentId, props.objectId); +export const ObjectPropertyTable = observer(function ObjectPropertyTable({ + objectId, + parentId, +}: ObjectPropertyTableProps) { + const children = useChildren(objectId); + const { isLoading } = useObjectFolder(parentId); - if (!children?.isLoaded || isLoading) { + if (!children.children || children.isLoading || isLoading) { return ; } diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabMixin.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabMixin.tsx index 6225f3b44b..6b8b0d9bf1 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabMixin.tsx +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabMixin.tsx @@ -8,7 +8,7 @@ import { observable } from 'mobx'; -import { NodesManagerService } from '@dbeaver/core/app'; +import { DBObjectService } from '@dbeaver/core/app'; import { injectable } from '@dbeaver/core/di'; import { VirtualFolderTabData } from './VirtualFolderTabData'; @@ -18,9 +18,10 @@ export class VirtualFolderTabMixin { @observable isActivated = false; - constructor(private virtualFolderTabData: VirtualFolderTabData, - private nodesManagerService: NodesManagerService) { - } + constructor( + private virtualFolderTabData: VirtualFolderTabData, + private dbObjectService: DBObjectService + ) {} getChildrenId() { return this.virtualFolderTabData.childrenIds; @@ -37,7 +38,7 @@ export class VirtualFolderTabMixin { try { for (const nodeId of this.getChildrenId()) { - await this.nodesManagerService.loadDatabaseObjectInfo(nodeId); + await this.dbObjectService.load(nodeId); } } finally { diff --git a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabService.tsx b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabService.tsx index dd949a0df4..e8911af34e 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabService.tsx +++ b/webapp/packages/object-viewer-plugin/src/ObjectPropertiesPage/VirtualFolderTab/VirtualFolderTabService.tsx @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { NodesManagerService, NodeWithParent, TabEntity } from '@dbeaver/core/app'; +import { NavNodeManagerService, NavNode, TabEntity } from '@dbeaver/core/app'; import { injectable, MixinProvider } from '@dbeaver/core/di'; import { VirtualFolderTabData } from './VirtualFolderTabData'; @@ -17,15 +17,14 @@ import { VirtualFolderTabModel } from './VirtualFolderTabModel'; @injectable() export class VirtualFolderTabService { - constructor(private nodesManagerService: NodesManagerService) { - } + constructor(private navNodeManagerService: NavNodeManagerService) { } createTabEntities(nodeId: string): TabEntity[] { - const children = this.nodesManagerService.getChildren(nodeId)?.children || []; + const children = this.navNodeManagerService.getTree(nodeId) || []; - const notFolderNodes = children.reduce( + const notFolderNodes = children.reduce( (nodes, nodeId) => { - const node = this.nodesManagerService.getNode(nodeId); + const node = this.navNodeManagerService.getNode(nodeId); if (node && !node.folder) { nodes.push(node); } diff --git a/webapp/packages/object-viewer-plugin/src/ObjectViewerPanel.tsx b/webapp/packages/object-viewer-plugin/src/ObjectViewerPanel.tsx index 68efa898fb..234ff3ed07 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectViewerPanel.tsx +++ b/webapp/packages/object-viewer-plugin/src/ObjectViewerPanel.tsx @@ -39,11 +39,11 @@ const stylesArray = [styles]; export const ObjectViewerPanel = observer(function ObjectViewerPanel({ tab, handler, }: TabHandlerPanelProps) { - const children = useChildren(tab.handlerState.objectId); + const { children, isLoading } = useChildren(tab.handlerState.objectId); const dbObjectPagesService = useService(DBObjectPageService); const pages = dbObjectPagesService.orderedPages; - if (!children?.isLoaded) { + if (!children || isLoading) { return ; } diff --git a/webapp/packages/object-viewer-plugin/src/ObjectViewerTab.tsx b/webapp/packages/object-viewer-plugin/src/ObjectViewerTab.tsx index 687e5c9cb1..baa313aae3 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectViewerTab.tsx +++ b/webapp/packages/object-viewer-plugin/src/ObjectViewerTab.tsx @@ -18,7 +18,7 @@ import { IObjectViewerTabState } from './IObjectViewerTabState'; export const ObjectViewerTab = observer(function ObjectViewerTab({ tab, handler, onSelect, onClose, style, }: TabHandlerTabProps) { - const node = useNode(tab.handlerState.objectId); + 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 1ce1d99241..8f7bb67dcc 100644 --- a/webapp/packages/object-viewer-plugin/src/ObjectViewerTabService.ts +++ b/webapp/packages/object-viewer-plugin/src/ObjectViewerTabService.ts @@ -6,10 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { observable } from 'mobx'; - import { - NodesManagerService, NavigationTabsService, INodeNavigationData, IContextProvider, @@ -21,6 +18,9 @@ import { objectCatalogProvider, objectSchemaProvider, NavNodeManagerService, + DBObjectService, + ROOT_NODE_PATH, + NavNode, } from '@dbeaver/core/app'; import { injectable } from '@dbeaver/core/di'; import { NotificationService } from '@dbeaver/core/eventsLog'; @@ -35,13 +35,15 @@ import { objectViewerTabHandlerKey } from './objectViewerTabHandlerKey'; @injectable() export class ObjectViewerTabService { - @observable private propertiesTabLoadingState: Map = new Map(); readonly tabHandler: TabHandler - constructor(private nodesManagerService: NodesManagerService, - private dbObjectPageService: DBObjectPageService, - private notificationService: NotificationService, - private navigationTabsService: NavigationTabsService) { + constructor( + private navNodeManagerService: NavNodeManagerService, + private dbObjectService: DBObjectService, + private dbObjectPageService: DBObjectPageService, + private notificationService: NotificationService, + private navigationTabsService: NavigationTabsService + ) { this.tabHandler = this.navigationTabsService .registerTabHandler({ @@ -60,95 +62,9 @@ export class ObjectViewerTabService { }); } - getConnection(context: ITab) { - const nodeInfo = this.nodesManagerService - .getNodeContainerInfo(context.handlerState.objectId); - - if (!nodeInfo.connectionId) { - return; - } - // connection node id differs from connection id - return NodeManagerUtils.connectionNodeIdToConnectionId(nodeInfo.connectionId); - } - - getDBObjectCatalog(context: ITab) { - const nodeInfo = this.nodesManagerService - .getNodeContainerInfo(context.handlerState.objectId); - - if (nodeInfo.catalogId) { - return; - } - return nodeInfo.catalogId; - } - - getDBObjectSchema(context: ITab) { - const nodeInfo = this.nodesManagerService - .getNodeContainerInfo(context.handlerState.objectId); - - if (!nodeInfo.schemaId) { - return; - } - return nodeInfo.schemaId; - } - registerTabHandler() { - 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 || !NodeManagerUtils.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' - && (!tab.handlerState.tabIcon || typeof tab.handlerState.tabIcon === 'string') - && (!tab.handlerState.tabTitle || typeof tab.handlerState.tabTitle === 'string') - ) { - const node = await this.nodesManagerService.loadNodeInfo(tab.handlerState.objectId); - if (node) { - tab.handlerState.tabIcon = node.icon; - tab.handlerState.tabTitle = node.name; - - return this.dbObjectPageService.restorePages(tab); - } - } - return false; - } - - async closeObjectTab(tab: ITab) { - await this.dbObjectPageService.closePages(tab); + this.navNodeManagerService.navigator.addHandler(this.navigationHandler.bind(this)); + this.navNodeManagerService.navNode.onDataUpdate.subscribe(this.updateTabs.bind(this)); } objectViewerTabContext = async ( @@ -156,7 +72,7 @@ export class ObjectViewerTabService { data: INodeNavigationData ): Promise => { const tabInfo = await contexts.getContext(this.navigationTabsService.navigationTabContext); - const nodeInfo = await contexts.getContext(this.nodesManagerService.navigationNodeContext); + const nodeInfo = await contexts.getContext(this.navNodeManagerService.navigationNavNodeContext); if (data.type !== NavigationType.closeConnection) { // check if tab already exist for object @@ -173,6 +89,8 @@ export class ObjectViewerTabService { handlerId: objectViewerTabHandlerKey, handlerState: { objectId: nodeInfo.nodeId, + parentId: nodeInfo.parentId, + parents: await nodeInfo.getParents(), folderId: nodeInfo.folderId, pageId: '', pagesState: new Map(), @@ -214,6 +132,115 @@ export class ObjectViewerTabService { }; } + private async updateTabs(data: Map) { + for (const tab of this.navigationTabsService.findTabs(isObjectViewerTab(tab => tab.restored))) { + if (!data.has(tab.handlerState.objectId)) { + await this.navigationTabsService.closeTab(tab.id, true); + } else if (tab.id === this.navigationTabsService.currentTabId) { + const loaded = this.navNodeManagerService.navNode.isLoaded({ + nodes: [{ nodeId: tab.handlerState.objectId, parentId: tab.handlerState.parentId }], + }); + const loading = this.navNodeManagerService.navNode.isDataLoading({ + nodes: [{ nodeId: tab.handlerState.objectId, parentId: tab.handlerState.parentId }], + }); + + if (!loaded && !loading) { + await this.navigationTabsService.selectTab(tab.id); + } + } + } + } + + private getConnection(context: ITab) { + const nodeInfo = this.navNodeManagerService + .getNodeContainerInfo(context.handlerState.objectId); + + if (!nodeInfo.connectionId) { + return; + } + // connection node id differs from connection id + return NodeManagerUtils.connectionNodeIdToConnectionId(nodeInfo.connectionId); + } + + private getDBObjectCatalog(context: ITab) { + const nodeInfo = this.navNodeManagerService + .getNodeContainerInfo(context.handlerState.objectId); + + if (nodeInfo.catalogId) { + return; + } + return nodeInfo.catalogId; + } + + private getDBObjectSchema(context: ITab) { + const nodeInfo = this.navNodeManagerService + .getNodeContainerInfo(context.handlerState.objectId); + + if (!nodeInfo.schemaId) { + return; + } + return nodeInfo.schemaId; + } + + private 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.navNodeManagerService.loadNode({ + nodeId: tab.handlerState.objectId, + parentId: tab.handlerState.parentId, + }); + const children = await this.navNodeManagerService.loadTree(tab.handlerState.objectId); + await this.dbObjectService.load(tab.handlerState.objectId); + + const folderId = tab.handlerState.folderId; + + if (children.length === 0 || !NodeManagerUtils.isDatabaseObject(folderId)) { + return; + } + const folderChildren = await this.navNodeManagerService.loadTree(folderId); + + await this.dbObjectService.load(folderChildren); + } catch (exception) { + this.notificationService.logException(exception, 'Error in Object Viewer while tab selecting'); + } + } + + private async restoreObjectTab(tab: ITab) { + if ( + typeof tab.handlerState?.folderId === 'string' + && typeof tab.handlerState.parentId === 'string' + && Array.isArray(tab.handlerState.parents) + && typeof tab.handlerState.objectId === 'string' + && (!tab.handlerState.tabIcon || typeof tab.handlerState.tabIcon === 'string') + && (!tab.handlerState.tabTitle || typeof tab.handlerState.tabTitle === 'string') + ) { + for (const nodeId of tab.handlerState.parents) { + await this.navNodeManagerService.loadTree(nodeId); + } + + const node = await this.navNodeManagerService.loadNode({ + nodeId: tab.handlerState.objectId, + parentId: tab.handlerState.parentId, + }); + if (node) { + tab.handlerState.tabIcon = node.icon; + tab.handlerState.tabTitle = node.name; + + return this.dbObjectPageService.restorePages(tab); + } + } + return false; + } + + private async closeObjectTab(tab: ITab) { + await this.dbObjectPageService.closePages(tab); + } + private async navigationHandler(contexts: IContextProvider) { try { const { tab, tabInfo, nodeInfo } = await contexts.getContext(this.objectViewerTabContext); diff --git a/webapp/packages/object-viewer-plugin/src/useObjectFolder.ts b/webapp/packages/object-viewer-plugin/src/useObjectFolder.ts index d2e5cc3e47..61d3af9f1e 100644 --- a/webapp/packages/object-viewer-plugin/src/useObjectFolder.ts +++ b/webapp/packages/object-viewer-plugin/src/useObjectFolder.ts @@ -6,14 +6,18 @@ * you may not use this file except in compliance with the License. */ +import { NavNodeManagerService, DBObjectService } from '@dbeaver/core/app'; import { useService } from '@dbeaver/core/di'; -import { ObjectViewerTabService } from './ObjectViewerTabService'; -export function useObjectFolder(objectId: string, folderId: string) { - const objectViewerTabService = useService(ObjectViewerTabService); +export function useObjectFolder(objectId: string) { + const navNodeManagerService = useService(NavNodeManagerService); + const dbObjectService = useService(DBObjectService); - const isLoading = objectViewerTabService.isTabLoading(`${objectId}_${folderId}`); + const children = navNodeManagerService.getTree(objectId) || []; + + const isLoading = children.some(navNodeId => !dbObjectService.getDBObject(navNodeId)) + && dbObjectService.dbObject.isDataLoading({ navNodeId: children }); return { isLoading }; } diff --git a/webapp/packages/sql-editor/src/SqlEditorBootstrap.ts b/webapp/packages/sql-editor/src/SqlEditorBootstrap.ts index 9898a3b684..6393deb63c 100644 --- a/webapp/packages/sql-editor/src/SqlEditorBootstrap.ts +++ b/webapp/packages/sql-editor/src/SqlEditorBootstrap.ts @@ -11,7 +11,7 @@ import { NavigationTreeContextMenuService, EObjectFeature, NodeManagerUtils, - NodeWithParent, ConnectionsManagerService, + NavNode, ConnectionsManagerService, } from '@dbeaver/core/app'; import { injectable } from '@dbeaver/core/di'; import { ContextMenuService, IContextMenuItem, IMenuContext } from '@dbeaver/core/dialogs'; @@ -21,11 +21,13 @@ import { SqlEditorTabService } from './SqlEditorTabService'; @injectable() export class SqlEditorBootstrap { - constructor(private mainMenuService: MainMenuService, - private contextMenuService: ContextMenuService, - private connectionsManagerService: ConnectionsManagerService, - private sqlEditorTabService: SqlEditorTabService, - private sqlEditorNavigatorService: SqlEditorNavigatorService) {} + constructor( + private mainMenuService: MainMenuService, + private contextMenuService: ContextMenuService, + private connectionsManagerService: ConnectionsManagerService, + private sqlEditorTabService: SqlEditorTabService, + private sqlEditorNavigatorService: SqlEditorNavigatorService + ) {} async bootstrap() { this.sqlEditorTabService.registerTabHandler(); @@ -40,20 +42,20 @@ export class SqlEditorBootstrap { } ); - const openSqlEditor: IContextMenuItem = { + const openSqlEditor: IContextMenuItem = { id: 'open-sql-editor', isPresent(context) { return context.contextType === NavigationTreeContextMenuService.nodeContextType - && Boolean((context.data as NodeWithParent)?.object?.features?.includes(EObjectFeature.dataSource)); + && context.data.objectFeatures.includes(EObjectFeature.dataSource); }, title: 'SQL', order: 2, - onClick: (context: IMenuContext) => { + onClick: (context: IMenuContext) => { const node = context.data; const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(node.id); this.sqlEditorNavigatorService.openNewEditor(connectionId); }, }; - this.contextMenuService.addMenuItem(this.contextMenuService.getRootMenuToken(), openSqlEditor); + this.contextMenuService.addMenuItem(this.contextMenuService.getRootMenuToken(), openSqlEditor); } } diff --git a/webapp/packages/sql-editor/src/SqlEditorNavigatorService.ts b/webapp/packages/sql-editor/src/SqlEditorNavigatorService.ts index 2ecdf6047c..19b3269214 100644 --- a/webapp/packages/sql-editor/src/SqlEditorNavigatorService.ts +++ b/webapp/packages/sql-editor/src/SqlEditorNavigatorService.ts @@ -14,7 +14,7 @@ import { NavigationService, IContextProvider, ITabOptions, - NodesManagerService, + NavNodeManagerService, INodeNavigationData, NavigationType, } from '@dbeaver/core/app'; @@ -57,19 +57,21 @@ export interface SQLEditorAction extends SQLEditorActionContext { 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) { + constructor( + private navigationTabsService: NavigationTabsService, + private connectionsManagerService: ConnectionsManagerService, + private notificationService: NotificationService, + private gql: GraphQLService, + private sqlDialectInfoService: SqlDialectInfoService, + private navNodeManagerService: NavNodeManagerService, + private navigationService: NavigationService + ) { this.navigator = this.navigationService.createNavigator( null, this.navigateHandler.bind(this) ); - this.nodesManagerService.navigator.addHandler(this.nodeNavigationHandler.bind(this)); + this.navNodeManagerService.navigator.addHandler(this.nodeNavigationHandler.bind(this)); } registerTabHandler() { @@ -100,7 +102,7 @@ export class SqlEditorNavigatorService { private async nodeNavigationHandler(contexts: IContextProvider) { try { - const nodeInfo = await contexts.getContext(this.nodesManagerService.navigationNodeContext); + const nodeInfo = await contexts.getContext(this.navNodeManagerService.navigationNavNodeContext); if (nodeInfo.type === NavigationType.closeConnection) { for (const tab of this.navigationTabsService.findTabs(