From 4d7c1fe4a6bb50a8e69935282c4d73db9ce25a3c Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 12 May 2021 18:59:39 +0300 Subject: [PATCH 01/16] fix(plugin-authentication): CB-938 pass linking param for default login --- webapp/packages/plugin-authentication/src/PluginBootstrap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/packages/plugin-authentication/src/PluginBootstrap.ts b/webapp/packages/plugin-authentication/src/PluginBootstrap.ts index 11dd05a769..b5ac144b90 100644 --- a/webapp/packages/plugin-authentication/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-authentication/src/PluginBootstrap.ts @@ -41,7 +41,7 @@ export class PluginBootstrap extends Bootstrap { order: 0, isHidden: () => this.serverConfigResource.enabledAuthProviders.length === 0 || !!this.authInfoService.userInfo, title: 'authentication_login', - onClick: () => this.authDialogService.showLoginForm(), + onClick: () => this.authDialogService.showLoginForm(false, null, true), } ); From 489178158fab9412eaf545c72a8a73277a0c6fdc Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 12 May 2021 22:22:06 +0300 Subject: [PATCH 02/16] fix(plugin-data-viewer): CB-752 update actions result --- .../src/DatabaseDataModel/DatabaseDataActions.ts | 12 +++++++++--- .../src/DatabaseDataModel/DatabaseDataSource.ts | 6 +++--- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts index ce1b9f783c..7761e50286 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataActions.ts @@ -50,11 +50,17 @@ implements IDatabaseDataActions { } updateResults(results: TResult[]): void { - const keys = Array.from(this.actions.keys()); + const actionsMap = Array.from(this.actions.entries()); - for (const key of keys) { - if (!results.some(result => result.id === key)) { + for (const [key, actions] of actionsMap) { + const result = results.find(result => result.id === key); + + if (!result) { this.actions.delete(key); + } else { + for (const action of actions.values()) { + action.updateResult(result); + } } } } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts index 6914d280dc..7677a780d6 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts @@ -118,6 +118,7 @@ implements IDatabaseDataSource { setResults(results: TResult[]): this { this.editor?.cancelChanges(); + this.actions.updateResults(results); this.results = results; return this; } @@ -194,8 +195,7 @@ implements IDatabaseDataSource { if (promise instanceof Promise) { this.activeRequest = promise; } - this.editor?.cancelChanges(); - this.results = await promise; + this.setResults(await promise); } finally { this.activeRequest = null; } @@ -220,7 +220,7 @@ implements IDatabaseDataSource { if (promise instanceof Promise) { this.activeSave = promise; } - this.results = await promise; + this.setResults(await promise); } finally { this.activeSave = null; } From 8aed3130a5f1d13cf84a1b2f8dbcea7b116fa6de Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 13 May 2021 19:53:26 +0300 Subject: [PATCH 03/16] refactor: CB-874 navigation tree --- .../src/NavigationTree/ElementsTree.tsx | 70 +++--- .../src/NavigationTree/NavigationTree.tsx | 6 +- .../NavigationTreeNode/NavigationNode.tsx | 7 +- .../NavigationNode/NavigationNodeControl.tsx | 4 +- .../NavigationNode/NavigationNodeNested.tsx | 46 ++-- .../TreeNodeMenu/treeNodeMenuStyles.ts | 2 +- .../NavigationTreeNode/useNavigationNode.ts | 67 ++--- .../NavigationTree/NavigationTreeService.ts | 41 ++- .../src/NavigationTree/TreeContext.ts | 10 +- .../NavigationTree/elementsTreeNameFilter.ts | 35 +++ .../src/NavigationTree/useElementsTree.ts | 238 ++++++++++++++++++ .../src/NavigationTree/useNavigationTree.ts | 37 ++- webapp/packages/core-app/src/index.ts | 1 + .../NodesManager/NavNodeInfoResource.ts | 2 +- .../NodesManager/NavNodeManagerService.ts | 49 +--- .../shared/NodesManager/NavTreeResource.ts | 19 ++ .../src/shared/NodesManager/useNode.ts | 41 ++- .../src/Tree/TreeNode/TreeNode.tsx | 61 ++--- .../src/Tree/TreeNode/TreeNodeContext.ts | 2 +- .../src/Tree/TreeNode/TreeNodeControl.tsx | 5 +- .../src/Tree/TreeNode/TreeNodeExpand.tsx | 5 +- .../src/Tree/TreeNode/TreeNodeFilter.tsx | 7 +- .../src/Tree/TreeNode/TreeNodeSelect.tsx | 29 +-- 23 files changed, 503 insertions(+), 281 deletions(-) create mode 100644 webapp/packages/core-app/src/NavigationTree/elementsTreeNameFilter.ts create mode 100644 webapp/packages/core-app/src/NavigationTree/useElementsTree.ts diff --git a/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx b/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx index f3f6303164..1ddd4ff9aa 100644 --- a/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx +++ b/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx @@ -7,18 +7,22 @@ */ import { observer } from 'mobx-react-lite'; -import { useCallback, useMemo, useState } from 'react'; +import { useMemo } from 'react'; import styled, { css } from 'reshadow'; import { Loader, useMapResource } from '@cloudbeaver/core-blocks'; -import { MetadataMap } from '@cloudbeaver/core-utils'; +import { useService } from '@cloudbeaver/core-di'; +import type { MetadataMap } from '@cloudbeaver/core-utils'; import type { NavNode } from '../shared/NodesManager/EntityTypes'; -import { ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource'; +import { NavNodeInfoResource, ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource'; import { NavTreeResource } from '../shared/NodesManager/NavTreeResource'; import { useChildren } from '../shared/useChildren'; +import { elementsTreeNameFilter } from './elementsTreeNameFilter'; +import { NavigationNodeNested } from './NavigationTreeNode/NavigationNode/NavigationNodeNested'; import { NavigationNodeElement } from './NavigationTreeNode/NavigationNodeElement'; import { ITreeContext, TreeContext } from './TreeContext'; +import { IElementsTreeFilter, ITreeNodeState, useElementsTree } from './useElementsTree'; const styles = css` tree { @@ -34,10 +38,6 @@ const styles = css` } `; -export interface ITreeNodeState { - filter: string; -} - interface Props { root?: string; selectionTree?: boolean; @@ -47,61 +47,63 @@ interface Props { }>; emptyPlaceholder: React.FC; className?: string; + filters?: IElementsTreeFilter[]; + customSelect?: (node: NavNode, multiple: boolean) => void; + isGroup?: (node: NavNode) => boolean; + onExpand?: (node: NavNode, state: boolean) => Promise | void; onOpen?: (node: NavNode) => Promise | void; - onSelect?: (node: NavNode, multiple: boolean) => void; - isSelected?: (node: NavNode) => boolean; + onSelect?: (node: NavNode, state: boolean) => void; onFilter?: (node: NavNode, value: string) => void; } export const ElementsTree: React.FC = observer(function ElementsTree({ - root, + root = ROOT_NODE_PATH, control, localState, selectionTree = false, emptyPlaceholder, + filters, className, + isGroup, + customSelect, + onExpand, onOpen, onSelect, - isSelected, onFilter, }) { const nodeChildren = useChildren(root); const Placeholder = emptyPlaceholder; - useMapResource(NavTreeResource, root || ROOT_NODE_PATH); - const [localTreeNodesState] = useState(() => new MetadataMap(() => ({ filter: '' }))); + const navNodeInfoResource = useService(NavNodeInfoResource); - const treeNodesState = localState || localTreeNodesState; + useMapResource(NavTreeResource, root); - const getTreeNodeState = useCallback((node: NavNode) => - treeNodesState.get(node.id), [treeNodesState]); + const nameFilter = useMemo(() => elementsTreeNameFilter(navNodeInfoResource), [navNodeInfoResource]); - const onFilterHandler = useCallback((node: NavNode, value: string) => { - const treeNodeState = treeNodesState.get(node.id); - treeNodeState.filter = value; - - if (onFilter) { - onFilter(node, value); - } - }, [treeNodesState, onFilter]); + const tree = useElementsTree({ + root, + localState, + filters: [nameFilter, ...(filters || [])], + isGroup, + onFilter, + customSelect, + onExpand, + onSelect, + }); const context = useMemo( () => ({ - treeNodesState, - getTreeNodeState, + tree, selectionTree, control, onOpen, - onSelect, - isSelected, - onFilter: onFilterHandler, }), - [control, selectionTree, onOpen, onSelect, isSelected, onFilterHandler, treeNodesState, getTreeNodeState] + [control, selectionTree, onOpen] ); if (!nodeChildren.children || nodeChildren.children.length === 0) { if (nodeChildren.isLoading()) { return styled(styles)( -
+
); @@ -112,10 +114,8 @@ export const ElementsTree: React.FC = observer(function ElementsTree({ return styled(styles)( - - {nodeChildren.children.map(id => ( - - ))} + + diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx index d2658b9c8d..36a1d38eee 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx @@ -58,7 +58,7 @@ export const NavigationTree = observer(function NavigationTree() { const [onFocus, onBlur] = useActiveView(navTreeService.getView); const [ref] = useFocus({ onFocus, onBlur }); const isEnabled = usePermission(EPermission.public); - const { isSelected, handleOpen, handleSelect } = useNavigationTree(); + const { handleOpen, handleSelect } = useNavigationTree(); if (!isEnabled) { return null; @@ -68,6 +68,7 @@ export const NavigationTree = observer(function NavigationTree() { styled(navigationTreeStyles)(
@@ -76,9 +77,8 @@ export const NavigationTree = observer(function NavigationTree() {
)} - isSelected={isSelected} + customSelect={handleSelect} onOpen={handleOpen} - onSelect={handleSelect} />
); diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode.tsx index 3f067d3f53..4908a2aff0 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode.tsx @@ -7,9 +7,8 @@ */ import { observer } from 'mobx-react-lite'; -import { useContext } from 'react'; -import { TreeNode, TreeNodeContext } from '@cloudbeaver/core-blocks'; +import { TreeNode } from '@cloudbeaver/core-blocks'; import type { NavNode } from '../../shared/NodesManager/EntityTypes'; import { NavigationNodeControl } from './NavigationNode/NavigationNodeControl'; @@ -27,8 +26,6 @@ export const NavigationNode: React.FC = observer(function NavigationNode( node, component, }) { - const context = useContext(TreeNodeContext); - const { control, selected, @@ -57,7 +54,7 @@ export const NavigationNode: React.FC = observer(function NavigationNode( onFilter={handleFilter} > - + {expanded && } ); }); diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx index 9016180952..b155161f3f 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx @@ -79,10 +79,10 @@ export const NavigationNodeControl: React.FC = observer(function Navigati - {connected && } + {connected && } {node.name} - + diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx index 2749281ea7..8e5348322e 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx @@ -11,57 +11,49 @@ import { observer } from 'mobx-react-lite'; import { useContext, useMemo } from 'react'; import styled from 'reshadow'; -import { TreeNodeContext, TreeNodeNested, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { resourceKeyList } from '@cloudbeaver/core-sdk'; +import { TreeNodeNested, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks'; import { useStyles } from '@cloudbeaver/core-theming'; -import { NavNodeInfoResource } from '../../../shared/NodesManager/NavNodeInfoResource'; -import { useChildren } from '../../../shared/useChildren'; +import { TreeContext } from '../../TreeContext'; interface Props { nodeId: string; component: React.FC<{ nodeId: string; }>; -} - -function isDefined(val: T | undefined | null): val is T { - return val !== undefined && val !== null; + root?: boolean; } export const NavigationNodeNested: React.FC = observer(function NavigationNodeNested({ nodeId, component, + root, }) { const styles = useStyles(TREE_NODE_STYLES); - const context = useContext(TreeNodeContext); - const navNodeInfo = useService(NavNodeInfoResource); - const childrenInfo = useChildren(nodeId); + const treeContext = useContext(TreeContext); - const children = useMemo(() => computed(() => { - if (!childrenInfo?.children) { - return []; - } + const children = useMemo( + () => computed(() => treeContext?.tree.getNodeChildren(nodeId) || []), + [nodeId, treeContext?.tree] + ).get(); - const childrenEntities = navNodeInfo.get(resourceKeyList(childrenInfo.children)).filter(isDefined); - - if (!context?.filterValue) { - return childrenEntities; - } - - return childrenEntities.filter(child => child.name?.toLowerCase().includes(context.filterValue.toLowerCase())); - }), [childrenInfo.children, context?.filterValue, navNodeInfo]).get(); - - if (!children.length || !context?.expanded) { + if (children.length === 0) { return null; } const NavigationNode = component; + if (root) { + return styled(styles)( + <> + {children.map(child => )} + + ); + } + return styled(styles)( - {children.map(child => )} + {children.map(child => )} ); }); diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/treeNodeMenuStyles.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/treeNodeMenuStyles.ts index d536d2827e..31bc93f280 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/treeNodeMenuStyles.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/treeNodeMenuStyles.ts @@ -12,7 +12,7 @@ export const treeNodeMenuStyles = css` MenuTrigger { margin-left: 16px; padding: 0; - height: 20px; + height: 16px; &:before { display: none; diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts index 5dc903fd17..928f4c7075 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts @@ -8,6 +8,7 @@ import { useContext, useEffect, useState } from 'react'; +import { useObjectRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import type { NavNode } from '../../shared/NodesManager/EntityTypes'; @@ -27,29 +28,29 @@ interface INavigationNode { leaf: boolean; handleExpand: () => void; handleOpen: () => void; - handleSelect: (isMultiple?: boolean) => void; + handleSelect: (isMultiple?: boolean, nested?: boolean) => void; handleFilter: (value: string) => void; filterValue: string; } export function useNavigationNode(node: NavNode): INavigationNode { - const context = useContext(TreeContext); + const contextRef = useObjectRef({ + context: useContext(TreeContext), + }); const navigationTreeService = useService(NavigationTreeService); const [processing, setProcessing] = useState(false); - const { isLoading, isOutdated } = useNode(node.id); + const { isLoading } = useNode(node.id); const children = useChildren(node.id); const loading = isLoading() || children.isLoading() || processing; - const isExpanded = navigationTreeService.isNodeExpanded(node.id); - let leaf = isLeaf(node) || (children.children?.length === 0 && !children.isOutdated()); + const state = contextRef.context?.tree.getNodeState(node.id); + const isExpanded = state?.expanded || false; + let leaf = isLeaf(node); let expanded = isExpanded && !leaf; if ( node.objectFeatures.includes(EObjectFeature.dataSource) - && ( - !node.objectFeatures.includes(EObjectFeature.dataSourceConnected) - || (!children.children && isOutdated()) - ) + && !node.objectFeatures.includes(EObjectFeature.dataSourceConnected) ) { leaf = false; expanded = false; @@ -62,61 +63,43 @@ export function useNavigationNode(node: NavNode): INavigationNode { clearTimeout(timeout); setProcessing(false); if (!state) { - navigationTreeService.expandNode(node.id, false); + contextRef.context?.tree.expand(node, false); return; } } - navigationTreeService.expandNode(node.id, !expanded); + contextRef.context?.tree.expand(node, !expanded); }; const handleOpen = async () => { setProcessing(true); try { - await context?.onOpen?.(node); + await contextRef.context?.onOpen?.(node); } finally { setProcessing(false); } }; - const handleSelect = (multiple = false) => { - context?.onSelect?.(node, multiple); + const handleSelect = (multiple = false, nested = false) => { + contextRef.context?.tree.select(node, multiple, nested); }; const handleFilter = (value: string) => { - context?.onFilter?.(node, value); + contextRef.context?.tree.filter(node, value); }; - // TODO: probably should be refactored - useEffect(() => { - if (expanded && children.isOutdated() && !children.isLoading() && children.isLoaded() && !isOutdated()) { - setProcessing(true); - navigationTreeService - .loadNestedNodes(node.id) - .then(state => { - setProcessing(false); - if (!state) { - navigationTreeService.expandNode(node.id, false); - } - }); - } - }, [expanded, children.isOutdated(), children.isLoading(), children.isLoaded(), isOutdated(), node]); - useEffect(() => () => { - if (!context?.selectionTree && node && context?.isSelected?.(node)) { - context.onSelect?.(node, true); + if (!contextRef.context?.selectionTree) { + const state = contextRef.context?.tree.getNodeState(node.id); + + if (state?.selected) { + contextRef.context?.tree.select(node, true, false); + } } - }, [context, node.id]); - - useEffect(() => () => { - // TODO: seems like selection & expand should be specific for separate tree definitions - navigationTreeService.expandNode(node.id, false); - - context?.treeNodesState?.delete(node.id); }, [node.id]); return { - control: context?.control, - selected: context?.isSelected?.(node) || false, + control: contextRef.context?.control, + selected: state?.selected || false, loading, expanded, leaf, @@ -124,7 +107,7 @@ export function useNavigationNode(node: NavNode): INavigationNode { handleOpen, handleSelect, handleFilter, - filterValue: context?.treeNodesState?.get(node.id).filter || '', + filterValue: state?.filter || '', }; } diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts index 33167f4549..ae2b1620a1 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts @@ -12,16 +12,16 @@ import { ConnectionAuthService } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { Executor, IExecutor } from '@cloudbeaver/core-executor'; -import { ResourceKeyUtils } from '@cloudbeaver/core-sdk'; import { MetadataMap } from '@cloudbeaver/core-utils'; import type { IActiveView } from '@cloudbeaver/core-view'; import { EObjectFeature } from '../shared/NodesManager/EObjectFeature'; import { NavNodeExtensionsService } from '../shared/NodesManager/NavNodeExtensionsService'; -import { NavNodeInfoResource, ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource'; +import { ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource'; import { NavNodeManagerService } from '../shared/NodesManager/NavNodeManagerService'; import { NavTreeResource } from '../shared/NodesManager/NavTreeResource'; import { NodeManagerUtils } from '../shared/NodesManager/NodeManagerUtils'; +import type { ITreeNodeState } from './useElementsTree'; export interface INavigationNodeMetadata { selected: boolean; @@ -35,7 +35,7 @@ export interface INavigationNodeSelectionData { @injectable() export class NavigationTreeService { - readonly navigationTreeMetadata: MetadataMap; + readonly treeState: MetadataMap; readonly nodeSelectionTask: IExecutor; constructor( @@ -43,30 +43,19 @@ export class NavigationTreeService { private notificationService: NotificationService, private connectionAuthService: ConnectionAuthService, private navNodeExtensionsService: NavNodeExtensionsService, - private navTreeResource: NavTreeResource, - private navNodeInfoResource: NavNodeInfoResource + private navTreeResource: NavTreeResource ) { makeObservable(this, { unselectAll: action, }); + this.treeState = new MetadataMap(() => ({ + filter: '', + expanded: false, + selected: false, + })); this.nodeSelectionTask = new Executor(); - this.navigationTreeMetadata = new MetadataMap(() => ({ - selected: false, - expanded: false, - })); this.getView = this.getView.bind(this); - - this.navNodeInfoResource.onItemDelete.addHandler(key => { - ResourceKeyUtils.forEach(key, key => { - this.navigationTreeMetadata.delete(key); - }); - }); - this.navTreeResource.onItemDelete.addHandler(key => { - ResourceKeyUtils.forEach(key, key => { - this.expandNode(key, false); - }); - }); } getChildren(id: string): string[] | undefined { @@ -95,7 +84,7 @@ export class NavigationTreeService { await this.unselectAll(); } - const metadata = this.navigationTreeMetadata.get(id); + const metadata = this.treeState.get(id); metadata.selected = !metadata.selected; await this.nodeSelectionTask.execute({ @@ -105,20 +94,20 @@ export class NavigationTreeService { } isNodeExpanded(navNodeId: string): boolean { - return this.navigationTreeMetadata.get(navNodeId).expanded; + return this.treeState.get(navNodeId).expanded; } isNodeSelected(navNodeId: string): boolean { - return this.navigationTreeMetadata.get(navNodeId).selected; + return this.treeState.get(navNodeId).selected; } expandNode(navNodeId: string, state: boolean): void { - const metadata = this.navigationTreeMetadata.get(navNodeId); + const metadata = this.treeState.get(navNodeId); metadata.expanded = state; } getView(): IActiveView | null { - const element = Array.from(this.navigationTreeMetadata).find(([key, metadata]) => metadata.selected); + const element = Array.from(this.treeState).find(([key, metadata]) => metadata.selected); if (!element) { return null; @@ -131,7 +120,7 @@ export class NavigationTreeService { } private async unselectAll() { - for (const [id, metadata] of this.navigationTreeMetadata) { + for (const [id, metadata] of this.treeState) { metadata.selected = false; await this.nodeSelectionTask.execute({ id, diff --git a/webapp/packages/core-app/src/NavigationTree/TreeContext.ts b/webapp/packages/core-app/src/NavigationTree/TreeContext.ts index feda4b1ee3..31e9d5ecc4 100644 --- a/webapp/packages/core-app/src/NavigationTree/TreeContext.ts +++ b/webapp/packages/core-app/src/NavigationTree/TreeContext.ts @@ -8,22 +8,16 @@ import { createContext } from 'react'; -import type { MetadataMap } from '@cloudbeaver/core-utils'; - import type { NavNode } from '../shared/NodesManager/EntityTypes'; -import type { ITreeNodeState } from './ElementsTree'; +import type { IElementsTree } from './useElementsTree'; export interface ITreeContext { - treeNodesState?: MetadataMap; - getTreeNodeState?: (node: NavNode) => ITreeNodeState; + tree: IElementsTree; selectionTree: boolean; control?: React.FC<{ node: NavNode; }>; onOpen?: (node: NavNode) => Promise | void; - onSelect?: (node: NavNode, multiple: boolean) => void; - isSelected?: (node: NavNode) => boolean; - onFilter?: (node: NavNode, value: string) => void; } export const TreeContext = createContext(null); diff --git a/webapp/packages/core-app/src/NavigationTree/elementsTreeNameFilter.ts b/webapp/packages/core-app/src/NavigationTree/elementsTreeNameFilter.ts new file mode 100644 index 0000000000..9d1672d885 --- /dev/null +++ b/webapp/packages/core-app/src/NavigationTree/elementsTreeNameFilter.ts @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { resourceKeyList } from '@cloudbeaver/core-sdk'; +import type { MetadataMap } from '@cloudbeaver/core-utils'; + +import type { NavNode } from '../shared/NodesManager/EntityTypes'; +import type { NavNodeInfoResource } from '../shared/NodesManager/NavNodeInfoResource'; +import type { IElementsTreeFilter, ITreeNodeState } from './useElementsTree'; + +function isDefined(val: T | undefined | null): val is T { + return val !== undefined && val !== null; +} + +export function elementsTreeNameFilter(resource: NavNodeInfoResource): IElementsTreeFilter { + return (node: NavNode, children: string[], state: MetadataMap) => { + const nodeState = state.get(node.id); + + if (nodeState.filter === '') { + return children; + } + + const nodes = resource + .get(resourceKeyList(children)) + .filter(isDefined) + .filter(child => child.name?.toLowerCase().includes(nodeState.filter.toLowerCase())); + + return nodes.map(node => node.id); + }; +} diff --git a/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts b/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts new file mode 100644 index 0000000000..1bb747278a --- /dev/null +++ b/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts @@ -0,0 +1,238 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { useState } from 'react'; + +import { useExecutor, useObjectRef } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { ResourceKeyUtils } from '@cloudbeaver/core-sdk'; +import { MetadataMap } from '@cloudbeaver/core-utils'; + +import type { NavNode } from '../shared/NodesManager/EntityTypes'; +import { NavNodeInfoResource } from '../shared/NodesManager/NavNodeInfoResource'; +import { NavTreeResource } from '../shared/NodesManager/NavTreeResource'; +import { NavigationTreeService } from './NavigationTreeService'; + +export type IElementsTreeFilter = ( + node: NavNode, + children: string[], + state: MetadataMap +) => string[]; + +export interface ITreeNodeState { + filter: string; + selected: boolean; + expanded: boolean; +} + +interface IOptions { + root: string; + localState?: MetadataMap; + filters?: IElementsTreeFilter[]; + customSelect?: (node: NavNode, multiple: boolean, nested: boolean) => void; + isGroup?: (node: NavNode) => boolean; + onExpand?: (node: NavNode, state: boolean) => void; + onSelect?: (node: NavNode, state: boolean) => void; + onFilter?: (node: NavNode, value: string) => void; +} + +export interface IElementsTree { + state: MetadataMap; + getNodeState: (nodeId: string) => ITreeNodeState; + getNodeChildren: (nodeId: string) => string[]; + filter: (node: NavNode, value: string) => void; + select: (node: NavNode, multiple: boolean, nested: boolean) => void; + expand: (node: NavNode, state: boolean) => void; +} + +export function useElementsTree(options: IOptions): IElementsTree { + const navNodeInfoResource = useService(NavNodeInfoResource); + const navTreeService = useService(NavigationTreeService); + const navTreeResource = useService(NavTreeResource); + + const [localTreeNodesState] = useState(() => new MetadataMap(() => ({ + filter: '', + selected: false, + expanded: false, + }))); + + const state = options.localState || localTreeNodesState; + + function getNodeChildren(nodeId: string): string[] { + const node = navNodeInfoResource.get(nodeId); + + if (!node) { + return []; // Maybe filter should accept nodeId, so we be able to apply filters to empty node + } + + return (options.filters || []) + .reduce( + (children, filter) => filter(node, children, state), + navTreeService.getChildren(node.id) || [] + ); + } + + function getNestedChildren(nodeId: string): string [] { + const nestedChildren: string[] = []; + const prevChildren = getNodeChildren(nodeId); + nestedChildren.push(...prevChildren); + + while (prevChildren.length) { + const nodeKey = prevChildren.shift()!; + const children = getNodeChildren(nodeKey); + prevChildren.push(...children); + nestedChildren.push(...children); + } + + return nestedChildren; + } + + function clearSelection(nodeId: string) { + const node = navNodeInfoResource.get(nodeId); + + const ignore = node && options.isGroup?.(node) + ? getNestedChildren(nodeId) + : []; + + for (const [id, nodeState] of state) { + if (nodeState.selected && id !== nodeId && !ignore.includes(id)) { + nodeState.selected = false; + + if (options.onSelect) { + const node = navNodeInfoResource.get(id); + + if (node) { + options.onSelect(node, false); + } + } + } + } + } + + function setSelection(nodeId: string, selected: boolean): void { + const node = navNodeInfoResource.get(nodeId); + + if (!node) { + return; + } + + const treeNodeState = state.get(nodeId); + + if (treeNodeState.selected === selected) { + return; + } + + if (options.isGroup?.(node)) { + const children = getNodeChildren(nodeId); + + for (const child of children) { + setSelection(child, selected); + } + } + + treeNodeState.selected = selected; + options.onSelect?.(node, selected); + } + + const elementsTree = useObjectRef({ + state, + getNodeState(nodeId: string) { + return this.state.get(nodeId); + }, + getNodeChildren, + filter(node: NavNode, value: string) { + const treeNodeState = this.state.get(node.id); + treeNodeState.filter = value; + + options.onFilter?.(node, value); + }, + expand(node: NavNode, state: boolean) { + const treeNodeState = this.state.get(node.id); + treeNodeState.expanded = state; + + options.onExpand?.(node, state); + }, + select(node: NavNode, multiple: boolean, nested: boolean) { + if (options.customSelect) { + options.customSelect(node, multiple, nested); + return; + } + + const treeNodeState = this.state.get(node.id); + + if (!multiple) { + clearSelection(node.id); + + if (treeNodeState.selected) { + return; + } + } + + setSelection(node.id, !treeNodeState.selected); + }, + }); + + async function refreshNode(nodeId: string) { + let children = [nodeId]; + + while (children.length > 0) { + const nextChildren: string[] = []; + + for (const child of children) { + const nodeState = state.get(child); + if (!nodeState.expanded && child !== options.root) { + continue; + } + + const loaded = await navTreeService.loadNestedNodes(child); + + if (!loaded) { + const node = navNodeInfoResource.get(child); + + if (node) { + elementsTree.expand(node, false); + } + continue; + } + + nextChildren.push(...(navTreeResource.get(child) || [])); + } + + children = nextChildren; + } + } + + useExecutor({ + executor: navTreeResource.onNodeRefresh, + handlers: [refreshNode], + }); + + useExecutor({ + executor: navNodeInfoResource.onItemDelete, + handlers: [function deleteNodeState(key) { + ResourceKeyUtils.forEach(key, key => { + state.delete(key); + }); + }], + }); + + useExecutor({ + executor: navTreeResource.onItemDelete, + handlers: [function collapseDeletedTree(key) { + ResourceKeyUtils.forEach(key, key => { + const node = navNodeInfoResource.get(key); + + if (node) { + elementsTree.expand(node, false); + } + }); + }], + }); + + return elementsTree; +} diff --git a/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts b/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts index 8883ec0a03..143d51a113 100644 --- a/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts +++ b/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts @@ -6,39 +6,30 @@ * you may not use this file except in compliance with the License. */ -import { useCallback } from 'react'; - +import { useObjectRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import type { NavNode } from '../shared/NodesManager/EntityTypes'; import { NavigationTreeService } from './NavigationTreeService'; interface INavigationTree { - isSelected: (node: NavNode) => boolean; + navigationTreeService: NavigationTreeService; handleOpen: (node: NavNode) => Promise; - handleSelect: (node: NavNode, isMultiple: boolean) => void; + handleSelect: (node: NavNode, state: boolean) => void; } +const bindActions: Array = ['handleOpen', 'handleSelect']; + export function useNavigationTree(): INavigationTree { const navigationTreeService = useService(NavigationTreeService); - const handleOpen = useCallback( - (node: NavNode) => navigationTreeService.navToNode(node.id, node.parentId), - [navigationTreeService] - ); - - const handleSelect = useCallback( - (node: NavNode, multiple: boolean) => navigationTreeService.selectNode(node.id, multiple), - [navigationTreeService]); - - const isSelected = useCallback( - (node: NavNode) => navigationTreeService.isNodeSelected(node.id), - [navigationTreeService] - ); - - return { - isSelected, - handleOpen, - handleSelect, - }; + return useObjectRef({ + navigationTreeService, + handleOpen(node: NavNode) { + return this.navigationTreeService.navToNode(node.id, node.parentId); + }, + handleSelect(node: NavNode, state: boolean) { + return this.navigationTreeService.selectNode(node.id, state); + }, + }, { navigationTreeService }, undefined, bindActions); } diff --git a/webapp/packages/core-app/src/index.ts b/webapp/packages/core-app/src/index.ts index c2ca8bead4..a17e14f187 100644 --- a/webapp/packages/core-app/src/index.ts +++ b/webapp/packages/core-app/src/index.ts @@ -27,6 +27,7 @@ export * from './NavigationTree/ElementsTree'; export * from './NavigationTree/TreeContext'; export * from './NavigationTree/NavigationTreeService'; export * from './NavigationTree/NavigationTreeContextMenuService'; +export * from './NavigationTree/useElementsTree'; export * from './AppLocaleService'; // Models diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts index 682d0db38c..428dbea403 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts @@ -60,7 +60,7 @@ export class NavNodeInfoResource extends CachedMapResource { }); } - protected async loader(key: ResourceKey) { + protected async loader(key: ResourceKey): Promise> { if (isResourceKeyList(key)) { const values: NavNode[] = []; for (const nodePath of key.list) { diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts index 776a33db5c..606d5fbb0c 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts @@ -16,7 +16,7 @@ import { PermissionsService, EPermission, ServerService } from '@cloudbeaver/core-root'; import { - GraphQLService, resourceKeyList, ResourceKey, ResourceKeyUtils + resourceKeyList, ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; import { NavigationService } from '@cloudbeaver/core-ui'; @@ -92,7 +92,6 @@ export class NavNodeManagerService extends Bootstrap { readonly navigator: IExecutor; constructor( - private graphQLService: GraphQLService, private permissionsService: PermissionsService, readonly connectionInfo: ConnectionInfoResource, readonly navTree: NavTreeResource, @@ -116,8 +115,6 @@ export class NavNodeManagerService extends Bootstrap { } register(): void { - // this.sessionDataResource.onDataUpdate.addHandler(this.refreshRoot.bind(this)); - // this.connectionInfo.onSessionUpdate.addHandler(this.connectionRefreshHandler.bind(this)); this.connectionInfo.onItemAdd.addHandler(this.connectionUpdateHandler.bind(this)); this.connectionInfo.onItemDelete.addHandler(this.connectionRemoveHandler.bind(this)); this.connectionInfo.onConnectionCreate.addHandler(this.connectionCreateHandler.bind(this)); @@ -135,15 +132,7 @@ export class NavNodeManagerService extends Bootstrap { } async refreshTree(navNodeId: string): Promise { - await this.graphQLService.sdk.navRefreshNode({ - nodePath: navNodeId, - }); - this.markTreeOutdated(navNodeId); - await this.navTree.refresh(navNodeId); - } - - markTreeOutdated(navNodeId: ResourceKey): void { - this.navTree.markOutdated(resourceKeyList(this.navTree.getNestedChildren(navNodeId))); + await this.navTree.refreshTree(navNodeId); } getTree(navNodeId: string): string[] | undefined @@ -169,10 +158,6 @@ export class NavNodeManagerService extends Bootstrap { this.navTree.delete(path); } - async refreshNode(navNodeId: string): Promise { - await this.navNodeInfoResource.refresh(navNodeId); - } - getNode(navNodeId: string): NavNode | undefined getNode(navNodeKey: NavNodeKey): NavNode | undefined getNode(navNodeKey: NavNodeKey[]): Array @@ -329,22 +314,6 @@ export class NavNodeManagerService extends Bootstrap { }; }; - async updateRoot(): Promise { - const enabled = await this.isNavTreeEnabled(); - if (enabled) { - await this.navTree.refresh(ROOT_NODE_PATH); - } - } - - async refreshRoot(): Promise { - this.navTree.delete(ROOT_NODE_PATH); - const enabled = await this.isNavTreeEnabled(); - if (enabled) { - // this.navTree.markOutdated(ROOT_NODE_PATH); - await this.navTree.refresh(ROOT_NODE_PATH); - } - } - private async connectionCreateHandler(connection: Connection) { const enabled = await this.isNavTreeEnabled(); if (!enabled) { @@ -352,7 +321,7 @@ export class NavNodeManagerService extends Bootstrap { } const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(connection.id); - this.markTreeOutdated(nodeId); + this.navTree.markTreeOutdated(nodeId); const tree = await this.navTree.load(ROOT_NODE_PATH); @@ -361,16 +330,6 @@ export class NavNodeManagerService extends Bootstrap { } } - private async connectionRefreshHandler(connections: Connection[]) { - const enabled = await this.isNavTreeEnabled(); - if (!enabled) { - return; - } - - // this.navTree.markOutdated(ROOT_NODE_PATH); - await this.navTree.refresh(ROOT_NODE_PATH); - } - private async connectionUpdateHandler(key: ResourceKey) { const enabled = await this.isNavTreeEnabled(); if (!enabled) { @@ -382,7 +341,7 @@ export class NavNodeManagerService extends Bootstrap { await this.navTree.load(ROOT_NODE_PATH); await ResourceKeyUtils.forEachAsync(key, async key => { const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key); - this.markTreeOutdated(nodeId); + this.navTree.markTreeOutdated(nodeId); // addOpenedConnection const connectionInfo = this.connectionInfo.get(key); diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts index fdf7312140..a797e56552 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts @@ -9,6 +9,7 @@ import { action, computed, makeObservable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; +import { Executor, IExecutor } from '@cloudbeaver/core-executor'; import { SessionDataResource } from '@cloudbeaver/core-root'; import { GraphQLService, @@ -39,6 +40,7 @@ interface INodeMetadata extends ICachedMapResourceMetadata { @injectable() export class NavTreeResource extends CachedMapResource { + readonly onNodeRefresh: IExecutor; protected metadata: MetadataMap; get childrenLimit(): number { @@ -66,10 +68,25 @@ export class NavTreeResource extends CachedMapResource { exception: null, includes: [], })); + + this.onNodeRefresh = new Executor(null, (a, b) => a === b); this.onDataOutdated.addHandler(navNodeInfoResource.markOutdated.bind(navNodeInfoResource)); this.sessionDataResource.onDataUpdate.addPostHandler(() => this.markOutdated()); } + async refreshTree(navNodeId: string): Promise { + await this.graphQLService.sdk.navRefreshNode({ + nodePath: navNodeId, + }); + this.markTreeOutdated(navNodeId); + await this.refresh(navNodeId); + await this.onNodeRefresh.execute(navNodeId); + } + + markTreeOutdated(navNodeId: ResourceKey): void { + this.markOutdated(resourceKeyList(this.getNestedChildren(navNodeId))); + } + setDetails(keyObject: ResourceKey, state: boolean): void { ResourceKeyUtils.forEach(keyObject, key => { const children = resourceKeyList(this.getNestedChildren(key)); @@ -253,6 +270,8 @@ export class NavTreeResource extends CachedMapResource { withDetails: metadata.withDetails, }); + navNodeInfo.hasChildren = navNodeChildren.length > 0; + return { navNodeChildren: navNodeChildren.slice(0, this.childrenLimit), navNodeInfo, parentPath }; } } diff --git a/webapp/packages/core-app/src/shared/NodesManager/useNode.ts b/webapp/packages/core-app/src/shared/NodesManager/useNode.ts index 3c4dd5d3c6..2e36b30192 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/useNode.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/useNode.ts @@ -6,23 +6,40 @@ * you may not use this file except in compliance with the License. */ -import { useCallback } from 'react'; - +import { useObjectRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; +import type { NavNode } from './EntityTypes'; import { NavNodeInfoResource } from './NavNodeInfoResource'; -export function useNode(navNodeId: string) { +interface IUseNodeHook { + navNodeId: string; + node: NavNode | undefined; + isLoading: () => boolean; + isLoaded: () => boolean; + isOutdated: () => boolean; +} + +const bindActions: Array = ['isLoading', 'isLoaded', 'isOutdated']; + +export function useNode(navNodeId: string): IUseNodeHook { const navNodeInfoResource = useService(NavNodeInfoResource); const node = navNodeInfoResource.get(navNodeId); - const deps = [navNodeId]; - - const isLoading = useCallback(() => navNodeInfoResource.isDataLoading(navNodeId), deps); - const isLoaded = useCallback(() => navNodeInfoResource.isLoaded(navNodeId), deps); - const isOutdated = useCallback(() => navNodeInfoResource.isOutdated(navNodeId), deps); - - return { - node, isLoading, isLoaded, isOutdated, - }; + return useObjectRef({ + navNodeId, + node, + isLoading() { + return navNodeInfoResource.isDataLoading(this.navNodeId); + }, + isLoaded() { + return navNodeInfoResource.isLoaded(this.navNodeId); + }, + isOutdated() { + return navNodeInfoResource.isOutdated(this.navNodeId); + }, + }, { + navNodeId, + node, + }, undefined, bindActions); } diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx index b97bb168eb..05dee4371f 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx @@ -6,11 +6,13 @@ * you may not use this file except in compliance with the License. */ -import { useCallback, useMemo, useRef, memo } from 'react'; +import { observable } from 'mobx'; +import { memo } from 'react'; import styled, { use } from 'reshadow'; import { useStyles } from '@cloudbeaver/core-theming'; +import { useObjectRef } from '../../useObjectRef'; import { ITreeNodeContext, TreeNodeContext } from './TreeNodeContext'; import { TREE_NODE_STYLES } from './TreeNodeStyles'; @@ -21,7 +23,7 @@ interface Props { leaf?: boolean; className?: string; onExpand?: () => void; - onSelect?: (multiple?: boolean) => void; + onSelect?: (multiple?: boolean, nested?: boolean) => void; onFilter?: (value: string) => void; filterValue?: string; onOpen?: () => void; @@ -37,41 +39,42 @@ export const TreeNode: React.FC = memo(function TreeNode({ children, ...handlers }) { - const handlersRef = useRef(handlers); - handlersRef.current = handlers; + const handlersRef = useObjectRef(handlers); - const handleExpand = useCallback(() => { - handlersRef.current.onExpand?.(); - }, []); - - const handleSelect = useCallback( - (multiple?: boolean): void => handlersRef.current.onSelect?.(multiple), - [] - ); - - const handleFilter = useCallback( - (value: string): void => handlersRef.current.onFilter?.(value), - [] - ); - - const handleOpen = useCallback(() => { - handlersRef.current.onOpen?.(); - }, []); - - const nodeContext = useMemo(() => ({ + const nodeContext = useObjectRef({ loading, selected, expanded, leaf, filterValue, - expand: handleExpand, - select: handleSelect, - filter: handleFilter, - open: handleOpen, - }), [loading, selected, expanded, leaf, filterValue, handleExpand, handleSelect, handleOpen, handleFilter]); + expand() { + handlersRef.onExpand?.(); + }, + select(multiple?: boolean, nested?: boolean): void { + handlersRef.onSelect?.(multiple, nested); + }, + filter(value: string): void { + handlersRef.onFilter?.(value); + }, + open() { + handlersRef.onOpen?.(); + }, + }, { + loading, + selected, + expanded, + leaf, + filterValue, + }, { + loading: observable.ref, + selected: observable.ref, + expanded: observable.ref, + leaf: observable.ref, + filterValue: observable.ref, + }); return styled(useStyles(TREE_NODE_STYLES))( - + {children} diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts index d6c9fe22f5..17b9dc67ff 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts @@ -13,7 +13,7 @@ export interface ITreeNodeContext { loading: boolean; selected: boolean; leaf: boolean; - select: (multiple?: boolean) => void; + select: (multiple?: boolean, nested?: boolean) => void; filter: (value: string) => void; filterValue: string; expand: () => void; diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx index 2ef30759d2..f5e041fca8 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx @@ -6,6 +6,7 @@ * you may not use this file except in compliance with the License. */ +import { observer } from 'mobx-react-lite'; import { useContext } from 'react'; import { TreeNodeContext } from './TreeNodeContext'; @@ -20,7 +21,7 @@ interface Props { big?: boolean; } -export const TreeNodeControl: React.FC = function TreeNodeControl({ +export const TreeNodeControl: React.FC = observer(function TreeNodeControl({ onClick, className, children, @@ -61,4 +62,4 @@ export const TreeNodeControl: React.FC = function TreeNodeControl({ {children} ); -}; +}); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx index 0fd443104d..a970ddd51d 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx @@ -6,6 +6,7 @@ * you may not use this file except in compliance with the License. */ +import { observer } from 'mobx-react-lite'; import { useContext } from 'react'; import styled from 'reshadow'; import { css } from 'reshadow'; @@ -28,7 +29,7 @@ interface Props { className?: string; } -export const TreeNodeExpand: React.FC = function TreeNodeExpand({ +export const TreeNodeExpand: React.FC = observer(function TreeNodeExpand({ leaf, big, className, @@ -58,4 +59,4 @@ export const TreeNodeExpand: React.FC = function TreeNodeExpand({ {!context.loading && !context.leaf && !leaf && !big && } ); -}; +}); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeFilter.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeFilter.tsx index 7b71ceaa4b..2e1847cc29 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeFilter.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeFilter.tsx @@ -6,6 +6,7 @@ * you may not use this file except in compliance with the License. */ +import { observer } from 'mobx-react-lite'; import { useCallback, useContext, useEffect, useState } from 'react'; import styled, { css, use } from 'reshadow'; @@ -56,7 +57,7 @@ interface Props { className?: string; } -export const TreeNodeFilter: React.FC = function TreeNodeFilter({ +export const TreeNodeFilter: React.FC = observer(function TreeNodeFilter({ disabled, className, }) { @@ -102,7 +103,7 @@ export const TreeNodeFilter: React.FC = function TreeNodeFilter({ useEffect(() => () => context.filter(''), []); return styled(useStyles(styles))( - + = function TreeNodeFilter({ ); -}; +}); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx index 74c16bbc1b..6c2c831e50 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx @@ -6,27 +6,24 @@ * you may not use this file except in compliance with the License. */ +import { observer } from 'mobx-react-lite'; import { useContext } from 'react'; -import styled from 'reshadow'; -import { css } from 'reshadow'; import { Checkbox } from '../../FormControls/Checkboxes/Checkbox'; import { TreeNodeContext } from './TreeNodeContext'; -const styles = css` - Icon { - cursor: pointer; - height: 100%; - width: 100%; - } -`; - interface Props { + group?: boolean; + onSelect?: () => void; + selected?: boolean; disabled?: boolean; className?: string; } -export const TreeNodeSelect: React.FC = function TreeNodeSelect({ +export const TreeNodeSelect: React.FC = observer(function TreeNodeSelect({ + onSelect, + group, + selected, disabled, className, }) { @@ -45,9 +42,13 @@ export const TreeNodeSelect: React.FC = function TreeNodeSelect({ event.preventDefault(); }; - return styled(styles)( + return (
- context.select(true)} /> + context.select(true, group))} + />
); -}; +}); From 6761b0a710eef37cac58ae25ee01406f1ac73cea Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 13 May 2021 21:25:20 +0300 Subject: [PATCH 04/16] fix(plugin-authentication): CB-938 enable link for base auth --- .../plugin-authentication/src/AuthenticationService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index 9566586bc6..254d163709 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -71,7 +71,7 @@ export class AuthenticationService extends Bootstrap { return; } - await this.auth(true); + await this.auth(true, null, true); } register(): void { @@ -87,7 +87,7 @@ export class AuthenticationService extends Bootstrap { return; } - await this.auth(false); + await this.auth(false, null, true); }); this.authProviderService.requestAuthProvider.addHandler(this.requestAuthProviderHandler); } From 70296096fb5b69536fd6204c4d6a4406c03b20b0 Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 13 May 2021 21:46:37 +0300 Subject: [PATCH 05/16] feat: CB-874 groups for navigation tree --- .../src/NavigationTree/ElementsTree.tsx | 5 +- .../NavigationTree/NavigationNodeComponent.ts | 20 ++++ .../src/NavigationTree/NavigationTree.tsx | 16 ++- .../NavigationTreeNode/NavigationNode.tsx | 11 +- .../NavigationNodeElement.tsx | 23 +++- .../navigationTreeConnectionGroupFilter.ts | 107 ++++++++++++++++++ .../navigationTreeConnectionGroupRenderer.tsx | 53 +++++++++ .../src/NavigationTree/useElementsTree.ts | 17 ++- webapp/packages/core-app/src/locales/en.ts | 2 + webapp/packages/core-app/src/locales/ru.ts | 2 + 10 files changed, 242 insertions(+), 14 deletions(-) create mode 100644 webapp/packages/core-app/src/NavigationTree/NavigationNodeComponent.ts create mode 100644 webapp/packages/core-app/src/NavigationTree/navigationTreeConnectionGroupFilter.ts create mode 100644 webapp/packages/core-app/src/NavigationTree/navigationTreeConnectionGroupRenderer.tsx diff --git a/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx b/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx index 1ddd4ff9aa..5b42fb6d47 100644 --- a/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx +++ b/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx @@ -22,7 +22,7 @@ import { elementsTreeNameFilter } from './elementsTreeNameFilter'; import { NavigationNodeNested } from './NavigationTreeNode/NavigationNode/NavigationNodeNested'; import { NavigationNodeElement } from './NavigationTreeNode/NavigationNodeElement'; import { ITreeContext, TreeContext } from './TreeContext'; -import { IElementsTreeFilter, ITreeNodeState, useElementsTree } from './useElementsTree'; +import { IElementsTreeCustomRenderer, IElementsTreeFilter, ITreeNodeState, useElementsTree } from './useElementsTree'; const styles = css` tree { @@ -48,6 +48,7 @@ interface Props { emptyPlaceholder: React.FC; className?: string; filters?: IElementsTreeFilter[]; + renderers?: IElementsTreeCustomRenderer[]; customSelect?: (node: NavNode, multiple: boolean) => void; isGroup?: (node: NavNode) => boolean; onExpand?: (node: NavNode, state: boolean) => Promise | void; @@ -63,6 +64,7 @@ export const ElementsTree: React.FC = observer(function ElementsTree({ selectionTree = false, emptyPlaceholder, filters, + renderers, className, isGroup, customSelect, @@ -83,6 +85,7 @@ export const ElementsTree: React.FC = observer(function ElementsTree({ root, localState, filters: [nameFilter, ...(filters || [])], + renderers, isGroup, onFilter, customSelect, diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationNodeComponent.ts b/webapp/packages/core-app/src/NavigationTree/NavigationNodeComponent.ts new file mode 100644 index 0000000000..e7903a70e7 --- /dev/null +++ b/webapp/packages/core-app/src/NavigationTree/NavigationNodeComponent.ts @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 type React from 'react'; + +import type { NavNode } from '../shared/NodesManager/EntityTypes'; + +interface Props { + node: NavNode; + component: React.FC<{ + nodeId: string; + }>; +} + +export type NavigationNodeComponent = React.FC; diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx index 36a1d38eee..a61c149e25 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx @@ -7,15 +7,19 @@ */ import { observer } from 'mobx-react-lite'; +import { useMemo } from 'react'; import styled, { css } from 'reshadow'; import { useFocus } from '@cloudbeaver/core-blocks'; +import { ConnectionInfoResource } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { usePermission, EPermission } from '@cloudbeaver/core-root'; import { useActiveView } from '@cloudbeaver/core-view'; -import { ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource'; +import { NavNodeInfoResource, ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource'; import { ElementsTree } from './ElementsTree'; +import { navigationTreeConnectionGroupFilter } from './navigationTreeConnectionGroupFilter'; +import { navigationTreeConnectionGroupRenderer } from './navigationTreeConnectionGroupRenderer'; import { NavigationTreeService } from './NavigationTreeService'; import { useNavigationTree } from './useNavigationTree'; @@ -55,11 +59,19 @@ const navigationTreeStyles = css` export const NavigationTree = observer(function NavigationTree() { const navTreeService = useService(NavigationTreeService); + const navNodeInfoResource = useService(NavNodeInfoResource); + const connectionInfoResource = useService(ConnectionInfoResource); + const [onFocus, onBlur] = useActiveView(navTreeService.getView); const [ref] = useFocus({ onFocus, onBlur }); const isEnabled = usePermission(EPermission.public); const { handleOpen, handleSelect } = useNavigationTree(); + const connectionGroupFilter = useMemo(() => navigationTreeConnectionGroupFilter( + connectionInfoResource, + navNodeInfoResource + ), [connectionInfoResource, navNodeInfoResource]); + if (!isEnabled) { return null; } @@ -69,6 +81,8 @@ export const NavigationTree = observer(function NavigationTree() { styled(navigationTreeStyles)(
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode.tsx index 4908a2aff0..569d03264e 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode.tsx @@ -10,19 +10,12 @@ import { observer } from 'mobx-react-lite'; import { TreeNode } from '@cloudbeaver/core-blocks'; -import type { NavNode } from '../../shared/NodesManager/EntityTypes'; +import type { NavigationNodeComponent } from '../NavigationNodeComponent'; import { NavigationNodeControl } from './NavigationNode/NavigationNodeControl'; import { NavigationNodeNested } from './NavigationNode/NavigationNodeNested'; import { useNavigationNode } from './useNavigationNode'; -interface Props { - node: NavNode; - component: React.FC<{ - nodeId: string; - }>; -} - -export const NavigationNode: React.FC = observer(function NavigationNode({ +export const NavigationNode: NavigationNodeComponent = observer(function NavigationNode({ node, component, }) { diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeElement.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeElement.tsx index 44dc1db873..7d97e4c807 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeElement.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeElement.tsx @@ -7,8 +7,12 @@ */ import { observer } from 'mobx-react-lite'; +import { useContext } from 'react'; -import { useNode } from '../../shared/NodesManager/useNode'; +import { useService } from '@cloudbeaver/core-di'; + +import { NavNodeInfoResource } from '../../shared/NodesManager/NavNodeInfoResource'; +import { TreeContext } from '../TreeContext'; import { NavigationNode } from './NavigationNode'; interface NavigationTreeNodeProps { @@ -18,7 +22,22 @@ interface NavigationTreeNodeProps { export const NavigationNodeElement = observer(function NavigationNodeElement({ nodeId, }: NavigationTreeNodeProps) { - const { node } = useNode(nodeId); + const context = useContext(TreeContext); + const navNodeInfoResource = useService(NavNodeInfoResource); + + if (context?.tree.renderers) { + for (const renderer of context.tree.renderers) { + const CustomRenderer = renderer(nodeId); + + if (CustomRenderer === undefined) { + continue; + } + + return ; + } + } + + const node = navNodeInfoResource.get(nodeId); if (!node) { return null; diff --git a/webapp/packages/core-app/src/NavigationTree/navigationTreeConnectionGroupFilter.ts b/webapp/packages/core-app/src/NavigationTree/navigationTreeConnectionGroupFilter.ts new file mode 100644 index 0000000000..e09ec5730a --- /dev/null +++ b/webapp/packages/core-app/src/NavigationTree/navigationTreeConnectionGroupFilter.ts @@ -0,0 +1,107 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { ConnectionInfoResource, EConnectionFeature } from '@cloudbeaver/core-connections'; +import { resourceKeyList } from '@cloudbeaver/core-sdk'; +import type { MetadataMap } from '@cloudbeaver/core-utils'; + +import type { NavNode } from '../shared/NodesManager/EntityTypes'; +import { EObjectFeature } from '../shared/NodesManager/EObjectFeature'; +import { NavNodeInfoResource, ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource'; +import { NodeManagerUtils } from '../shared/NodesManager/NodeManagerUtils'; +import type { IElementsTreeFilter, ITreeNodeState } from './useElementsTree'; + +export const NAVIGATION_TREE_CONNECTION_GROUPS = { + unsorted: 'nav-tree://connection-group/unsorted', + manageable: 'nav-tree://connection-group/manageable', + unmanageable: 'nav-tree://connection-group/unmanageable', +}; + +function isDefined(val: T | undefined | null): val is T { + return val !== undefined && val !== null; +} + +function sortManageable(connectionInfoResource: ConnectionInfoResource): (nodeA: NavNode, nodeB: NavNode) => number { + return (nodeA: NavNode, nodeB: NavNode): number => { + const nodeAConnection = nodeA.objectFeatures.includes(EObjectFeature.dataSource); + const nodeBConnection = nodeB.objectFeatures.includes(EObjectFeature.dataSource); + + if (!nodeAConnection || !nodeBConnection) { + if (nodeAConnection === nodeBConnection) { + return 0; + } + return nodeAConnection ? 1 : -1; + } + + const connectionA = connectionInfoResource.get(NodeManagerUtils.connectionNodeIdToConnectionId(nodeA.id)); + const connectionB = connectionInfoResource.get(NodeManagerUtils.connectionNodeIdToConnectionId(nodeB.id)); + + const nodeAManageable = connectionA?.features.includes(EConnectionFeature.manageable); + const nodeBManageable = connectionB?.features.includes(EConnectionFeature.manageable); + + if (nodeAManageable === nodeBManageable) { + return 0; + } + + return nodeBManageable ? 1 : -1; + }; +} + +export function navigationTreeConnectionGroupFilter( + connectionInfoResource: ConnectionInfoResource, + resource: NavNodeInfoResource +): IElementsTreeFilter { + return (node: NavNode, children: string[]) => { + if (node.id !== ROOT_NODE_PATH) { + return children; + } + + const nodes = resource + .get(resourceKeyList(children)) + .filter(isDefined) + .sort(sortManageable(connectionInfoResource)); + + let groupedChildren: string[] = []; + let lastGroup = NAVIGATION_TREE_CONNECTION_GROUPS.unsorted; + let groups = 0; + + for (const node of nodes) { + const connection = connectionInfoResource.get(NodeManagerUtils.connectionNodeIdToConnectionId(node.id)); + const manageable = connection?.features.includes(EConnectionFeature.manageable); + + let nextGroup = NAVIGATION_TREE_CONNECTION_GROUPS.unsorted; + + if (manageable) { + nextGroup = NAVIGATION_TREE_CONNECTION_GROUPS.manageable; + } else if (connection) { + nextGroup = NAVIGATION_TREE_CONNECTION_GROUPS.unmanageable; + } + + if (nextGroup !== lastGroup) { + if (nextGroup === NAVIGATION_TREE_CONNECTION_GROUPS.manageable) { + groupedChildren.push(NAVIGATION_TREE_CONNECTION_GROUPS.manageable); + } else { + groupedChildren.push(NAVIGATION_TREE_CONNECTION_GROUPS.unmanageable); + } + groups++; + lastGroup = nextGroup; + } + + groupedChildren.push(node.id); + } + + if (groups === 1) { + groupedChildren = groupedChildren.filter(id => + id !== NAVIGATION_TREE_CONNECTION_GROUPS.manageable + && id !== NAVIGATION_TREE_CONNECTION_GROUPS.unmanageable + ); + } + + return groupedChildren; + }; +} diff --git a/webapp/packages/core-app/src/NavigationTree/navigationTreeConnectionGroupRenderer.tsx b/webapp/packages/core-app/src/NavigationTree/navigationTreeConnectionGroupRenderer.tsx new file mode 100644 index 0000000000..3f787c880a --- /dev/null +++ b/webapp/packages/core-app/src/NavigationTree/navigationTreeConnectionGroupRenderer.tsx @@ -0,0 +1,53 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import styled, { css } from 'reshadow'; + +import { Translate } from '@cloudbeaver/core-localization'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; + +import { NAVIGATION_TREE_CONNECTION_GROUPS } from './navigationTreeConnectionGroupFilter'; +import type { ElementsTreeCustomRendererComponent } from './useElementsTree'; + +export function navigationTreeConnectionGroupRenderer(nodeId: string): ElementsTreeCustomRendererComponent | undefined { + if (nodeId !== NAVIGATION_TREE_CONNECTION_GROUPS.manageable + && nodeId !== NAVIGATION_TREE_CONNECTION_GROUPS.unmanageable) { + return; + } + + if (nodeId === NAVIGATION_TREE_CONNECTION_GROUPS.manageable) { + return ManageableGroup; + } + + return UnManageableGroup; +} + +const styles = composes( + css` + connection-group { + composes: theme-text-text-hint-on-light from global; + } + `, + css` + connection-group { + composes: theme-typography--caption from global; + padding: 4px 12px; + + &:not(:first-child) { + margin-top: 8px; + } + } + ` +); + +const ManageableGroup: ElementsTreeCustomRendererComponent = function ManageableGroup() { + return styled(useStyles(styles))(); +}; +const UnManageableGroup: ElementsTreeCustomRendererComponent = function UnManageableGroup() { + return styled(useStyles(styles))(); +}; diff --git a/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts b/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts index 1bb747278a..34aa0dac77 100644 --- a/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts +++ b/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts @@ -6,6 +6,7 @@ * you may not use this file except in compliance with the License. */ +import { observable } from 'mobx'; import { useState } from 'react'; import { useExecutor, useObjectRef } from '@cloudbeaver/core-blocks'; @@ -18,6 +19,15 @@ import { NavNodeInfoResource } from '../shared/NodesManager/NavNodeInfoResource' import { NavTreeResource } from '../shared/NodesManager/NavTreeResource'; import { NavigationTreeService } from './NavigationTreeService'; +export type ElementsTreeCustomRendererComponent = React.FC<{ + nodeId: string; + component: React.FC<{ + nodeId: string; + }>; +}>; + +export type IElementsTreeCustomRenderer = (nodeId: string) => ElementsTreeCustomRendererComponent | undefined; + export type IElementsTreeFilter = ( node: NavNode, children: string[], @@ -34,6 +44,7 @@ interface IOptions { root: string; localState?: MetadataMap; filters?: IElementsTreeFilter[]; + renderers?: IElementsTreeCustomRenderer[]; customSelect?: (node: NavNode, multiple: boolean, nested: boolean) => void; isGroup?: (node: NavNode) => boolean; onExpand?: (node: NavNode, state: boolean) => void; @@ -42,6 +53,8 @@ interface IOptions { } export interface IElementsTree { + root: string; + renderers: IElementsTreeCustomRenderer[]; state: MetadataMap; getNodeState: (nodeId: string) => ITreeNodeState; getNodeChildren: (nodeId: string) => string[]; @@ -140,6 +153,8 @@ export function useElementsTree(options: IOptions): IElementsTree { } const elementsTree = useObjectRef({ + root: options.root, + renderers: options.renderers || [], state, getNodeState(nodeId: string) { return this.state.get(nodeId); @@ -175,7 +190,7 @@ export function useElementsTree(options: IOptions): IElementsTree { setSelection(node.id, !treeNodeState.selected); }, - }); + }, undefined, { root: observable.ref, renderers: observable.ref }); async function refreshNode(nodeId: string) { let children = [nodeId]; diff --git a/webapp/packages/core-app/src/locales/en.ts b/webapp/packages/core-app/src/locales/en.ts index 274907b4af..ab7371bb09 100644 --- a/webapp/packages/core-app/src/locales/en.ts +++ b/webapp/packages/core-app/src/locales/en.ts @@ -18,6 +18,8 @@ export default [ ['app_navigationTree_connection_view_option_showSystemObjects', 'Show system objects'], ['app_navigationTree_refreshNode', 'Refresh'], ['app_navigationTree_context_disconnect', 'Disconnect'], + ['app_navigationTree_connection_group_user', 'User Connections'], + ['app_navigationTree_connection_group_shared', 'Shared Connections'], ['app_shared_inlineEditor_dialog_title', 'Edit mode'], ['app_shared_inlineEditor_dialog_apply', 'Apply'], ['app_shared_inlineEditor_dialog_cancel', 'Cancel'], diff --git a/webapp/packages/core-app/src/locales/ru.ts b/webapp/packages/core-app/src/locales/ru.ts index 227338fd4d..36d39a8b49 100644 --- a/webapp/packages/core-app/src/locales/ru.ts +++ b/webapp/packages/core-app/src/locales/ru.ts @@ -21,6 +21,8 @@ export default [ ['app_navigationTree_connection_view_option_showSystemObjects', 'Показывать системные объекты'], ['app_navigationTree_refreshNode', 'Обновить'], ['app_navigationTree_context_disconnect', 'Отключить'], + ['app_navigationTree_connection_group_user', 'Пользовательские подключения'], + ['app_navigationTree_connection_group_shared', 'Общие подключения'], ['app_shared_inlineEditor_dialog_title', 'Редактирование'], ['app_shared_inlineEditor_dialog_apply', 'Применить'], ['app_shared_inlineEditor_dialog_cancel', 'Отменить'], From b49f81556a27299db8ea3026e5f944dcfa94b8c7 Mon Sep 17 00:00:00 2001 From: serge-rider Date: Thu, 13 May 2021 22:27:55 +0300 Subject: [PATCH 06/16] =?UTF-8?q?=D0=A1B-938=20User=20account=20link=20fix?= =?UTF-8?q?ed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/auth/impl/WebServiceAuthImpl.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java index f4d6db88ed..bf453ddca6 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java @@ -141,11 +141,10 @@ public class WebServiceAuthImpl implements DBWServiceAuth { userId); } } - } else { - // We may need to associate new credentials with active user - if (linkWithActiveUser) { - securityController.setUserCredentials(userId, authProvider, userCredentials); - } + } + // We may need to associate new credentials with active user + if (linkWithActiveUser) { + securityController.setUserCredentials(userId, authProvider, userCredentials); } } @@ -153,7 +152,7 @@ public class WebServiceAuthImpl implements DBWServiceAuth { throw new DBCException("Invalid user credentials"); } } - if (curUser != null && !curUser.getUserId().equals(userId)) { + if (linkWithActiveUser && curUser != null && !curUser.getUserId().equals(userId)) { log.debug("Attempt to authorize user '" + userId + "' while user '" + curUser.getUserId() + "' already authorized"); throw new DBCException("You cannot authorize with different users credentials"); } From 0edc1d2a8136ee4f6389692d39b1c3e5abbd6a68 Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 13 May 2021 23:07:26 +0300 Subject: [PATCH 07/16] fix(core-app): CB-874 navigation tree behavior --- .../NavigationTreeNode/useNavigationNode.ts | 21 ++---- .../src/NavigationTree/useElementsTree.ts | 68 ++++++++++--------- .../NodesManager/NavNodeManagerService.ts | 49 +++++++------ .../src/ConnectionInfoResource.ts | 16 +++-- .../src/Resource/CachedMapResource.ts | 4 +- .../src/DataGrid/Formatters/CellFormatter.tsx | 3 +- 6 files changed, 81 insertions(+), 80 deletions(-) diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts index 928f4c7075..1f201ad87a 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts @@ -9,13 +9,11 @@ import { useContext, useEffect, useState } from 'react'; import { useObjectRef } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; import type { NavNode } from '../../shared/NodesManager/EntityTypes'; import { EObjectFeature } from '../../shared/NodesManager/EObjectFeature'; import { useNode } from '../../shared/NodesManager/useNode'; import { useChildren } from '../../shared/useChildren'; -import { NavigationTreeService } from '../NavigationTreeService'; import { TreeContext } from '../TreeContext'; interface INavigationNode { @@ -37,7 +35,6 @@ export function useNavigationNode(node: NavNode): INavigationNode { const contextRef = useObjectRef({ context: useContext(TreeContext), }); - const navigationTreeService = useService(NavigationTreeService); const [processing, setProcessing] = useState(false); const { isLoading } = useNode(node.id); const children = useChildren(node.id); @@ -56,20 +53,6 @@ export function useNavigationNode(node: NavNode): INavigationNode { expanded = false; } - const handleExpand = async () => { - if (!expanded) { - const timeout = setTimeout(() => setProcessing(true), 1); - const state = await navigationTreeService.loadNestedNodes(node.id); - clearTimeout(timeout); - setProcessing(false); - if (!state) { - contextRef.context?.tree.expand(node, false); - return; - } - } - contextRef.context?.tree.expand(node, !expanded); - }; - const handleOpen = async () => { setProcessing(true); try { @@ -79,6 +62,10 @@ export function useNavigationNode(node: NavNode): INavigationNode { } }; + const handleExpand = async () => { + contextRef.context?.tree.expand(node, !expanded); + }; + const handleSelect = (multiple = false, nested = false) => { contextRef.context?.tree.select(node, multiple, nested); }; diff --git a/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts b/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts index 34aa0dac77..9ba3e45a16 100644 --- a/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts +++ b/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts @@ -76,6 +76,36 @@ export function useElementsTree(options: IOptions): IElementsTree { const state = options.localState || localTreeNodesState; + async function loadTree(nodeId: string) { + let children = [nodeId]; + + while (children.length > 0) { + const nextChildren: string[] = []; + + for (const child of children) { + const nodeState = state.get(child); + if (!nodeState.expanded && child !== options.root) { + continue; + } + + const loaded = await navTreeService.loadNestedNodes(child); + + if (!loaded) { + const node = navNodeInfoResource.get(child); + + if (node) { + elementsTree.expand(node, false); + } + continue; + } + + nextChildren.push(...(navTreeResource.get(child) || [])); + } + + children = nextChildren; + } + } + function getNodeChildren(nodeId: string): string[] { const node = navNodeInfoResource.get(nodeId); @@ -166,10 +196,14 @@ export function useElementsTree(options: IOptions): IElementsTree { options.onFilter?.(node, value); }, - expand(node: NavNode, state: boolean) { + async expand(node: NavNode, state: boolean) { const treeNodeState = this.state.get(node.id); treeNodeState.expanded = state; + if (state) { + await loadTree(node.id); + } + options.onExpand?.(node, state); }, select(node: NavNode, multiple: boolean, nested: boolean) { @@ -192,39 +226,9 @@ export function useElementsTree(options: IOptions): IElementsTree { }, }, undefined, { root: observable.ref, renderers: observable.ref }); - async function refreshNode(nodeId: string) { - let children = [nodeId]; - - while (children.length > 0) { - const nextChildren: string[] = []; - - for (const child of children) { - const nodeState = state.get(child); - if (!nodeState.expanded && child !== options.root) { - continue; - } - - const loaded = await navTreeService.loadNestedNodes(child); - - if (!loaded) { - const node = navNodeInfoResource.get(child); - - if (node) { - elementsTree.expand(node, false); - } - continue; - } - - nextChildren.push(...(navTreeResource.get(child) || [])); - } - - children = nextChildren; - } - } - useExecutor({ executor: navTreeResource.onNodeRefresh, - handlers: [refreshNode], + handlers: [loadTree], }); useExecutor({ diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts index 606d5fbb0c..d5d6ec5d13 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts @@ -6,6 +6,8 @@ * you may not use this file except in compliance with the License. */ +import { runInAction } from 'mobx'; + import { ConnectionAuthService, Connection, ConnectionInfoResource } from '@cloudbeaver/core-connections'; @@ -140,14 +142,14 @@ export class NavNodeManagerService extends Bootstrap { getTree(navNodeKey: NavNodeKey[]): Array getTree(navNodeId: string | NavNodeKey | NavNodeKey[]): string[] | undefined | Array { if (typeof navNodeId === 'string') { - return this.navTree.data.get(navNodeId); + return this.navTree.get(navNodeId); } if (Array.isArray(navNodeId)) { return navNodeId.map(node => this.navTree.data.get(node.nodeId)); } - return this.navTree.data.get(navNodeId.nodeId); + return this.navTree.get(navNodeId.nodeId); } loadTree(navNodeId: string): Promise { @@ -339,35 +341,40 @@ export class NavNodeManagerService extends Bootstrap { // const count = ResourceKeyUtils.count(key); await this.navTree.load(ROOT_NODE_PATH); - await ResourceKeyUtils.forEachAsync(key, async key => { - const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key); - this.navTree.markTreeOutdated(nodeId); - // addOpenedConnection - const connectionInfo = this.connectionInfo.get(key); + runInAction(() => { + ResourceKeyUtils.forEach(key, async key => { + const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key); + this.navTree.markTreeOutdated(nodeId); - if (!connectionInfo?.connected) { - this.removeTree(nodeId); - } + if (this.navTree.has(nodeId)) { + const connectionInfo = this.connectionInfo.get(key); - // if (count > 1) { - this.navNodeInfoResource.markOutdated(nodeId); + if (!connectionInfo?.connected) { + this.removeTree(nodeId); + } + } + + // if (count > 1) { + this.navNodeInfoResource.markOutdated(nodeId); // } else { // await this.navNodeInfoResource.refresh(nodeId); // } + }); }); } - private async connectionRemoveHandler(key: ResourceKey) { - ResourceKeyUtils.forEach(key, key => { - // deleteConnection - const navNodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key); + private connectionRemoveHandler(key: ResourceKey) { + runInAction(() => { + ResourceKeyUtils.forEach(key, key => { + const navNodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key); - const node = this.getNode(navNodeId); - if (!node) { - return; - } - this.navTree.deleteInNode(node.parentId, [navNodeId]); + const node = this.getNode(navNodeId); + if (!node) { + return; + } + this.navTree.deleteInNode(node.parentId, [navNodeId]); + }); }); } diff --git a/webapp/packages/core-connections/src/ConnectionInfoResource.ts b/webapp/packages/core-connections/src/ConnectionInfoResource.ts index 117cd1278b..4dd332ec13 100644 --- a/webapp/packages/core-connections/src/ConnectionInfoResource.ts +++ b/webapp/packages/core-connections/src/ConnectionInfoResource.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { action, makeObservable } from 'mobx'; +import { action, makeObservable, runInAction } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { Executor, ExecutorInterrupter, IExecutor } from '@cloudbeaver/core-executor'; @@ -102,14 +102,16 @@ export class ConnectionInfoResource extends CachedMapResource { + const unrestoredConnectionIdList = Array.from(this.data.values()) + .map(connection => connection.id) + .filter(connectionId => !connections.some(connection => connection.id === connectionId)); + + this.delete(resourceKeyList(unrestoredConnectionIdList)); + }); + await this.addList(connections); - const unrestoredConnectionIdList = Array.from(this.data.values()) - .map(connection => connection.id) - .filter(connectionId => !connections.some(connection => connection.id === connectionId)); - - this.delete(resourceKeyList(unrestoredConnectionIdList)); - await this.onSessionUpdate.execute(connections); }); } finally { diff --git a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts index ae307c3ffc..20b3a2a250 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { computed, makeObservable } from 'mobx'; +import { action, computed, makeObservable } from 'mobx'; import { Executor, IExecutor } from '@cloudbeaver/core-executor'; import { MetadataMap } from '@cloudbeaver/core-utils'; @@ -77,6 +77,8 @@ export abstract class CachedMapResource< })); makeObservable(this, { + set: action, + delete: action, values: computed, keys: computed, }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx index b2aac49aa3..594f977f83 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/CellFormatter.tsx @@ -29,7 +29,6 @@ function getClasses(rawValue: any) { export const CellFormatter: React.FC = observer(function CellFormatter({ rowIdx, row, column, isCellSelected }) { const editorRef = useRef(null); - const cellRef = useRef(null); const cellContext = useContext(CellContext); const context = useContext(DataGridContext); const editingContext = useContext(EditingContext); @@ -66,7 +65,7 @@ export const CellFormatter: React.FC = observer(function CellFor return styled()( <> - + {value} {(isCellSelected || cellContext?.mouse.state.mouseEnter) && context && ( From b15c4f5b372bfa9598da928c7731e5375c6d6205 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Fri, 14 May 2021 14:23:36 +0300 Subject: [PATCH 08/16] feat(core-dialogs): CB-853 limit auth dialog footer width --- .../src/DatabaseAuthDialog/DBAuthDialogFooter.tsx | 2 +- .../src/DatabaseAuthDialog/DatabaseAuthDialog.tsx | 2 +- webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx | 2 +- .../plugin-authentication/src/Dialog/AuthDialogFooter.tsx | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/webapp/packages/core-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx b/webapp/packages/core-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx index d54168c4fe..9a228fdd5c 100644 --- a/webapp/packages/core-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx +++ b/webapp/packages/core-connections/src/DatabaseAuthDialog/DBAuthDialogFooter.tsx @@ -15,7 +15,7 @@ import { useTranslate } from '@cloudbeaver/core-localization'; const styles = css` footer-container { display: flex; - height: 100%; + width: min-content; flex: 1; align-items: center; justify-content: flex-end; diff --git a/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx b/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx index 5842b4f60e..f8ba8139f3 100644 --- a/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx +++ b/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx @@ -44,7 +44,7 @@ const styles = composes( css` CommonDialogWrapper { min-height: 400px; - min-width: 600px; + min-width: 500px; } SubmittingForm { overflow: auto; diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx index 2427e5c84d..c2be7c71d2 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx @@ -32,7 +32,7 @@ const styles = composes( css` CommonDialogWrapper { min-height: 400px; - min-width: 600px; + min-width: 500px; } SubmittingForm { overflow: auto; diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx index c65b7f331d..494af736f6 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx @@ -15,7 +15,7 @@ import { useTranslate } from '@cloudbeaver/core-localization'; const styles = css` footer-container { display: flex; - height: 100%; + width: min-content; flex: 1; align-items: center; justify-content: flex-end; From b9f5c22a0d9895ed9d014ff8621778fbad2b4a03 Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 14 May 2021 14:42:58 +0300 Subject: [PATCH 09/16] fix: CB-989 CB-821 navigation tree action processing --- .../NavigationNodeElement.tsx | 1 + .../NavigationTreeNode/useNavigationNode.ts | 51 ++++++++----------- .../src/NavigationTree/useElementsTree.ts | 50 +++++++++--------- .../src/Tree/TreeNode/TreeNode.tsx | 47 ++++++++++++----- .../src/Tree/TreeNode/TreeNodeContext.ts | 9 ++-- .../src/Tree/TreeNode/TreeNodeExpand.tsx | 10 ++-- .../src/Tree/TreeNode/TreeNodeFilter.tsx | 2 +- .../src/Tree/TreeNode/TreeNodeIcon.tsx | 2 +- 8 files changed, 98 insertions(+), 74 deletions(-) diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeElement.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeElement.tsx index 7d97e4c807..acd7c7ed8f 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeElement.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeElement.tsx @@ -43,5 +43,6 @@ export const NavigationNodeElement = observer(function NavigationNodeElement({ return null; } + // TODO: after node update reference can be lost and NavigationNode skip update return ; }); diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts index 1f201ad87a..642475db1a 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { useContext, useEffect, useState } from 'react'; +import { useContext, useEffect } from 'react'; import { useObjectRef } from '@cloudbeaver/core-blocks'; @@ -24,24 +24,30 @@ interface INavigationNode { loading: boolean; expanded: boolean; leaf: boolean; - handleExpand: () => void; - handleOpen: () => void; - handleSelect: (isMultiple?: boolean, nested?: boolean) => void; - handleFilter: (value: string) => void; + handleExpand: () => Promise; + handleOpen: () => Promise; + handleSelect: (isMultiple?: boolean, nested?: boolean) => Promise; + handleFilter: (value: string) => Promise; filterValue: string; } -export function useNavigationNode(node: NavNode): INavigationNode { +export function useNavigationNode({ id }: NavNode): INavigationNode { const contextRef = useObjectRef({ context: useContext(TreeContext), }); - const [processing, setProcessing] = useState(false); - const { isLoading } = useNode(node.id); + const { node, isLoading } = useNode(id); + + // TODO: hack to provide actual node information + if (!node) { + throw new Error('Node should exists'); + } + const children = useChildren(node.id); - const loading = isLoading() || children.isLoading() || processing; + const loading = isLoading() || children.isLoading(); const state = contextRef.context?.tree.getNodeState(node.id); const isExpanded = state?.expanded || false; + let leaf = isLeaf(node); let expanded = isExpanded && !leaf; @@ -53,26 +59,13 @@ export function useNavigationNode(node: NavNode): INavigationNode { expanded = false; } - const handleOpen = async () => { - setProcessing(true); - try { - await contextRef.context?.onOpen?.(node); - } finally { - setProcessing(false); - } - }; - - const handleExpand = async () => { - contextRef.context?.tree.expand(node, !expanded); - }; - - const handleSelect = (multiple = false, nested = false) => { - contextRef.context?.tree.select(node, multiple, nested); - }; - - const handleFilter = (value: string) => { - contextRef.context?.tree.filter(node, value); - }; + const handleOpen = async () => contextRef.context?.onOpen?.(node); + const handleExpand = async () => contextRef.context?.tree.expand(node, !expanded); + const handleSelect = async ( + multiple = false, + nested = false + ) => contextRef.context?.tree.select(node, multiple, nested); + const handleFilter = async (value: string) => contextRef.context?.tree.filter(node, value); useEffect(() => () => { if (!contextRef.context?.selectionTree) { diff --git a/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts b/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts index 9ba3e45a16..55376b5148 100644 --- a/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts +++ b/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts @@ -45,11 +45,11 @@ interface IOptions { localState?: MetadataMap; filters?: IElementsTreeFilter[]; renderers?: IElementsTreeCustomRenderer[]; - customSelect?: (node: NavNode, multiple: boolean, nested: boolean) => void; + customSelect?: (node: NavNode, multiple: boolean, nested: boolean) => Promise | void; isGroup?: (node: NavNode) => boolean; - onExpand?: (node: NavNode, state: boolean) => void; - onSelect?: (node: NavNode, state: boolean) => void; - onFilter?: (node: NavNode, value: string) => void; + onExpand?: (node: NavNode, state: boolean) => Promise | void; + onSelect?: (node: NavNode, state: boolean) => Promise | void; + onFilter?: (node: NavNode, value: string) => Promise | void; } export interface IElementsTree { @@ -58,9 +58,9 @@ export interface IElementsTree { state: MetadataMap; getNodeState: (nodeId: string) => ITreeNodeState; getNodeChildren: (nodeId: string) => string[]; - filter: (node: NavNode, value: string) => void; - select: (node: NavNode, multiple: boolean, nested: boolean) => void; - expand: (node: NavNode, state: boolean) => void; + filter: (node: NavNode, value: string) => Promise; + select: (node: NavNode, multiple: boolean, nested: boolean) => Promise; + expand: (node: NavNode, state: boolean) => Promise; } export function useElementsTree(options: IOptions): IElementsTree { @@ -94,7 +94,7 @@ export function useElementsTree(options: IOptions): IElementsTree { const node = navNodeInfoResource.get(child); if (node) { - elementsTree.expand(node, false); + await elementsTree.expand(node, false); } continue; } @@ -135,7 +135,7 @@ export function useElementsTree(options: IOptions): IElementsTree { return nestedChildren; } - function clearSelection(nodeId: string) { + async function clearSelection(nodeId: string) { const node = navNodeInfoResource.get(nodeId); const ignore = node && options.isGroup?.(node) @@ -150,14 +150,14 @@ export function useElementsTree(options: IOptions): IElementsTree { const node = navNodeInfoResource.get(id); if (node) { - options.onSelect(node, false); + await options.onSelect(node, false); } } } } } - function setSelection(nodeId: string, selected: boolean): void { + async function setSelection(nodeId: string, selected: boolean): Promise { const node = navNodeInfoResource.get(nodeId); if (!node) { @@ -174,12 +174,16 @@ export function useElementsTree(options: IOptions): IElementsTree { const children = getNodeChildren(nodeId); for (const child of children) { - setSelection(child, selected); + await setSelection(child, selected); + } + + if (children.length === 0) { + return; } } treeNodeState.selected = selected; - options.onSelect?.(node, selected); + await options.onSelect?.(node, selected); } const elementsTree = useObjectRef({ @@ -190,11 +194,11 @@ export function useElementsTree(options: IOptions): IElementsTree { return this.state.get(nodeId); }, getNodeChildren, - filter(node: NavNode, value: string) { + async filter(node: NavNode, value: string) { const treeNodeState = this.state.get(node.id); treeNodeState.filter = value; - options.onFilter?.(node, value); + await options.onFilter?.(node, value); }, async expand(node: NavNode, state: boolean) { const treeNodeState = this.state.get(node.id); @@ -204,25 +208,25 @@ export function useElementsTree(options: IOptions): IElementsTree { await loadTree(node.id); } - options.onExpand?.(node, state); + await options.onExpand?.(node, state); }, - select(node: NavNode, multiple: boolean, nested: boolean) { + async select(node: NavNode, multiple: boolean, nested: boolean) { if (options.customSelect) { - options.customSelect(node, multiple, nested); + await options.customSelect(node, multiple, nested); return; } const treeNodeState = this.state.get(node.id); if (!multiple) { - clearSelection(node.id); + await clearSelection(node.id); if (treeNodeState.selected) { return; } } - setSelection(node.id, !treeNodeState.selected); + await setSelection(node.id, !treeNodeState.selected); }, }, undefined, { root: observable.ref, renderers: observable.ref }); @@ -242,12 +246,12 @@ export function useElementsTree(options: IOptions): IElementsTree { useExecutor({ executor: navTreeResource.onItemDelete, - handlers: [function collapseDeletedTree(key) { - ResourceKeyUtils.forEach(key, key => { + handlers: [async function collapseDeletedTree(key) { + await ResourceKeyUtils.forEachAsync(key, async key => { const node = navNodeInfoResource.get(key); if (node) { - elementsTree.expand(node, false); + await elementsTree.expand(node, false); } }); }], diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx index 05dee4371f..4fe45d60b3 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx @@ -22,11 +22,11 @@ interface Props { expanded?: boolean; leaf?: boolean; className?: string; - onExpand?: () => void; - onSelect?: (multiple?: boolean, nested?: boolean) => void; - onFilter?: (value: string) => void; + onExpand?: () => Promise | void; + onSelect?: (multiple?: boolean, nested?: boolean) => Promise | void; + onFilter?: (value: string) => Promise | void; filterValue?: string; - onOpen?: () => void; + onOpen?: () => Promise | void; } export const TreeNode: React.FC = memo(function TreeNode({ @@ -41,23 +41,45 @@ export const TreeNode: React.FC = memo(function TreeNode({ }) { const handlersRef = useObjectRef(handlers); + async function processAction(action: () => Promise) { + const timeout = setTimeout(() => { + nodeContext.processing = true; + }, 250); + + try { + await action(); + } finally { + clearTimeout(timeout); + nodeContext.processing = false; + } + } + const nodeContext = useObjectRef({ + processing: false, loading, selected, expanded, leaf, filterValue, - expand() { - handlersRef.onExpand?.(); + async expand() { + await processAction(async () => { + await handlersRef.onExpand?.(); + }); }, - select(multiple?: boolean, nested?: boolean): void { - handlersRef.onSelect?.(multiple, nested); + async select(multiple?: boolean, nested?: boolean) { + await processAction(async () => { + await handlersRef.onSelect?.(multiple, nested); + }); }, - filter(value: string): void { - handlersRef.onFilter?.(value); + async filter(value: string) { + await processAction(async () => { + await handlersRef.onFilter?.(value); + }); }, - open() { - handlersRef.onOpen?.(); + async open() { + await processAction(async () => { + await handlersRef.onOpen?.(); + }); }, }, { loading, @@ -66,6 +88,7 @@ export const TreeNode: React.FC = memo(function TreeNode({ leaf, filterValue, }, { + processing: observable.ref, loading: observable.ref, selected: observable.ref, expanded: observable.ref, diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts index 17b9dc67ff..8ea6ad5daa 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts @@ -9,15 +9,16 @@ import { createContext } from 'react'; export interface ITreeNodeContext { + processing: boolean; expanded: boolean; loading: boolean; selected: boolean; leaf: boolean; - select: (multiple?: boolean, nested?: boolean) => void; - filter: (value: string) => void; + select: (multiple?: boolean, nested?: boolean) => Promise; + filter: (value: string) => Promise; filterValue: string; - expand: () => void; - open: () => void; + expand: () => Promise; + open: () => Promise; } export const TreeNodeContext = createContext(null); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx index a970ddd51d..b744897a9d 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx @@ -52,11 +52,13 @@ export const TreeNodeExpand: React.FC = observer(function TreeNodeExpand( event.preventDefault(); }; + const loading = context.loading || context.processing; + return styled(styles)( - - {context.loading && } - {!context.loading && !context.leaf && !leaf && big && } - {!context.loading && !context.leaf && !leaf && !big && } + + {loading && } + {!loading && !context.leaf && !leaf && big && } + {!loading && !context.leaf && !leaf && !big && } ); }); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeFilter.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeFilter.tsx index 2e1847cc29..22c59b75bb 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeFilter.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeFilter.tsx @@ -100,7 +100,7 @@ export const TreeNodeFilter: React.FC = observer(function TreeNodeFilter( } }, [filterEnabled]); - useEffect(() => () => context.filter(''), []); + useEffect(() => () => { context.filter(''); }, []); return styled(useStyles(styles))( diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.tsx index d97d4ea901..bf87a1880b 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.tsx @@ -27,7 +27,7 @@ export const TreeNodeIcon: React.FC = function TreeNodeIcon({ children, }) { return styled(styles)( - + {children} From 248eb4d8385dd1ee560ce4bb3df27f2985c1be23 Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 14 May 2021 15:23:06 +0300 Subject: [PATCH 10/16] fix(core-blocks): CB-991 pass div props through containers --- .../src/Containers/ColoredContainer.tsx | 11 ++++---- .../core-blocks/src/Containers/Container.tsx | 11 ++++---- .../core-blocks/src/Containers/Group.tsx | 17 +++++++------ .../core-blocks/src/Containers/GroupItem.tsx | 14 +++-------- .../core-blocks/src/Containers/GroupTitle.tsx | 14 +++-------- .../src/Containers/IContainerProps.ts | 2 +- .../Containers/filterContainerFakeProps.ts | 25 +++++++++++++++++++ .../src/Containers/filterLayoutFakeProps.ts | 23 +++++++++++++++++ 8 files changed, 77 insertions(+), 40 deletions(-) create mode 100644 webapp/packages/core-blocks/src/Containers/filterContainerFakeProps.ts create mode 100644 webapp/packages/core-blocks/src/Containers/filterLayoutFakeProps.ts diff --git a/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx b/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx index 4dac1c0024..aa1770abd6 100644 --- a/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx +++ b/webapp/packages/core-blocks/src/Containers/ColoredContainer.tsx @@ -8,12 +8,11 @@ import { forwardRef } from 'react'; +import { filterContainerFakeProps } from './filterContainerFakeProps'; import type { IContainerProps } from './IContainerProps'; -export const ColoredContainer = forwardRef(function ColoredContainer({ children, className }, ref) { - return ( -
- {children} -
- ); +export const ColoredContainer = forwardRef>(function ColoredContainer(props, ref) { + const divProps = filterContainerFakeProps(props); + + return
; }); diff --git a/webapp/packages/core-blocks/src/Containers/Container.tsx b/webapp/packages/core-blocks/src/Containers/Container.tsx index 729edee681..246412a3b5 100644 --- a/webapp/packages/core-blocks/src/Containers/Container.tsx +++ b/webapp/packages/core-blocks/src/Containers/Container.tsx @@ -8,12 +8,11 @@ import { forwardRef } from 'react'; +import { filterContainerFakeProps } from './filterContainerFakeProps'; import type { IContainerProps } from './IContainerProps'; -export const Container = forwardRef(function Container({ children, className }, ref) { - return ( -
- {children} -
- ); +export const Container = forwardRef>(function Container(props, ref) { + const divProps = filterContainerFakeProps(props); + + return
; }); diff --git a/webapp/packages/core-blocks/src/Containers/Group.tsx b/webapp/packages/core-blocks/src/Containers/Group.tsx index a366b44f70..1d0b2084bf 100644 --- a/webapp/packages/core-blocks/src/Containers/Group.tsx +++ b/webapp/packages/core-blocks/src/Containers/Group.tsx @@ -8,19 +8,22 @@ import { forwardRef } from 'react'; +import { filterContainerFakeProps } from './filterContainerFakeProps'; import type { IContainerProps } from './IContainerProps'; interface Props extends IContainerProps { - className?: string; form?: boolean; center?: boolean; box?: boolean; } -export const Group = forwardRef(function Group({ children, className }, ref) { - return ( -
- {children} -
- ); +export const Group = forwardRef>(function Group({ + form, + center, + box, + ...rest +}, ref) { + const divProps = filterContainerFakeProps(rest); + + return
; }); diff --git a/webapp/packages/core-blocks/src/Containers/GroupItem.tsx b/webapp/packages/core-blocks/src/Containers/GroupItem.tsx index 7fda0f808b..ac9d627b2e 100644 --- a/webapp/packages/core-blocks/src/Containers/GroupItem.tsx +++ b/webapp/packages/core-blocks/src/Containers/GroupItem.tsx @@ -6,16 +6,10 @@ * you may not use this file except in compliance with the License. */ +import { filterLayoutFakeProps } from './filterLayoutFakeProps'; import type { ILayoutSizeProps } from './ILayoutSizeProps'; -interface Props extends ILayoutSizeProps { - className?: string; -} - -export const GroupItem: React.FC = function GroupItem({ children, className }) { - return ( -
- {children} -
- ); +export const GroupItem: React.FC> = function GroupItem(props) { + const divProps = filterLayoutFakeProps(props); + return
; }; diff --git a/webapp/packages/core-blocks/src/Containers/GroupTitle.tsx b/webapp/packages/core-blocks/src/Containers/GroupTitle.tsx index 15d5df45f4..aa75a006f2 100644 --- a/webapp/packages/core-blocks/src/Containers/GroupTitle.tsx +++ b/webapp/packages/core-blocks/src/Containers/GroupTitle.tsx @@ -6,16 +6,10 @@ * you may not use this file except in compliance with the License. */ +import { filterLayoutFakeProps } from './filterLayoutFakeProps'; import type { ILayoutSizeProps } from './ILayoutSizeProps'; -interface Props extends ILayoutSizeProps { - className?: string; -} - -export const GroupTitle: React.FC = function GroupTitle({ children, className }) { - return ( -

- {children} -

- ); +export const GroupTitle: React.FC> = function GroupTitle(props) { + const divProps = filterLayoutFakeProps(props); + return

; }; diff --git a/webapp/packages/core-blocks/src/Containers/IContainerProps.ts b/webapp/packages/core-blocks/src/Containers/IContainerProps.ts index 7245a36ea7..4a10253242 100644 --- a/webapp/packages/core-blocks/src/Containers/IContainerProps.ts +++ b/webapp/packages/core-blocks/src/Containers/IContainerProps.ts @@ -8,7 +8,7 @@ import type { ILayoutSizeProps } from './ILayoutSizeProps'; -export interface IContainerProps extends ILayoutSizeProps, React.HTMLAttributes { +export interface IContainerProps extends ILayoutSizeProps { baseline?: boolean; center?: boolean; vertical?: boolean; diff --git a/webapp/packages/core-blocks/src/Containers/filterContainerFakeProps.ts b/webapp/packages/core-blocks/src/Containers/filterContainerFakeProps.ts new file mode 100644 index 0000000000..8f378cb977 --- /dev/null +++ b/webapp/packages/core-blocks/src/Containers/filterContainerFakeProps.ts @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { filterLayoutFakeProps } from './filterLayoutFakeProps'; +import type { IContainerProps } from './IContainerProps'; + +export function filterContainerFakeProps(props: T): Omit { + const { + baseline, + center, + vertical, + wrap, + overflow, + parent, + gap, + ...rest + } = filterLayoutFakeProps(props); + + return rest as Omit; +} diff --git a/webapp/packages/core-blocks/src/Containers/filterLayoutFakeProps.ts b/webapp/packages/core-blocks/src/Containers/filterLayoutFakeProps.ts new file mode 100644 index 0000000000..c403edb89a --- /dev/null +++ b/webapp/packages/core-blocks/src/Containers/filterLayoutFakeProps.ts @@ -0,0 +1,23 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 type { ILayoutSizeProps } from './ILayoutSizeProps'; + +export function filterLayoutFakeProps(props: T): Omit { + const { + keepSize, + tiny, + small, + medium, + large, + fill, + ...rest + } = props; + + return rest; +} From d8caa1f4244085b88a706f6cfe2a270b4be920a2 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Fri, 14 May 2021 15:28:35 +0300 Subject: [PATCH 11/16] feat(core-dialogs): CB-853 add grid to confirmation dialog --- .../src/CommonDialog/ConfirmationDialog.tsx | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/webapp/packages/core-dialogs/src/CommonDialog/ConfirmationDialog.tsx b/webapp/packages/core-dialogs/src/CommonDialog/ConfirmationDialog.tsx index 17af9d110a..376559c2ac 100644 --- a/webapp/packages/core-dialogs/src/CommonDialog/ConfirmationDialog.tsx +++ b/webapp/packages/core-dialogs/src/CommonDialog/ConfirmationDialog.tsx @@ -52,22 +52,24 @@ export const ConfirmationDialog: DialogComponent = fu return styled(useStyles(commonDialogThemeStyle, commonDialogBaseStyle, style))(
- + {icon && } - -

- {subTitle} -
- - - +
+ +

+ {rejectDialog && ( + + + + )}
+ {subTitle && {subTitle}}
- - + + - +