From f73a2f8b02b38fc7f86de08fa69fa7e0f3a1b8bf Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 10 Sep 2021 19:04:50 +0300 Subject: [PATCH] feat: CB-1216 CB-1217 CB-1239 metadata editor update --- .../NavigationTreeNode/useNavigationNode.ts | 9 +- .../ConnectionSelector/ConnectionSelector.tsx | 2 +- webapp/packages/core-app/src/index.ts | 2 +- ...DBObjectService.ts => DBObjectResource.ts} | 2 +- .../NodesManager/NavNodeContextMenuService.ts | 8 +- .../shared/NodesManager/NavTreeResource.ts | 7 +- .../NodesManager/useDatabaseObjectInfo.ts | 10 +- .../core-blocks/src/ExceptionMessage.tsx | 5 +- .../core-blocks/src/Loader/Loader.tsx | 10 +- .../core-blocks/src/useMapResource.ts | 100 ++++++------ .../ConnectionExecutionContextResource.ts | 8 +- .../ConnectionAccess/ConnectionAccess.tsx | 4 +- .../src/Resource/CachedMapResource.ts | 3 + .../core-sdk/src/Resource/ResourceKeyList.ts | 19 +++ .../src/DataViewerTabService.ts | 2 +- .../src/DdlViewer/DDLViewerTabPanel.tsx | 10 +- .../FolderTabRenderer.tsx | 17 ++- .../NavNodeMetadata/ObjectProperties.tsx | 73 ++++----- .../VirtualFolder/VirtualFolderPanel.tsx | 24 ++- .../ObjectPropertiesPage/ObjectFolders.tsx | 65 +++++--- .../ObjectPropertiesPagePanel.tsx | 10 -- .../ObjectPropertyTable.tsx | 41 +++-- .../src/ObjectViewerPanel.tsx | 142 ++++++++++++------ .../src/ObjectViewerTabService.ts | 43 +----- .../src/useObjectFolder.ts | 8 +- 25 files changed, 357 insertions(+), 267 deletions(-) rename webapp/packages/core-app/src/shared/NodesManager/{DBObjectService.ts => DBObjectResource.ts} (96%) diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts index 642475db1a..d2790d13b8 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts @@ -31,16 +31,11 @@ interface INavigationNode { filterValue: string; } -export function useNavigationNode({ id }: NavNode): INavigationNode { +export function useNavigationNode(node: NavNode): INavigationNode { const contextRef = useObjectRef({ context: useContext(TreeContext), }); - const { node, isLoading } = useNode(id); - - // TODO: hack to provide actual node information - if (!node) { - throw new Error('Node should exists'); - } + const { isLoading } = useNode(node.id); const children = useChildren(node.id); const loading = isLoading() || children.isLoading(); diff --git a/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelector.tsx b/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelector.tsx index a061d0a014..732bc07161 100644 --- a/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelector.tsx +++ b/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelector.tsx @@ -58,7 +58,7 @@ export const ConnectionSelector = observer(function ConnectionSelector() { const style = useStyles(styles); const controller = useController(ConnectionSelectorController); const isEnabled = usePermission(EPermission.public); - const driver = useMapResource(DBDriverResource, null, { onLoad: resource => isEnabled && resource.loadAll() }); + const driver = useMapResource(DBDriverResource, null, { onLoad: resource => { isEnabled && resource.loadAll(); } }); const ConnectionMenu = TopMenuItem; const SchemaOrCatalogMenu = TopMenuItem; diff --git a/webapp/packages/core-app/src/index.ts b/webapp/packages/core-app/src/index.ts index 67949fbf65..7d28685eb7 100644 --- a/webapp/packages/core-app/src/index.ts +++ b/webapp/packages/core-app/src/index.ts @@ -14,7 +14,7 @@ export * from './shared/NodesManager/extensions/IObjectSchemaSetter'; export * from './shared/NodesManager/NavNodeView/IFolderTransform'; export * from './shared/NodesManager/NavNodeView/NavNodeViewService'; export * from './shared/NodesManager/ConnectionDialogsService'; -export * from './shared/NodesManager/DBObjectService'; +export * from './shared/NodesManager/DBObjectResource'; export * from './shared/NodesManager/NavNodeContextMenuService'; export * from './shared/NodesManager/NavNodeInfoResource'; export * from './shared/NodesManager/NavNodeManagerService'; diff --git a/webapp/packages/core-app/src/shared/NodesManager/DBObjectService.ts b/webapp/packages/core-app/src/shared/NodesManager/DBObjectResource.ts similarity index 96% rename from webapp/packages/core-app/src/shared/NodesManager/DBObjectService.ts rename to webapp/packages/core-app/src/shared/NodesManager/DBObjectResource.ts index a0e076dc7b..4bd76d9c1a 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/DBObjectService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/DBObjectResource.ts @@ -15,7 +15,7 @@ import type { DBObject } from './EntityTypes'; import { NavNodeInfoResource } from './NavNodeInfoResource'; @injectable() -export class DBObjectService extends CachedMapResource { +export class DBObjectResource extends CachedMapResource { constructor( private graphQLService: GraphQLService, private navNodeInfoResource: NavNodeInfoResource diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts index 3964828b37..4058873d06 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts @@ -195,7 +195,7 @@ export class NavNodeContextMenuService extends Bootstrap { onClick: async context => { const node = context.data.node; try { - await this.navNodeManagerService.refreshTree(node.id); + await this.navNodeManagerService.refreshTree(node.parentId); } catch (exception) { this.notificationService.logException(exception, 'Failed to refresh node'); } @@ -227,14 +227,10 @@ export class NavNodeContextMenuService extends Bootstrap { if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) { if (name !== result) { - const notification = this.notificationService.processNotification(() => ProcessSnackbar, {}, { title: 'ui_rename_processing' }); try { await this.navTreeResource.changeName(node, result); - - const message = `prev: ${name}\nnew: ${result}`; - notification.controller.resolve(`${node.nodeType} was renamed`, message); } catch (exception) { - notification.controller.reject(exception, 'Error occured while renaming'); + this.notificationService.logException(exception, 'Error occurred while renaming'); } } } diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts index baf63c5ee5..0d5eda994a 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts @@ -130,7 +130,7 @@ export class NavTreeResource extends CachedMapResource { } async changeName(node: NavNode, name: string): Promise { - await this.performUpdate(node.id, [], async () => { + await this.performUpdate(resourceKeyList([node.id, node.parentId]), [], async () => { await this.graphQLService.sdk.navRenameNode({ nodePath: node.id, newName: name, @@ -213,10 +213,11 @@ export class NavTreeResource extends CachedMapResource { for (const id of items) { this.data.delete(id); } + const allKeys = resourceKeyList(items); this.markUpdated(allKeys); this.onItemDelete.execute(allKeys); - this.navNodeInfoResource.delete(ResourceKeyUtils.exclude(allKeys, key)); + this.navNodeInfoResource.delete(allKeys); } protected async loader(key: ResourceKey): Promise> { @@ -263,7 +264,7 @@ export class NavTreeResource extends CachedMapResource { const connectionInfo = this.connectionInfo.get(key); if (!connectionInfo?.connected) { - this.delete(nodeId); + this.delete(resourceKeyList(this.get(nodeId) || [])); } else { await this.markTreeOutdated(nodeId); } diff --git a/webapp/packages/core-app/src/shared/NodesManager/useDatabaseObjectInfo.ts b/webapp/packages/core-app/src/shared/NodesManager/useDatabaseObjectInfo.ts index 3e27fe6006..f8588dc35b 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/useDatabaseObjectInfo.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/useDatabaseObjectInfo.ts @@ -8,13 +8,13 @@ import { useService } from '@cloudbeaver/core-di'; -import { DBObjectService } from './DBObjectService'; +import { DBObjectResource } from './DBObjectResource'; export function useDatabaseObjectInfo(navNodeId: string) { - const dbObjectService = useService(DBObjectService); - const dbObject = dbObjectService.get(navNodeId); - const isLoading = dbObjectService.isDataLoading(navNodeId); - const isLoaded = dbObjectService.isLoaded(navNodeId); + const dbObjectResource = useService(DBObjectResource); + const dbObject = dbObjectResource.get(navNodeId); + const isLoading = dbObjectResource.isDataLoading(navNodeId); + const isLoaded = dbObjectResource.isLoaded(navNodeId); return { dbObject, isLoading, isLoaded }; } diff --git a/webapp/packages/core-blocks/src/ExceptionMessage.tsx b/webapp/packages/core-blocks/src/ExceptionMessage.tsx index 25116c1411..89ed894adc 100644 --- a/webapp/packages/core-blocks/src/ExceptionMessage.tsx +++ b/webapp/packages/core-blocks/src/ExceptionMessage.tsx @@ -94,17 +94,18 @@ interface Props { message?: string; exception?: Error; inline?: boolean; + className?: string; onRetry?: () => void; } export const ExceptionMessage = observer(function ExceptionMessage({ - name, message, exception = null, inline, onRetry, + name, message, exception = null, inline, className, onRetry, }) { const translate = useTranslate(); const error = useErrorDetails(exception); return styled(styles)( - + {name || error.details?.name} diff --git a/webapp/packages/core-blocks/src/Loader/Loader.tsx b/webapp/packages/core-blocks/src/Loader/Loader.tsx index e39ef2206b..0b636594c5 100644 --- a/webapp/packages/core-blocks/src/Loader/Loader.tsx +++ b/webapp/packages/core-blocks/src/Loader/Loader.tsx @@ -11,7 +11,7 @@ import { useState, useEffect } from 'react'; import styled, { use } from 'reshadow'; import { Translate } from '@cloudbeaver/core-localization'; -import { useStyles } from '@cloudbeaver/core-theming'; +import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming'; import { Button } from '../Button'; import { ExceptionMessage } from '../ExceptionMessage'; @@ -49,6 +49,7 @@ interface Props { className?: string; fullSize?: boolean; state?: LoaderState | LoaderState[]; + style?: ComponentStyle; children?: () => React.ReactNode; onCancel?: () => void; } @@ -72,6 +73,7 @@ export const Loader = observer(function Loader({ loading = true, inlineException, state, + style, children, onCancel, }) { @@ -111,7 +113,7 @@ export const Loader = observer(function Loader({ } } - const style = useStyles(loaderStyles, overlay && overlayStyles); + style = useStyles(loaderStyles, style, overlay && overlayStyles); const [isVisible, setVisible] = useState(loading); const spinnerURL = (secondary || overlay) ? spinnerType.secondary : spinnerType.primary; @@ -128,7 +130,9 @@ export const Loader = observer(function Loader({ }, [loading]); if (exception && !loading) { - return ; + return styled(style)( + + ); } if (children && (!loader || !loading)) { diff --git a/webapp/packages/core-blocks/src/useMapResource.ts b/webapp/packages/core-blocks/src/useMapResource.ts index 23c8a0833c..678ad4fb68 100644 --- a/webapp/packages/core-blocks/src/useMapResource.ts +++ b/webapp/packages/core-blocks/src/useMapResource.ts @@ -6,6 +6,7 @@ * you may not use this file except in compliance with the License. */ +import { observable } from 'mobx'; import { useEffect, useState } from 'react'; import { IServiceConstructor, useService } from '@cloudbeaver/core-di'; @@ -13,15 +14,15 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { CachedResourceIncludeArgs, CachedMapResource, CachedMapResourceGetter, ResourceKey, CachedMapResourceValue, CachedMapResourceKey, CachedMapResourceArguments, CachedMapResourceLoader, ResourceKeyList, CachedMapResourceListGetter } from '@cloudbeaver/core-sdk'; import type { ILoadableState } from './Loader/Loader'; -import { useObjectRef } from './useObjectRef'; +import { useObservableRef } from './useObservableRef'; interface IActions< TKeyArg extends ResourceKey>, TResource extends CachedMapResource, TIncludes > { - isActive?: () => Promise | boolean; - onLoad?: (resource: TResource) => Promise | any; + isActive?: (resource: TResource) => Promise | boolean; + onLoad?: (resource: TResource) => Promise | boolean | void; onData?: ( data: CachedMapResourceLoader< TKeyArg, @@ -129,7 +130,7 @@ export function useMapResource< includes = keyObj.includes; } - const refObj = useObjectRef(() => ({ + const refObj = useObservableRef(() => ({ loading: false, prevData: undefined as CachedMapResourceLoader< TKeyArg, @@ -137,8 +138,52 @@ export function useMapResource< CachedMapResourceValue, TIncludes > | undefined, - load: () => {}, + async load() { + const { resource, actions, prevData, key, includes } = this; + + const active = await actions?.isActive?.(resource); + + if (this.loading || active === false) { + return; + } + + this.loading = true; + + try { + const prevent = await actions?.onLoad?.(resource); + + if (key === null || prevent === true) { + setException(null); + return; + } + + const newData = await resource.load(key, includes as any); + setException(null); + + try { + await actions?.onData?.( + newData, + resource, + prevData + ); + } finally { + this.prevData = newData; + } + } catch (exception) { + if (resource.getException(key) === null) { + setException(exception); + } + actions?.onError?.(exception); + if (!this.exceptionObserved) { + notifications.logException(exception, 'Can\'t load data'); + } + } finally { + this.loading = false; + } + }, }), { + loading: observable.ref, + }, { exceptionObserved: false, resource, key, @@ -149,49 +194,6 @@ export function useMapResource< const outdated = resource.isOutdated(key); - refObj.load = async function load() { - const { loading, resource, actions, prevData } = refObj; - - const active = await actions?.isActive?.(); - - if (loading || active === false) { - return; - } - - this.loading = true; - - try { - await actions?.onLoad?.(resource); - - if (key === null) { - return; - } - - const newData = await resource.load(key, includes as any); - setException(null); - - try { - await actions?.onData?.( - newData, - resource, - prevData - ); - } finally { - refObj.prevData = newData; - } - } catch (exception) { - if (resource.getException(key) === null) { - setException(exception); - } - actions?.onError?.(exception); - if (!refObj.exceptionObserved) { - notifications.logException(exception, 'Can\'t load data'); - } - } finally { - this.loading = false; - } - }; - const [result] = useState< IMapResourceResult | IMapResourceListResult @@ -226,7 +228,7 @@ export function useMapResource< return false; } - return resource.isDataLoading(refObj.key); + return refObj.loading || resource.isDataLoading(refObj.key); }, })); diff --git a/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextResource.ts b/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextResource.ts index c1786548bb..05bde70ffd 100644 --- a/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextResource.ts +++ b/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContextResource.ts @@ -7,7 +7,7 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import { SessionDataResource } from '@cloudbeaver/core-root'; +import { EPermission, PermissionsResource, SessionDataResource } from '@cloudbeaver/core-root'; import { GraphQLService, CachedMapResource, @@ -30,6 +30,7 @@ export class ConnectionExecutionContextResource extends CachedMapResource { await this.load(ConnectionExecutionContextResource.keyAll); }); + this.permissionsResource.onDataOutdated.addHandler(this.markOutdated.bind(this)); connectionInfoResource.onItemAdd.addHandler(this.updateConnectionContexts.bind(this)); connectionInfoResource.onItemDelete.addHandler(this.deleteConnectionContexts.bind(this)); @@ -168,6 +170,10 @@ export class ConnectionExecutionContextResource extends CachedMapResource ): Promise> { + if (!this.permissionsResource.has(EPermission.public)) { + return this.data; + } + const all = ResourceKeyUtils.hasMark(key, ConnectionExecutionContextResource.keyAll.mark); await ResourceKeyUtils.forEachAsync(all ? ConnectionExecutionContextResource.keyAll : key, async contextId => { diff --git a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx index a381fc8b2a..5690343916 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/ConnectionAccess/ConnectionAccess.tsx @@ -75,11 +75,11 @@ export const ConnectionAccess: TabContainerPanelComponent || state.initialGrantedSubjects.some(subject => !state.grantedSubjects.includes(subject)); const users = useMapResource(UsersResource, null, { - onLoad: resource => resource.loadAll(), + onLoad: resource => { resource.loadAll(); }, }); const roles = useMapResource(RolesResource, null, { - onLoad: resource => resource.loadAll(), + onLoad: resource => { resource.loadAll(); }, }); const grantedUsers = useMemo(() => computed(() => users.resource.values diff --git a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts index d22e21f46d..5529e4c31d 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts @@ -107,6 +107,9 @@ export abstract class CachedMapResource< getException(key: ResourceKeyList): Array; getException(key: ResourceKey): Array| Error | null; getException(key: ResourceKey): Array| Error | null { + if (ResourceKeyUtils.some(key, key => !this.has(key))) { // TODO: metadata isn't properly cleared while rename with opened in Metadata Editor node + return null; + } key = this.transformParam(key); return ResourceKeyUtils.map(key, key => this.metadata.get(key).exception); } diff --git a/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts b/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts index cd45d38608..e0fdf30313 100644 --- a/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts +++ b/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts @@ -47,6 +47,7 @@ export interface ResourceKeyUtils { map: MapFnc; includes: (first: ResourceKey, second: ResourceKey) => boolean; exclude: (first: ResourceKeyList, second: ResourceKey) => ResourceKey; + join: (first: ResourceKey, second: ResourceKey) => ResourceKey; } // eslint-disable-next-line @typescript-eslint/no-redeclare @@ -151,6 +152,24 @@ export const ResourceKeyUtils: ResourceKeyUtils = { return resourceKeyList(param.list.filter(param => param !== key), param.mark); }, + + join(param: ResourceKey, key: ResourceKey): ResourceKey { + const list: TKey[] = []; + + if (isResourceKeyList(param)) { + list.push(...param.list); + } else { + list.push(param); + } + + if (isResourceKeyList(key)) { + list.push(...key.list); + } else { + list.push(key); + } + + return resourceKeyList(list); + }, }; export function isResourceKeyList(data: any): data is ResourceKeyList { diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts index 997f009d6c..d3f2c7f84d 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerTabService.ts @@ -77,7 +77,7 @@ export class DataViewerTabService { return; } - const node = await this.navNodeManagerService.loadNode({ + const node = this.navNodeManagerService.getNode({ nodeId: tab.handlerState.objectId, parentId: tab.handlerState.parentId, }); diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx index 4e275b07e8..b8d57048a7 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewer/DDLViewerTabPanel.tsx @@ -7,10 +7,11 @@ */ import { observer } from 'mobx-react-lite'; +import { useEffect } from 'react'; import styled, { css } from 'reshadow'; import type { NavNodeTransformViewComponent } from '@cloudbeaver/core-app'; -import { Loader, useTab } from '@cloudbeaver/core-blocks'; +import { Loader } from '@cloudbeaver/core-blocks'; import { useController } from '@cloudbeaver/core-di'; import { SQLCodeEditorLoader } from '@cloudbeaver/plugin-sql-editor'; @@ -32,7 +33,12 @@ const styles = css` export const DDLViewerTabPanel: NavNodeTransformViewComponent = observer(function DDLViewerTabPanel({ nodeId, folderId }) { const controller = useController(DdlViewerController, nodeId); - useTab(folderId, () => controller.load()); + + useEffect(() => { + controller.load(); + }); + // TODO: not triggered in switch case with lazy + // useTab(folderId, () => controller.load()); if (controller.isLoading) { return ; diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/FolderTabRenderer.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/FolderTabRenderer.tsx index 84d7cd05b9..c5b1a3b476 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/FolderTabRenderer.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/FolderTabRenderer.tsx @@ -9,9 +9,10 @@ import { observer } from 'mobx-react-lite'; import styled from 'reshadow'; -import { NavNodeViewService, useNode } from '@cloudbeaver/core-app'; -import { Tab, TabIcon, TabTitle } from '@cloudbeaver/core-blocks'; +import { DBObjectResource, NavNodeViewService, useChildren, useNode } from '@cloudbeaver/core-app'; +import { getComputed, Loader, Tab, TabIcon, TabTitle, useStateDelay } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; +import { resourceKeyList } from '@cloudbeaver/core-sdk'; import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming'; interface IFolderTabRendererProps { @@ -45,11 +46,23 @@ interface INavNodeTabProps { const NavNodeTab = observer(function NavNodeTab({ nodeId, style }) { const nodeInfo = useNode(nodeId); + const children = useChildren(nodeId); + const dbObjectResource = useService(DBObjectResource); + const childrenList = resourceKeyList(children.children || []); + + const loading = useStateDelay(getComputed(() => ( + nodeInfo.isLoaded() && nodeInfo.isLoading() + ) || ( + children.isLoaded() && children.isLoading() + ) || ( + dbObjectResource.isLoaded(childrenList) && dbObjectResource.isDataLoading(childrenList) + )), 300); return styled(useStyles(style))( {nodeInfo.node?.icon && } {nodeInfo.node?.name} + ); }); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/NavNodeMetadata/ObjectProperties.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/NavNodeMetadata/ObjectProperties.tsx index 418ba2ac18..2256a60502 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/NavNodeMetadata/ObjectProperties.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/NavNodeMetadata/ObjectProperties.tsx @@ -9,8 +9,8 @@ import { observer } from 'mobx-react-lite'; import styled from 'reshadow'; -import { useDatabaseObjectInfo } from '@cloudbeaver/core-app'; -import { ColoredContainer, Loader, TextPlaceholder, useObjectPropertyCategories, GroupTitle, ObjectPropertyInfoFormNew, Group } from '@cloudbeaver/core-blocks'; +import { DBObjectResource } from '@cloudbeaver/core-app'; +import { ColoredContainer, Loader, TextPlaceholder, useObjectPropertyCategories, GroupTitle, ObjectPropertyInfoFormNew, Group, useMapResource } from '@cloudbeaver/core-blocks'; import { BASE_CONTAINERS_STYLES } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; @@ -26,42 +26,43 @@ export const ObjectProperties = observer(function ObjectProperties({ objectId, }) { const translate = useTranslate(); - const { dbObject, isLoading } = useDatabaseObjectInfo(objectId); + const dbObject = useMapResource(DBObjectResource, objectId); const styles = useStyles(BASE_CONTAINERS_STYLES); - const { categories, isUncategorizedExists } = useObjectPropertyCategories(dbObject?.properties ?? emptyArray); - const properties = dbObject?.properties; - - if (!properties && isLoading) { - return ; - } - - if (!properties || properties.length === 0) { - return {translate('plugin_object_viewer_table_no_items')}; - } + const { categories, isUncategorizedExists } = useObjectPropertyCategories(dbObject.data?.properties ?? emptyArray); + const properties = dbObject.data?.properties; return styled(styles)( - - {isUncategorizedExists && ( - - - - )} - {categories.map(category => ( - - {category} - - - ))} - + {() => styled(styles)( + <> + {!properties || properties.length === 0 ? ( + {translate('plugin_object_viewer_table_no_items')} + ) : ( + + {isUncategorizedExists && ( + + + + )} + {categories.map(category => ( + + {category} + + + ))} + + )} + + )} + ); }); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx index 7d16bdfd3d..f6a0c66bd0 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx @@ -8,7 +8,8 @@ import styled, { css } from 'reshadow'; -import { NavNodeInfoResource, NavNodeTransformViewComponent, useChildren } from '@cloudbeaver/core-app'; +import { DBObjectResource, NavNodeInfoResource, NavNodeTransformViewComponent, NavTreeResource } from '@cloudbeaver/core-app'; +import { Loader, useMapResource } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { resourceKeyList } from '@cloudbeaver/core-sdk'; @@ -29,16 +30,27 @@ export const VirtualFolderPanel: NavNodeTransformViewComponent = function Virtua }) { const nodeType = VirtualFolderUtils.getNodeType(folderId); const navNodeInfoResource = useService(NavNodeInfoResource); - const children = useChildren(nodeId); + const tree = useMapResource(NavTreeResource, nodeId); + const key = resourceKeyList([nodeId, ...tree.data || []]); + const dbObject = useMapResource(DBObjectResource, key, { + async onLoad(resource: DBObjectResource) { + await resource.loadChildren(nodeId, key); + }, + }); const nodeIds = navNodeInfoResource - .get(resourceKeyList(children.children || [])) + .get(resourceKeyList(tree.data || [])) .filter(node => node?.nodeType === nodeType) .map(node => node!.id); return styled(style)( - - - + {() => styled(style)( + <> + + + + + )} + ); }; diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectFolders.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectFolders.tsx index b12d7a4501..8c8ee429dd 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectFolders.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectFolders.tsx @@ -11,8 +11,9 @@ import { useEffect } from 'react'; import styled, { css } from 'reshadow'; import { ITab, NavNodeManagerService, NavNodeViewService, NavTreeResource } from '@cloudbeaver/core-app'; -import { ITabData, TabList, TabPanel, TabsState, useMapResource, verticalTabStyles } from '@cloudbeaver/core-blocks'; +import { ITabData, Loader, TabList, TabPanel, TabsState, TextPlaceholder, useMapResource, verticalTabStyles } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; +import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles, composes } from '@cloudbeaver/core-theming'; import type { IObjectViewerTabState } from '../IObjectViewerTabState'; @@ -45,6 +46,19 @@ const styles = composes( TabList { border-right: 1px solid; } + TabTitle { + flex: 1; + } + tab-loader { + width: 16px; + height: 16px; + flex-shrink: 0; + margin-right: 16px; + overflow: hidden; + } + ExceptionMessage { + padding: 24px; + } ` ); @@ -55,20 +69,16 @@ interface IProps { } export const ObjectFolders = observer(function ObjectFolders({ tab }) { + const translate = useTranslate(); const navNodeManagerService = useService(NavNodeManagerService); const navNodeViewService = useService(NavNodeViewService); + const style = useStyles(verticalTabStyles, styles); const nodeId = tab.handlerState.objectId; const parentId = tab.handlerState.parentId; let folderId = tab.handlerState.folderId; - useMapResource(NavTreeResource, nodeId, { - onLoad: async resource => { - for (const nodeId of tab.handlerState.parents) { - await resource.load(nodeId); - } - }, - }); + const children = useMapResource(NavTreeResource, nodeId); const folders = navNodeViewService.getFolders(nodeId) || []; @@ -86,20 +96,29 @@ export const ObjectFolders = observer(function ObjectFolders({ tab }) { folderId = folders[0]; } - return styled(useStyles(verticalTabStyles, styles))( - - - - {folders.map(folderId => ( - - ))} - - {folders.map(folderId => ( - - - - ))} - - + return styled(style)( + {() => styled(style)( + <> + {folders.length > 0 ? ( + + + + {folders.map(folderId => ( + + ))} + + {folders.map(folderId => ( + + + + ))} + + + ) : ( + {translate('plugin_object_viewer_table_no_items')} + )} + + )} + ); }); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx index 0237568530..6232e51dc9 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx @@ -37,16 +37,6 @@ export const ObjectPropertiesPagePanel: ObjectPagePanelComponent = observer(func tab, }) { const style = useStyles(viewerStyles); - const translate = useTranslate(); - const { children, isLoading } = useChildren(tab.handlerState.objectId); - - if (!children && isLoading()) { - return ; - } - - if (!children) { - return {translate('plugin_object_viewer_table_no_items')}; - } return styled(style)( diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx index b8f0aade55..9a63bd7621 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx @@ -9,12 +9,12 @@ import { observer } from 'mobx-react-lite'; import styled from 'reshadow'; -import { useChildren } from '@cloudbeaver/core-app'; -import { Loader, TextPlaceholder } from '@cloudbeaver/core-blocks'; +import { DBObjectResource, NavTreeResource } from '@cloudbeaver/core-app'; +import { Loader, TextPlaceholder, useMapResource } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; +import { resourceKeyList } from '@cloudbeaver/core-sdk'; import { css } from '@reshadow/react'; -import { useObjectFolder } from '../../useObjectFolder'; import { ObjectChildrenPropertyTable } from './ObjectChildrenPropertyTable'; const styles = css` @@ -22,6 +22,9 @@ const styles = css` flex: auto; overflow: hidden; } + ExceptionMessage { + padding: 24px; + } `; interface ObjectPropertyTableProps { @@ -34,21 +37,27 @@ export const ObjectPropertyTable = observer(function O parentId, }) { const translate = useTranslate(); - const children = useChildren(objectId); - const { isLoading } = useObjectFolder(objectId); - - if ((!children.children && children.isLoading()) || isLoading) { - return ; - } - - if (!children?.children || !children.children.length) { - return {translate('plugin_object_viewer_table_no_items')}; - } + const tree = useMapResource(NavTreeResource, objectId); + const key = resourceKeyList([objectId, ...tree.data || []]); + const dbObject = useMapResource(DBObjectResource, key, { + async onLoad(resource: DBObjectResource) { + await resource.loadChildren(objectId, key); + }, + }); return styled(styles)( -
- -
+ {() => styled(styles)( + <> + {!tree.data || tree.data.length === 0 ? ( + {translate('plugin_object_viewer_table_no_items')} + ) : ( +
+ +
+ )} + + )} +
); }); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx b/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx index 5c499420e8..83eb869ed3 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectViewerPanel.tsx @@ -6,13 +6,14 @@ * you may not use this file except in compliance with the License. */ +import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useCallback, useState } from 'react'; +import { useCallback } from 'react'; import styled, { css } from 'reshadow'; -import { useChildren, TabHandlerPanelComponent } from '@cloudbeaver/core-app'; -import { Loader, TabsBox, TabPanel, TextPlaceholder, Button } from '@cloudbeaver/core-blocks'; -import { useConnectionInfo } from '@cloudbeaver/core-connections'; +import { TabHandlerPanelComponent, NavTreeResource, NavNodeInfoResource } from '@cloudbeaver/core-app'; +import { Loader, TabsBox, TabPanel, TextPlaceholder, Button, useMapResource, useObservableRef } from '@cloudbeaver/core-blocks'; +import { ConnectionInfoResource, ConnectionsManagerService } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles, composes } from '@cloudbeaver/core-theming'; @@ -35,6 +36,9 @@ const styles = composes( tab-outer:only-child { display: none; } + ExceptionMessage { + padding: 24px; + } ` ); @@ -42,26 +46,72 @@ export const ObjectViewerPanel: TabHandlerPanelComponent tab, }) { const translate = useTranslate(); - const [connecting, setConnecting] = useState(false); - const connection = useConnectionInfo(tab.handlerState.connectionId || ''); const style = useStyles(styles); - const { - children, isOutdated, isLoading, isLoaded, - } = useChildren(tab.handlerState.objectId); const dbObjectPagesService = useService(DBObjectPageService); + const connectionsManagerService = useService(ConnectionsManagerService); + const navNodeInfoResource = useService(NavNodeInfoResource); + + const state = useObservableRef(() => ({ + connecting: false, + notFound: false, + }), { + connecting: observable.ref, + notFound: observable.ref, + }, false); + + const connection = useMapResource(ConnectionInfoResource, tab.handlerState.connectionId || null, { + isActive: resource => !tab.handlerState.connectionId || !resource.has(tab.handlerState.connectionId), + }); + + const children = useMapResource(NavTreeResource, tab.handlerState.parentId, { + onLoad: async resource => { + if (tab.handlerState.parents.length === 0) { + return true; + } + + const first = tab.handlerState.parents[0]; + await resource.load(first); + + for (const nodeId of tab.handlerState.parents) { + if (!navNodeInfoResource.has(nodeId)) { + state.notFound = true; + return true; + } + await resource.load(nodeId); + } + state.notFound = false; + return false; + }, + isActive: () => connection.data?.connected || false, + }); + + const dataPreloaded = children.isLoaded() && !!children.data?.includes(tab.handlerState.objectId) && !state.notFound; + + const node = useMapResource(navNodeInfoResource, dataPreloaded ? tab.handlerState.objectId : null, { + onData(data) { + tab.handlerState.tabIcon = data.icon; + tab.handlerState.tabTitle = data.name; + }, + }); + const pages = dbObjectPagesService.orderedPages; const handleConnect = useCallback(async () => { - setConnecting(true); - try { - await connection.connect(); - } finally { - setConnecting(false); + if (state.connecting || !connection.data) { + return; } - }, [connection]); - if (connection.connectionInfo && !connection.connectionInfo.connected) { - if (connecting || connection.isLoading()) { + state.connecting = true; + + try { + await connectionsManagerService.requireConnection(connection.data.id); + } finally { + state.connecting = false; + } + }, []); + + if (connection.data && !connection.data.connected) { + if (state.connecting || connection.isLoading()) { return ; } @@ -72,37 +122,39 @@ export const ObjectViewerPanel: TabHandlerPanelComponent ); } - if (tab.handlerState.error) { + if (tab.handlerState.error || state.notFound) { return {translate('plugin_object_viewer_error')}; } - if (!isLoaded() || (!isOutdated() && isLoading())) { - return ; - } - - if (!children) { - return {translate('plugin_object_viewer_table_no_items')}; - } - return styled(style)( - ( - - ))} - style={styles} - > - {pages.map(page => ( - - - - ))} - + + {() => styled(style)( + <> + {node.data ? ( + ( + + ))} + style={styles} + > + {pages.map(page => ( + + + + ))} + + ) : ( + {translate('plugin_object_viewer_table_no_items')} + )} + + )} + ); }); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts b/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts index 6d0227c5da..bed8deeca7 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectViewerTabService.ts @@ -11,17 +11,15 @@ import { INodeNavigationData, ITab, TabHandler, - NodeManagerUtils, objectCatalogProvider, objectSchemaProvider, - NavNodeManagerService, - DBObjectService + NavNodeManagerService } from '@cloudbeaver/core-app'; import { connectionProvider, ConnectionInfoResource, Connection } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import type { IAsyncContextLoader, IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; +import { ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; import type { IObjectViewerTabContext } from './IObjectViewerTabContext'; import type { IObjectViewerTabState } from './IObjectViewerTabState'; @@ -37,7 +35,6 @@ export class ObjectViewerTabService { constructor( private navNodeManagerService: NavNodeManagerService, - private dbObjectService: DBObjectService, private dbObjectPageService: DBObjectPageService, private notificationService: NotificationService, private navigationTabsService: NavigationTabsService, @@ -291,51 +288,15 @@ export class ObjectViewerTabService { } } - for (const nodeId of tab.handlerState.parents) { - await this.navNodeManagerService.loadTree(nodeId); - } - - // TODO: must be loaded by info folder? - const node = await this.navNodeManagerService.loadNode({ - nodeId: tab.handlerState.objectId, - parentId: tab.handlerState.parentId, - }); - const currentPage = this.dbObjectPageService.getPage(tab.handlerState.pageId); if (currentPage) { await this.dbObjectPageService.selectPage(tab, currentPage); } - if (node) { - tab.handlerState.tabIcon = node.icon; - tab.handlerState.tabTitle = node.name; - } - - await this.dbObjectService.load(tab.handlerState.objectId); - const children = await this.navNodeManagerService.loadTree(tab.handlerState.objectId); - if (tab.handlerState.childrenError) { return; } - - try { - const folderId = tab.handlerState.folderId; - - if (children.length === 0 || !NodeManagerUtils.isDatabaseObject(folderId)) { - await this.dbObjectService.loadChildren(tab.handlerState.objectId, resourceKeyList(children)); - return; - } - const folderChildren = await this.navNodeManagerService.loadTree(folderId); - - await this.dbObjectService.loadChildren(folderId, resourceKeyList(folderChildren)); - } catch (exception) { - if (tab.handlerState.childrenError) { - return; - } - tab.handlerState.childrenError = true; - this.notificationService.logException(exception, 'Object Viewer Error', 'Error in Object Viewer while folder selecting'); - } } catch (exception) { tab.handlerState.error = true; this.notificationService.logException(exception, 'Object Viewer Error', 'Error in Object Viewer while tab selecting'); diff --git a/webapp/packages/plugin-object-viewer/src/useObjectFolder.ts b/webapp/packages/plugin-object-viewer/src/useObjectFolder.ts index ae7ca38d66..b3973a53ba 100644 --- a/webapp/packages/plugin-object-viewer/src/useObjectFolder.ts +++ b/webapp/packages/plugin-object-viewer/src/useObjectFolder.ts @@ -6,18 +6,18 @@ * you may not use this file except in compliance with the License. */ -import { NavNodeManagerService, DBObjectService } from '@cloudbeaver/core-app'; +import { NavNodeManagerService, DBObjectResource } from '@cloudbeaver/core-app'; import { useService } from '@cloudbeaver/core-di'; import { resourceKeyList } from '@cloudbeaver/core-sdk'; export function useObjectFolder(objectId: string) { const navNodeManagerService = useService(NavNodeManagerService); - const dbObjectService = useService(DBObjectService); + const dbObjectResource = useService(DBObjectResource); const children = navNodeManagerService.getTree(objectId) || []; - const isLoading = !dbObjectService.isLoaded(resourceKeyList(children)) - && dbObjectService.isDataLoading(resourceKeyList(children)); + const isLoading = !dbObjectResource.isLoaded(resourceKeyList(children)) + && dbObjectResource.isDataLoading(resourceKeyList(children)); return { isLoading }; }