diff --git a/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls index 996d076caf..630a985e6c 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls @@ -56,8 +56,8 @@ type DatabaseObjectInfo { type NavigatorNodeInfo { # Node ID - generally a full path to the node from root of tree id: ID! - # # Node URI - a unique path to a node including all parent nodes - # uri: ID! @since(version: "23.3.1") + # Node URI - a unique path to a node including all parent nodes + uri: ID! @since(version: "23.3.1") # Node human readable name name: String #Node full name diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.m.css b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.m.css index cbb45c7cba..6dd5c7563a 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.m.css +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.m.css @@ -1,10 +1,8 @@ .treeNodeNested { box-sizing: border-box; padding-left: 8px; - display: none; &.root { padding: 0; - display: block; } } diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/DATA_CONTEXT_NAV_NODE_ID.ts b/webapp/packages/core-navigation-tree/src/NodesManager/DATA_CONTEXT_NAV_NODE_ID.ts new file mode 100644 index 0000000000..971a33d97a --- /dev/null +++ b/webapp/packages/core-navigation-tree/src/NodesManager/DATA_CONTEXT_NAV_NODE_ID.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { createDataContext } from '@cloudbeaver/core-data-context'; + +export const DATA_CONTEXT_NAV_NODE_ID = createDataContext('nav-node-id'); diff --git a/webapp/packages/core-navigation-tree/src/index.ts b/webapp/packages/core-navigation-tree/src/index.ts index c76f59c59d..00c9c6c9e3 100644 --- a/webapp/packages/core-navigation-tree/src/index.ts +++ b/webapp/packages/core-navigation-tree/src/index.ts @@ -5,6 +5,7 @@ export * from './NodesManager/extensions/IObjectNavNodeProvider'; export * from './NodesManager/DBObjectResource'; export * from './NodesManager/DATA_CONTEXT_ACTIVE_NODE'; export * from './NodesManager/DATA_CONTEXT_NAV_NODE'; +export * from './NodesManager/DATA_CONTEXT_NAV_NODE_ID'; export * from './NodesManager/DATA_CONTEXT_NAV_NODES'; export * from './NodesManager/NavNodeInfoResource'; export * from './NodesManager/NavNodeManagerService'; diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/DataTransformers/TreeDataTransformer.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/DataTransformers/TreeDataTransformer.ts new file mode 100644 index 0000000000..e5a4c64782 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/DataTransformers/TreeDataTransformer.ts @@ -0,0 +1,9 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export type TreeDataTransformer = (nodeId: string, data: T) => T; diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/DataTransformers/applyTransforms.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/DataTransformers/applyTransforms.ts new file mode 100644 index 0000000000..7ea11fa424 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/DataTransformers/applyTransforms.ts @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { TreeDataTransformer } from './TreeDataTransformer'; + +export function applyTransforms(id: string, data: T, transformers?: TreeDataTransformer[]) { + if (!transformers) { + return data; + } + + for (const transformer of transformers) { + data = transformer(id, data); + } + + return data; +} diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/DataTransformers/rootTransformers.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/DataTransformers/rootTransformers.ts new file mode 100644 index 0000000000..ad16c6657d --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/DataTransformers/rootTransformers.ts @@ -0,0 +1,36 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { INode } from '../INode'; +import type { INodeState } from '../INodeState'; +import type { TreeDataTransformer } from './TreeDataTransformer'; + +export function rootNodeStateTransformer(root: string): TreeDataTransformer { + return function rootNodeStateTransformer(nodeId, data) { + if (nodeId === root) { + return { + ...data, + expanded: true, + }; + } + + return data; + }; +} + +export function rootNodeTransformer(root: string): TreeDataTransformer { + return function rootNodeTransformer(nodeId, data) { + if (nodeId === root) { + return { + ...data, + leaf: false, + }; + } + + return data; + }; +} diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/INode.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/INode.ts new file mode 100644 index 0000000000..d2759d641b --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/INode.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export interface INode { + name: string; + tooltip?: string; + icon?: string; + leaf?: boolean; +} diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/INodeRenderer.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/INodeRenderer.ts new file mode 100644 index 0000000000..d87914343c --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/INodeRenderer.ts @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export interface INodeComponentBaseProps { + nodeId: string; + offsetHeight: number; +} + +export interface INodeComponentProps extends INodeComponentBaseProps { + childrenRenderer: React.FC; +} + +export type NodeComponent = React.FC; + +export type INodeRenderer = (nodeId: string) => NodeComponent | null; diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/INodeState.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/INodeState.ts new file mode 100644 index 0000000000..3cf03b12d2 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/INodeState.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export interface INodeState { + selected: boolean; + expanded: boolean; +} diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/Node.tsx b/webapp/packages/plugin-navigation-tree/src/TreeNew/Node.tsx new file mode 100644 index 0000000000..79db54e011 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/Node.tsx @@ -0,0 +1,51 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; +import { useContext } from 'react'; + +import { TreeNode } from '@cloudbeaver/core-blocks'; + +import { TreeContext } from './contexts/TreeContext'; +import { TreeDataContext } from './contexts/TreeDataContext'; +import type { NodeComponent } from './INodeRenderer'; +import { NodeControl } from './NodeControl'; +import { useNodeDnD } from './useNodeDnD'; + +export const Node: NodeComponent = observer(function Node({ nodeId, offsetHeight, childrenRenderer }) { + const tree = useContext(TreeContext)!; + const data = useContext(TreeDataContext)!; + + const { expanded, selected } = data.getState(nodeId); + + const dndData = useNodeDnD(nodeId, () => { + if (!selected) { + tree.selectNode(nodeId, true); + } + }); + + function handleOpen() { + return tree.openNode(nodeId); + } + + function handleToggleExpand() { + return tree.expandNode(nodeId, !expanded); + } + + function handleSelect() { + tree.selectNode(nodeId, !selected); + } + + const NavigationTreeChildrenNew = childrenRenderer; + + return ( + + + {expanded && } + + ); +}); diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/NodeChildren.tsx b/webapp/packages/plugin-navigation-tree/src/TreeNew/NodeChildren.tsx new file mode 100644 index 0000000000..4e73f223f7 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/NodeChildren.tsx @@ -0,0 +1,79 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; +import { useContext, useId } from 'react'; + +import { getComputed, TreeNodeNested } from '@cloudbeaver/core-blocks'; + +import { NodeSizeCacheContext } from './contexts/NodeSizeCacheContext'; +import { TreeDataContext } from './contexts/TreeDataContext'; +import { TreeVirtualizationContext } from './contexts/TreeVirtualizationContext'; +import { NodeRenderer } from './NodeRenderer'; + +interface Props { + nodeId: string; + offsetHeight: number; + root?: boolean; +} + +const OVERSCAN = 128; + +function getPositionWithOverscan(position: number, forward: boolean) { + if (forward) { + return position - (position % OVERSCAN) + OVERSCAN; + } + + return position - (position % OVERSCAN); +} + +const NodeChildrenObserved = observer(function NodeChildren({ nodeId, offsetHeight, root }) { + const data = useContext(TreeDataContext)!; + const optimization = useContext(TreeVirtualizationContext)!; + const sizeCache = useContext(NodeSizeCacheContext)!; + const firstId = useId(); + const lastId = useId(); + const viewPortFrom = getComputed(() => getPositionWithOverscan(optimization.viewPort.from, false)) - offsetHeight; + const viewPortTo = getComputed(() => getPositionWithOverscan(optimization.viewPort.to, true)) - offsetHeight; + + const children = data.getChildren(nodeId); + + function renderChildren() { + let offset = 0; + let postFillHeight = 0; + + const elements = []; + + for (let i = 0; i < children.length; i++) { + const child = children[i]; + const size = sizeCache.getSize(child); + + if (offset + size < viewPortFrom) { + offset += size; + } else if (offset < viewPortTo) { + if (offset > 0 && elements.length === 0) { + elements.push(
); + } + + elements.push(); + offset += size; + } else { + postFillHeight += size; + } + } + + if (postFillHeight > 0) { + elements.push(
); + } + + return elements; + } + + return {renderChildren()}; +}); + +export const NodeChildren = NodeChildrenObserved; diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/NodeControl.tsx b/webapp/packages/plugin-navigation-tree/src/TreeNew/NodeControl.tsx new file mode 100644 index 0000000000..eafd487a9a --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/NodeControl.tsx @@ -0,0 +1,36 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; +import { forwardRef, useContext } from 'react'; + +import { TreeNodeControl, TreeNodeExpand, TreeNodeIcon, TreeNodeName } from '@cloudbeaver/core-blocks'; + +import { TreeContext } from './contexts/TreeContext'; +import { TreeDataContext } from './contexts/TreeDataContext'; + +interface Props { + nodeId: string; +} + +export const NodeControl = observer( + forwardRef(function NodeControl({ nodeId }, ref) { + const data = useContext(TreeDataContext)!; + const tree = useContext(TreeContext)!; + + const node = data.getNode(nodeId); + const height = tree.getNodeHeight(nodeId); + + return ( + + + + {node.name} + + ); + }), +); diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/NodeRenderer.tsx b/webapp/packages/plugin-navigation-tree/src/TreeNew/NodeRenderer.tsx new file mode 100644 index 0000000000..1719998b05 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/NodeRenderer.tsx @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; +import { useContext } from 'react'; + +import { TreeContext } from './contexts/TreeContext'; +import type { NodeComponent } from './INodeRenderer'; +import { Node } from './Node'; + +export const NodeRenderer: NodeComponent = observer(function NodeRenderer(props) { + const tree = useContext(TreeContext)!; + const NodeComponent = tree.getNodeComponent(props.nodeId) ?? Node; + + return ; +}); diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/Tree.tsx b/webapp/packages/plugin-navigation-tree/src/TreeNew/Tree.tsx new file mode 100644 index 0000000000..927a1cc2af --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/Tree.tsx @@ -0,0 +1,61 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import type { IDataContext } from '@cloudbeaver/core-data-context'; + +import { NodeSizeCacheContext } from './contexts/NodeSizeCacheContext'; +import { TreeContext } from './contexts/TreeContext'; +import { TreeDataContext } from './contexts/TreeDataContext'; +import { TreeDnDContext } from './contexts/TreeDnDContext'; +import { TreeVirtualizationContext } from './contexts/TreeVirtualizationContext'; +import type { INodeRenderer } from './INodeRenderer'; +import { NodeChildren } from './NodeChildren'; +import { useNodeSizeCache } from './useNodeSizeCache'; +import { useTree } from './useTree'; +import type { ITreeData } from './useTreeData'; +import { useTreeDnD } from './useTreeDnD'; +import { useTreeVirtualization } from './useTreeVirtualization'; + +export interface NavigationTreeNewProps { + data: ITreeData; + nodeRenderers?: INodeRenderer[]; + onNodeDoubleClick?(id: string): void | Promise; + getNodeDnDContext?(id: string, context: IDataContext): void; + getNodeHeight(id: string): number; +} + +export const Tree = observer(function Tree({ data, nodeRenderers, onNodeDoubleClick, getNodeDnDContext, getNodeHeight }) { + const tree = useTree({ + data, + nodeRenderers, + onNodeDoubleClick, + getNodeHeight, + }); + const mountOptimization = useTreeVirtualization(); + const elementsSizeCache = useNodeSizeCache(tree, data); + const treeDnD = useTreeDnD({ + getContext: getNodeDnDContext, + }); + + return ( +
+ + + + + + + + + + + +
+ ); +}); diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/TreeLazy.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/TreeLazy.ts new file mode 100644 index 0000000000..f53d85411e --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/TreeLazy.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { importLazyComponent } from '@cloudbeaver/core-utils'; + +export const Tree = importLazyComponent(() => import('./Tree').then(m => m.Tree)); diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/TreeState.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/TreeState.ts new file mode 100644 index 0000000000..b343427829 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/TreeState.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { MetadataMap } from '@cloudbeaver/core-utils'; + +import type { INodeState } from './INodeState'; + +export type TreeState = MetadataMap; diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/contexts/NodeSizeCacheContext.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/contexts/NodeSizeCacheContext.ts new file mode 100644 index 0000000000..03eaadef45 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/contexts/NodeSizeCacheContext.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { createContext } from 'react'; + +import type { INodeSizeCache } from '../useNodeSizeCache'; + +export const NodeSizeCacheContext = createContext(undefined); diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/contexts/TreeContext.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/contexts/TreeContext.ts new file mode 100644 index 0000000000..46bb2ad6d3 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/contexts/TreeContext.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { createContext } from 'react'; + +import type { ITree } from '../useTree'; + +export const TreeContext = createContext(undefined); diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/contexts/TreeDataContext.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/contexts/TreeDataContext.ts new file mode 100644 index 0000000000..39f7564a77 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/contexts/TreeDataContext.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { createContext } from 'react'; + +import type { ITreeData } from '../useTreeData'; + +export const TreeDataContext = createContext(undefined); diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/contexts/TreeDnDContext.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/contexts/TreeDnDContext.ts new file mode 100644 index 0000000000..7328faae1a --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/contexts/TreeDnDContext.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { createContext } from 'react'; + +import type { ITreeDnD } from '../useTreeDnD'; + +export const TreeDnDContext = createContext(undefined); diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/contexts/TreeVirtualizationContext.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/contexts/TreeVirtualizationContext.ts new file mode 100644 index 0000000000..99b8290777 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/contexts/TreeVirtualizationContext.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { createContext } from 'react'; + +import type { ITreeVirtualization } from '../useTreeVirtualization'; + +export const TreeVirtualizationContext = createContext(undefined); diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/useNodeDnD.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/useNodeDnD.ts new file mode 100644 index 0000000000..5c25f4d305 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/useNodeDnD.ts @@ -0,0 +1,31 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { useContext } from 'react'; + +import { useDataContext } from '@cloudbeaver/core-data-context'; +import { useDNDData } from '@cloudbeaver/core-ui'; + +import { TreeDnDContext } from './contexts/TreeDnDContext'; + +export function useNodeDnD(nodeId: string, onDragStart: () => void) { + const treeDnD = useContext(TreeDnDContext)!; + const context = useDataContext(); + + const dndData = useDNDData(context, { + canDrag: () => true, + onDragStart: () => { + treeDnD.getContext(nodeId, context); + onDragStart(); + }, + onDragEnd: () => { + treeDnD.getContext(nodeId, context); + }, + }); + + return dndData; +} diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/useNodeSizeCache.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/useNodeSizeCache.ts new file mode 100644 index 0000000000..787d56cdc7 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/useNodeSizeCache.ts @@ -0,0 +1,50 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { computed, type IComputedValue } from 'mobx'; +import { useState } from 'react'; + +import { useObjectRef } from '@cloudbeaver/core-blocks'; +import { MetadataMap } from '@cloudbeaver/core-utils'; + +import type { ITree } from './useTree'; +import type { ITreeData } from './useTreeData'; + +export interface INodeSizeCache { + getSize(id: string): number; +} + +export function useNodeSizeCache(tree: ITree, treeData: ITreeData): INodeSizeCache { + const [sizeRangeCache] = useState( + () => + new MetadataMap>((id, metadata) => + computed(() => { + let size = tree.getNodeHeight(id); + const expanded = treeData.getState(id).expanded; + + if (expanded) { + const children = treeData.getChildren(id); + + for (const child of children) { + size += metadata.get(child).get(); + } + } + + return size; + }), + ), + ); + + return useObjectRef( + () => ({ + getSize(id: string): number { + return sizeRangeCache.get(id).get(); + }, + }), + {}, + ); +} diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/useTree.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/useTree.ts new file mode 100644 index 0000000000..91978ad042 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/useTree.ts @@ -0,0 +1,84 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observable } from 'mobx'; + +import { useObservableRef } from '@cloudbeaver/core-blocks'; + +import type { INodeRenderer, NodeComponent } from './INodeRenderer'; +import type { ITreeData } from './useTreeData'; + +interface IOptions { + data: ITreeData; + nodeRenderers?: INodeRenderer[]; + onNodeDoubleClick?(id: string): void | Promise; + getNodeHeight(id: string): number; +} + +export interface ITree { + getNodeComponent(id: string): NodeComponent | null; + getNodeHeight(id: string): number; + + openNode(id: string): Promise; + expandNode(id: string, state: boolean): Promise; + selectNode(id: string, state: boolean): void; +} + +export function useTree(options: IOptions): ITree { + options = useObservableRef(options, { + data: observable.ref, + nodeRenderers: observable.ref, + onNodeDoubleClick: observable.ref, + getNodeHeight: observable.ref, + }); + + const data = useObservableRef( + () => ({ + getNodeComponent(id: string): NodeComponent | null { + if (!options.nodeRenderers) { + return null; + } + + for (const renderer of options.nodeRenderers) { + const component = renderer(id); + + if (component) { + return component; + } + } + + return null; + }, + getNodeHeight(id: string): number { + return options.getNodeHeight(id); + }, + async openNode(id: string) { + await options.onNodeDoubleClick?.(id); + }, + async expandNode(id: string, state: boolean) { + try { + options.data.updateState(id, { expanded: state }); + if (state) { + await options.data.load(id, true); + const children = options.data.getChildren(id); + + if (children.length === 0) { + options.data.updateState(id, { expanded: false }); + } + } + } catch (exception) {} + }, + selectNode(id: string, state: boolean) { + options.data.updateState(id, { selected: state }); + }, + }), + {}, + {}, + ); + + return data; +} diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/useTreeData.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/useTreeData.ts new file mode 100644 index 0000000000..b2a90633a6 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/useTreeData.ts @@ -0,0 +1,135 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { computed, IComputedValue, observable } from 'mobx'; +import { useEffect, useState } from 'react'; + +import { useObservableRef } from '@cloudbeaver/core-blocks'; +import { MetadataMap } from '@cloudbeaver/core-utils'; + +import { applyTransforms } from './DataTransformers/applyTransforms'; +import { rootNodeStateTransformer, rootNodeTransformer } from './DataTransformers/rootTransformers'; +import type { TreeDataTransformer } from './DataTransformers/TreeDataTransformer'; +import type { INode } from './INode'; +import type { INodeState } from './INodeState'; +import type { TreeState } from './TreeState'; +import { useTreeState } from './useTreeState'; + +interface IOptions { + rootId: string; + externalState?: TreeState; + getNode(id: string): INode; + getChildren: (node: string) => string[]; + load(nodeId: string, manual: boolean): Promise; + + childrenTransformers?: TreeDataTransformer[]; + nodeTransformers?: TreeDataTransformer[]; + stateTransformers?: TreeDataTransformer[]; +} + +export interface ITreeData { + rootId: string; + + getNode(id: string): INode; + getChildren: (node: string) => string[]; + getState(id: string): Readonly; + + updateState(id: string, state: Partial): void; + load(nodeId: string, manual: boolean): Promise; + update(): Promise; +} + +export function useTreeData(options: IOptions): ITreeData { + options = useObservableRef( + { + ...options, + childrenTransformers: [...(options.childrenTransformers || [])], + nodeTransformers: [...(options.nodeTransformers || [])], + stateTransformers: [...(options.stateTransformers || [])], + }, + { + rootId: observable.ref, + getNode: observable.ref, + getChildren: observable.ref, + load: observable.ref, + + childrenTransformers: observable.ref, + nodeTransformers: observable.ref, + stateTransformers: observable.ref, + }, + ); + const state = useTreeState(options.externalState); + const [nodeCache] = useState( + () => + new MetadataMap>(id => + computed(() => applyTransforms(id, options.getNode(id), [rootNodeTransformer(options.rootId), ...(options.nodeTransformers || [])])), + ), + ); + const [childrenCache] = useState( + () => + new MetadataMap>(id => + computed(() => applyTransforms(id, options.getChildren(id), options.childrenTransformers)), + ), + ); + const [stateCache] = useState( + () => + new MetadataMap>(id => + computed(() => applyTransforms(id, state.getState(id), [rootNodeStateTransformer(options.rootId), ...(options.stateTransformers || [])])), + ), + ); + + const treeData = useObservableRef( + () => ({ + getNode(id: string): INode { + return nodeCache.get(id).get(); + }, + getChildren(nodeId: string): string[] { + return childrenCache.get(nodeId).get(); + }, + getState(id: string): Readonly { + return stateCache.get(id).get(); + }, + updateState(id: string, state: Partial) { + this.state.updateState(id, state); + }, + async load(nodeId: string, manual: boolean) { + await options.load(nodeId, manual); + }, + async update() { + const nodes = [this.rootId]; + + while (nodes.length > 0) { + const nodeId = nodes.shift()!; + const state = this.state.getState(nodeId); + + if (!state.expanded) { + continue; + } + + await options.load(nodeId, false); + + const children = this.getChildren(nodeId); + + if (children.length === 0) { + this.state.updateState(nodeId, { expanded: false }); + continue; + } + + nodes.push(...children); + } + }, + }), + { state: observable.ref, rootId: observable.ref }, + { state, rootId: options.rootId }, + ); + + useEffect(() => { + treeData.update(); + }, [options.rootId]); + + return treeData; +} diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/useTreeDnD.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/useTreeDnD.ts new file mode 100644 index 0000000000..c444445491 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/useTreeDnD.ts @@ -0,0 +1,34 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { useObjectRef } from '@cloudbeaver/core-blocks'; +import type { IDataContext } from '@cloudbeaver/core-data-context'; + +interface IOptions { + getContext?(id: string, context: IDataContext): void; +} + +export interface ITreeDnD { + getContext(id: string, context: IDataContext): void; + canDrop(moveContext: IDataContext): boolean; +} + +export function useTreeDnD(options: IOptions): ITreeDnD { + options = useObjectRef(options); + + return useObjectRef( + () => ({ + getContext(id: string, context: IDataContext): void { + options.getContext?.(id, context); + }, + canDrop(moveContext: IDataContext): boolean { + return true; + }, + }), + {}, + ); +} diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/useTreeState.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/useTreeState.ts new file mode 100644 index 0000000000..aada8f2b52 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/useTreeState.ts @@ -0,0 +1,50 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observable } from 'mobx'; +import { useState } from 'react'; + +import { useObservableRef } from '@cloudbeaver/core-blocks'; +import { MetadataMap } from '@cloudbeaver/core-utils'; + +import type { INodeState } from './INodeState'; +import type { TreeState } from './TreeState'; + +export interface ITreeState { + getState(id: string): Readonly; + updateState(id: string, state: Partial): void; +} + +export function useTreeState(externalState?: TreeState): ITreeState { + const [innerState] = useState( + () => + new MetadataMap(() => + observable({ + expanded: false, + selected: false, + showInFilter: false, + }), + ), + ); + + const state = externalState ?? innerState; + + return useObservableRef( + () => ({ + getState(id: string): Readonly { + return state.get(id); + }, + updateState(id: string, state: Partial): void { + Object.assign(this.getState(id), state); + }, + }), + { + state: observable.ref, + }, + { state }, + ); +} diff --git a/webapp/packages/plugin-navigation-tree/src/TreeNew/useTreeVirtualization.ts b/webapp/packages/plugin-navigation-tree/src/TreeNew/useTreeVirtualization.ts new file mode 100644 index 0000000000..812855b803 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/TreeNew/useTreeVirtualization.ts @@ -0,0 +1,87 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observable, runInAction } from 'mobx'; +import { useEffect } from 'react'; + +import { useObjectRef } from '@cloudbeaver/core-blocks'; + +interface IPrivate extends ITreeVirtualization { + observer: ResizeObserver | null; + element: HTMLElement | null; + handleScroll(event: Event): void; + handleResize(): void; + dispose(): void; +} + +export interface ITreeVirtualization { + viewPort: { from: number; to: number }; + setRootRef(element: HTMLElement | null): void; +} + +export function useTreeVirtualization(): ITreeVirtualization { + const mountOptimization = useObjectRef( + () => ({ + observer: null, + element: null, + viewPort: observable({ from: 0, to: 0 }), + setRootRef(element: HTMLElement | null) { + if (this.element === element) { + return; + } + + if (this.element) { + this.dispose(); + } + + this.element = element; + if (element) { + this.observer = new ResizeObserver(this.handleResize); + element.addEventListener('scroll', this.handleScroll); + this.observer.observe(element); + + runInAction(() => { + this.viewPort.from = element.scrollTop; + this.viewPort.to = element.scrollTop + element.clientHeight; + }); + } + }, + dispose() { + if (this.element) { + this.element.removeEventListener('scroll', this.handleScroll); + } + if (this.observer) { + this.observer.disconnect(); + this.observer = null; + } + }, + handleScroll(event) { + runInAction(() => { + const target = event.target as HTMLElement; + + this.viewPort.from = target.scrollTop; + this.viewPort.to = target.scrollTop + target.clientHeight; + }); + }, + handleResize() { + runInAction(() => { + if (!this.element) { + return; + } + this.viewPort.from = this.element.scrollTop; + this.viewPort.to = this.element.scrollTop + this.element.clientHeight; + }); + }, + }), + false, + ['setRootRef', 'dispose', 'handleScroll', 'handleResize'], + ); + + useEffect(() => () => mountOptimization.dispose(), []); + + return mountOptimization; +} diff --git a/webapp/packages/plugin-navigation-tree/src/index.ts b/webapp/packages/plugin-navigation-tree/src/index.ts index 1a763288d1..6280a219e5 100644 --- a/webapp/packages/plugin-navigation-tree/src/index.ts +++ b/webapp/packages/plugin-navigation-tree/src/index.ts @@ -36,6 +36,9 @@ export { default as NavigationNodeControlRendererStyles } from './NavigationTree export { default as NavigationNodeControlStyles } from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.m.css'; export * from './NavigationTree/NavigationTreeLoader'; +export * from './TreeNew/TreeLazy'; +export * from './TreeNew/useTreeData'; +export * from './TreeNew/INode'; export * from './NavigationTree/getNavigationTreeUserSettingsId'; export * from './NodesManager/NavNodeView/IFolderTransform';