Merge branch 'devel' into feat/context-sort

This commit is contained in:
Naumov Alexey
2021-05-18 14:28:24 +03:00
54 changed files with 969 additions and 441 deletions
@@ -118,6 +118,15 @@ public class WebNavigatorNodeInfo {
if (node instanceof DBNContainer) {
features.add("container");
}
boolean isShared = false;
if (node instanceof DBNDatabaseNode) {
isShared = !((DBNDatabaseNode) node).getDataSourceContainer().isManageable();
} else if (node instanceof DBNLocalFolder) {
//isShared = ((DBNLocalFolder) node).getDataSourceRegistry().is
}
if (isShared) {
features.add("shared");
}
return features.toArray(new String[0]);
}
@@ -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");
}
@@ -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 { IElementsTreeCustomRenderer, 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,66 @@ 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> | void;
onOpen?: (node: NavNode) => Promise<void> | 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<Props> = observer(function ElementsTree({
root,
root = ROOT_NODE_PATH,
control,
localState,
selectionTree = false,
emptyPlaceholder,
filters,
renderers,
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<string, ITreeNodeState>(() => ({ 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 || [])],
renderers,
isGroup,
onFilter,
customSelect,
onExpand,
onSelect,
});
const context = useMemo<ITreeContext>(
() => ({
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)(
<center as="div">
<center>
<Loader />
</center>
);
@@ -112,10 +117,8 @@ export const ElementsTree: React.FC<Props> = observer(function ElementsTree({
return styled(styles)(
<TreeContext.Provider value={context}>
<tree as="div" className={className}>
{nodeChildren.children.map(id => (
<NavigationNodeElement key={id} nodeId={id} />
))}
<tree className={className}>
<NavigationNodeNested nodeId={root} component={NavigationNodeElement} root />
<Loader loading={nodeChildren.isLoading()} overlay />
</tree>
</TreeContext.Provider>
@@ -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<Props>;
@@ -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,10 +59,18 @@ 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<HTMLDivElement>({ onFocus, onBlur });
const isEnabled = usePermission(EPermission.public);
const { isSelected, handleOpen, handleSelect } = useNavigationTree();
const { handleOpen, handleSelect } = useNavigationTree();
const connectionGroupFilter = useMemo(() => navigationTreeConnectionGroupFilter(
connectionInfoResource,
navNodeInfoResource
), [connectionInfoResource, navNodeInfoResource]);
if (!isEnabled) {
return null;
@@ -68,6 +80,9 @@ export const NavigationTree = observer(function NavigationTree() {
<inside-box ref={ref} as='div' tabIndex={0}>
<ElementsTree
root={ROOT_NODE_PATH}
localState={navTreeService.treeState}
filters={[connectionGroupFilter]}
renderers={[navigationTreeConnectionGroupRenderer]}
emptyPlaceholder={() => styled(navigationTreeStyles)(
<center as="div">
<message as="div">
@@ -76,9 +91,8 @@ export const NavigationTree = observer(function NavigationTree() {
</message>
</center>
)}
isSelected={isSelected}
customSelect={handleSelect}
onOpen={handleOpen}
onSelect={handleSelect}
/>
</inside-box>
);
@@ -7,28 +7,18 @@
*/
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 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<Props> = observer(function NavigationNode({
export const NavigationNode: NavigationNodeComponent = observer(function NavigationNode({
node,
component,
}) {
const context = useContext(TreeNodeContext);
const {
control,
selected,
@@ -57,7 +47,7 @@ export const NavigationNode: React.FC<Props> = observer(function NavigationNode(
onFilter={handleFilter}
>
<Control node={node} />
<NavigationNodeNested nodeId={node.id} component={component} />
{expanded && <NavigationNodeNested nodeId={node.id} component={component} />}
</TreeNode>
);
});
@@ -79,10 +79,10 @@ export const NavigationNodeControl: React.FC<Props> = observer(function Navigati
<TreeNodeControl onClick={onClickHandler}>
<TreeNodeExpand />
<TreeNodeIcon icon={node.icon}>
{connected && <status as='div' />}
{connected && <status />}
</TreeNodeIcon>
<TreeNodeName>{node.name}</TreeNodeName>
<portal as="div">
<portal>
<TreeNodeMenu node={node} selected={context?.selected} />
</portal>
</TreeNodeControl>
@@ -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<T>(val: T | undefined | null): val is T {
return val !== undefined && val !== null;
root?: boolean;
}
export const NavigationNodeNested: React.FC<Props> = 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 => <NavigationNode key={child} nodeId={child} />)}
</>
);
}
return styled(styles)(
<TreeNodeNested>
{children.map(child => <NavigationNode key={child.id} nodeId={child.id} />)}
{children.map(child => <NavigationNode key={child} nodeId={child} />)}
</TreeNodeNested>
);
});
@@ -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,11 +22,27 @@ 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 <CustomRenderer nodeId={nodeId} component={NavigationNodeElement} />;
}
}
const node = navNodeInfoResource.get(nodeId);
if (!node) {
return null;
}
// TODO: after node update reference can be lost and NavigationNode skip update
return <NavigationNode node={node} component={NavigationNodeElement} />;
});
@@ -12,7 +12,7 @@ export const treeNodeMenuStyles = css`
MenuTrigger {
margin-left: 16px;
padding: 0;
height: 20px;
height: 16px;
&:before {
display: none;
@@ -6,15 +6,14 @@
* you may not use this file except in compliance with the License.
*/
import { useContext, useEffect, useState } from 'react';
import { useContext, useEffect } from 'react';
import { useService } from '@cloudbeaver/core-di';
import { useObjectRef } from '@cloudbeaver/core-blocks';
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 {
@@ -25,98 +24,62 @@ interface INavigationNode {
loading: boolean;
expanded: boolean;
leaf: boolean;
handleExpand: () => void;
handleOpen: () => void;
handleSelect: (isMultiple?: boolean) => void;
handleFilter: (value: string) => void;
handleExpand: () => Promise<void>;
handleOpen: () => Promise<void>;
handleSelect: (isMultiple?: boolean, nested?: boolean) => Promise<void>;
handleFilter: (value: string) => Promise<void>;
filterValue: string;
}
export function useNavigationNode(node: NavNode): INavigationNode {
const context = useContext(TreeContext);
const navigationTreeService = useService(NavigationTreeService);
const [processing, setProcessing] = useState(false);
const { isLoading, isOutdated } = useNode(node.id);
const children = useChildren(node.id);
const loading = isLoading() || children.isLoading() || processing;
export function useNavigationNode({ id }: NavNode): INavigationNode {
const contextRef = useObjectRef({
context: useContext(TreeContext),
});
const { node, isLoading } = useNode(id);
const isExpanded = navigationTreeService.isNodeExpanded(node.id);
let leaf = isLeaf(node) || (children.children?.length === 0 && !children.isOutdated());
// 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();
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;
}
const handleExpand = async () => {
if (!expanded) {
const timeout = setTimeout(() => setProcessing(true), 1);
const state = await navigationTreeService.loadNestedNodes(node.id);
clearTimeout(timeout);
setProcessing(false);
if (!state) {
navigationTreeService.expandNode(node.id, false);
return;
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) {
const state = contextRef.context?.tree.getNodeState(node.id);
if (state?.selected) {
contextRef.context?.tree.select(node, true, false);
}
}
navigationTreeService.expandNode(node.id, !expanded);
};
const handleOpen = async () => {
setProcessing(true);
try {
await context?.onOpen?.(node);
} finally {
setProcessing(false);
}
};
const handleSelect = (multiple = false) => {
context?.onSelect?.(node, multiple);
};
const handleFilter = (value: string) => {
context?.onFilter?.(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);
}
}, [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 +87,7 @@ export function useNavigationNode(node: NavNode): INavigationNode {
handleOpen,
handleSelect,
handleFilter,
filterValue: context?.treeNodesState?.get(node.id).filter || '',
filterValue: state?.filter || '',
};
}
@@ -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<string, INavigationNodeMetadata>;
readonly treeState: MetadataMap<string, ITreeNodeState>;
readonly nodeSelectionTask: IExecutor<INavigationNodeSelectionData>;
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<NavigationTreeService, 'unselectAll'>(this, {
unselectAll: action,
});
this.treeState = new MetadataMap(() => ({
filter: '',
expanded: false,
selected: false,
}));
this.nodeSelectionTask = new Executor();
this.navigationTreeMetadata = new MetadataMap<string, INavigationNodeMetadata>(() => ({
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<string> | 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,
@@ -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<string, ITreeNodeState>;
getTreeNodeState?: (node: NavNode) => ITreeNodeState;
tree: IElementsTree;
selectionTree: boolean;
control?: React.FC<{
node: NavNode;
}>;
onOpen?: (node: NavNode) => Promise<void> | void;
onSelect?: (node: NavNode, multiple: boolean) => void;
isSelected?: (node: NavNode) => boolean;
onFilter?: (node: NavNode, value: string) => void;
}
export const TreeContext = createContext<ITreeContext | null>(null);
@@ -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<T>(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<string, ITreeNodeState>) => {
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);
};
}
@@ -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<T>(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;
};
}
@@ -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))(<connection-group><Translate token='app_navigationTree_connection_group_user' /></connection-group>);
};
const UnManageableGroup: ElementsTreeCustomRendererComponent = function UnManageableGroup() {
return styled(useStyles(styles))(<connection-group><Translate token='app_navigationTree_connection_group_shared' /></connection-group>);
};
@@ -0,0 +1,261 @@
/*
* 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 { observable } from 'mobx';
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 ElementsTreeCustomRendererComponent = React.FC<{
nodeId: string;
component: React.FC<{
nodeId: string;
}>;
}>;
export type IElementsTreeCustomRenderer = (nodeId: string) => ElementsTreeCustomRendererComponent | undefined;
export type IElementsTreeFilter = (
node: NavNode,
children: string[],
state: MetadataMap<string, ITreeNodeState>
) => string[];
export interface ITreeNodeState {
filter: string;
selected: boolean;
expanded: boolean;
}
interface IOptions {
root: string;
localState?: MetadataMap<string, ITreeNodeState>;
filters?: IElementsTreeFilter[];
renderers?: IElementsTreeCustomRenderer[];
customSelect?: (node: NavNode, multiple: boolean, nested: boolean) => Promise<void> | void;
isGroup?: (node: NavNode) => boolean;
onExpand?: (node: NavNode, state: boolean) => Promise<void> | void;
onSelect?: (node: NavNode, state: boolean) => Promise<void> | void;
onFilter?: (node: NavNode, value: string) => Promise<void> | void;
}
export interface IElementsTree {
root: string;
renderers: IElementsTreeCustomRenderer[];
state: MetadataMap<string, ITreeNodeState>;
getNodeState: (nodeId: string) => ITreeNodeState;
getNodeChildren: (nodeId: string) => string[];
filter: (node: NavNode, value: string) => Promise<void>;
select: (node: NavNode, multiple: boolean, nested: boolean) => Promise<void>;
expand: (node: NavNode, state: boolean) => Promise<void>;
}
export function useElementsTree(options: IOptions): IElementsTree {
const navNodeInfoResource = useService(NavNodeInfoResource);
const navTreeService = useService(NavigationTreeService);
const navTreeResource = useService(NavTreeResource);
const [localTreeNodesState] = useState(() => new MetadataMap<string, ITreeNodeState>(() => ({
filter: '',
selected: false,
expanded: false,
})));
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) {
await elementsTree.expand(node, false);
}
continue;
}
nextChildren.push(...(navTreeResource.get(child) || []));
}
children = nextChildren;
}
}
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;
}
async 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) {
await options.onSelect(node, false);
}
}
}
}
}
async function setSelection(nodeId: string, selected: boolean): Promise<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) {
await setSelection(child, selected);
}
if (children.length === 0) {
return;
}
}
treeNodeState.selected = selected;
await options.onSelect?.(node, selected);
}
const elementsTree = useObjectRef<IElementsTree>({
root: options.root,
renderers: options.renderers || [],
state,
getNodeState(nodeId: string) {
return this.state.get(nodeId);
},
getNodeChildren,
async filter(node: NavNode, value: string) {
const treeNodeState = this.state.get(node.id);
treeNodeState.filter = value;
await options.onFilter?.(node, value);
},
async expand(node: NavNode, state: boolean) {
const treeNodeState = this.state.get(node.id);
treeNodeState.expanded = state;
if (state) {
await loadTree(node.id);
}
await options.onExpand?.(node, state);
},
async select(node: NavNode, multiple: boolean, nested: boolean) {
if (options.customSelect) {
await options.customSelect(node, multiple, nested);
return;
}
const treeNodeState = this.state.get(node.id);
if (!multiple) {
await clearSelection(node.id);
if (treeNodeState.selected) {
return;
}
}
await setSelection(node.id, !treeNodeState.selected);
},
}, undefined, { root: observable.ref, renderers: observable.ref });
useExecutor({
executor: navTreeResource.onNodeRefresh,
handlers: [loadTree],
});
useExecutor({
executor: navNodeInfoResource.onItemDelete,
handlers: [function deleteNodeState(key) {
ResourceKeyUtils.forEach(key, key => {
state.delete(key);
});
}],
});
useExecutor({
executor: navTreeResource.onItemDelete,
handlers: [async function collapseDeletedTree(key) {
await ResourceKeyUtils.forEachAsync(key, async key => {
const node = navNodeInfoResource.get(key);
if (node) {
await elementsTree.expand(node, false);
}
});
}],
});
return elementsTree;
}
@@ -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<void>;
handleSelect: (node: NavNode, isMultiple: boolean) => void;
handleSelect: (node: NavNode, state: boolean) => void;
}
const bindActions: Array<keyof INavigationTree> = ['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<INavigationTree>({
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);
}
+1
View File
@@ -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
@@ -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'],
@@ -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', 'Отменить'],
@@ -60,7 +60,7 @@ export class NavNodeInfoResource extends CachedMapResource<string, NavNode> {
});
}
protected async loader(key: ResourceKey<string>) {
protected async loader(key: ResourceKey<string>): Promise<Map<string, NavNode>> {
if (isResourceKeyList(key)) {
const values: NavNode[] = [];
for (const nodePath of key.list) {
@@ -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';
@@ -16,7 +18,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 +94,6 @@ export class NavNodeManagerService extends Bootstrap {
readonly navigator: IExecutor<INodeNavigationData>;
constructor(
private graphQLService: GraphQLService,
private permissionsService: PermissionsService,
readonly connectionInfo: ConnectionInfoResource,
readonly navTree: NavTreeResource,
@@ -116,8 +117,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 +134,7 @@ export class NavNodeManagerService extends Bootstrap {
}
async refreshTree(navNodeId: string): Promise<void> {
await this.graphQLService.sdk.navRefreshNode({
nodePath: navNodeId,
});
this.markTreeOutdated(navNodeId);
await this.navTree.refresh(navNodeId);
}
markTreeOutdated(navNodeId: ResourceKey<string>): void {
this.navTree.markOutdated(resourceKeyList(this.navTree.getNestedChildren(navNodeId)));
await this.navTree.refreshTree(navNodeId);
}
getTree(navNodeId: string): string[] | undefined
@@ -151,14 +142,14 @@ export class NavNodeManagerService extends Bootstrap {
getTree(navNodeKey: NavNodeKey[]): Array<string[] | undefined>
getTree(navNodeId: string | NavNodeKey | NavNodeKey[]): string[] | undefined | Array<string[] | undefined> {
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<string[]> {
@@ -169,10 +160,6 @@ export class NavNodeManagerService extends Bootstrap {
this.navTree.delete(path);
}
async refreshNode(navNodeId: string): Promise<void> {
await this.navNodeInfoResource.refresh(navNodeId);
}
getNode(navNodeId: string): NavNode | undefined
getNode(navNodeKey: NavNodeKey): NavNode | undefined
getNode(navNodeKey: NavNodeKey[]): Array<NavNode | undefined>
@@ -329,22 +316,6 @@ export class NavNodeManagerService extends Bootstrap {
};
};
async updateRoot(): Promise<void> {
const enabled = await this.isNavTreeEnabled();
if (enabled) {
await this.navTree.refresh(ROOT_NODE_PATH);
}
}
async refreshRoot(): Promise<void> {
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 +323,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 +332,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<string>) {
const enabled = await this.isNavTreeEnabled();
if (!enabled) {
@@ -380,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.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<string>) {
ResourceKeyUtils.forEach(key, key => {
// deleteConnection
const navNodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key);
private connectionRemoveHandler(key: ResourceKey<string>) {
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]);
});
});
}
@@ -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<string, string[]> {
readonly onNodeRefresh: IExecutor<string>;
protected metadata: MetadataMap<string, INodeMetadata>;
get childrenLimit(): number {
@@ -66,10 +68,25 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
exception: null,
includes: [],
}));
this.onNodeRefresh = new Executor<string>(null, (a, b) => a === b);
this.onDataOutdated.addHandler(navNodeInfoResource.markOutdated.bind(navNodeInfoResource));
this.sessionDataResource.onDataUpdate.addPostHandler(() => this.markOutdated());
}
async refreshTree(navNodeId: string): Promise<void> {
await this.graphQLService.sdk.navRefreshNode({
nodePath: navNodeId,
});
this.markTreeOutdated(navNodeId);
await this.refresh(navNodeId);
await this.onNodeRefresh.execute(navNodeId);
}
markTreeOutdated(navNodeId: ResourceKey<string>): void {
this.markOutdated(resourceKeyList(this.getNestedChildren(navNodeId)));
}
setDetails(keyObject: ResourceKey<string>, state: boolean): void {
ResourceKeyUtils.forEach(keyObject, key => {
const children = resourceKeyList(this.getNestedChildren(key));
@@ -253,6 +270,8 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
withDetails: metadata.withDetails,
});
navNodeInfo.hasChildren = navNodeInfo.hasChildren && navNodeChildren.length > 0;
return { navNodeChildren: navNodeChildren.slice(0, this.childrenLimit), navNodeInfo, parentPath };
}
}
@@ -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<keyof IUseNodeHook> = ['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<IUseNodeHook>({
navNodeId,
node,
isLoading() {
return navNodeInfoResource.isDataLoading(this.navNodeId);
},
isLoaded() {
return navNodeInfoResource.isLoaded(this.navNodeId);
},
isOutdated() {
return navNodeInfoResource.isOutdated(this.navNodeId);
},
}, {
navNodeId,
node,
}, undefined, bindActions);
}
@@ -8,12 +8,11 @@
import { forwardRef } from 'react';
import { filterContainerFakeProps } from './filterContainerFakeProps';
import type { IContainerProps } from './IContainerProps';
export const ColoredContainer = forwardRef<HTMLDivElement, IContainerProps>(function ColoredContainer({ children, className }, ref) {
return (
<div ref={ref} className={className}>
{children}
</div>
);
export const ColoredContainer = forwardRef<HTMLDivElement, IContainerProps & React.HTMLAttributes<HTMLDivElement>>(function ColoredContainer(props, ref) {
const divProps = filterContainerFakeProps(props);
return <div ref={ref} {...divProps} />;
});
@@ -8,12 +8,11 @@
import { forwardRef } from 'react';
import { filterContainerFakeProps } from './filterContainerFakeProps';
import type { IContainerProps } from './IContainerProps';
export const Container = forwardRef<HTMLDivElement, IContainerProps>(function Container({ children, className }, ref) {
return (
<div ref={ref} className={className}>
{children}
</div>
);
export const Container = forwardRef<HTMLDivElement, IContainerProps & React.HTMLAttributes<HTMLDivElement>>(function Container(props, ref) {
const divProps = filterContainerFakeProps(props);
return <div ref={ref} {...divProps} />;
});
@@ -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<HTMLDivElement, Props>(function Group({ children, className }, ref) {
return (
<div ref={ref} className={className}>
{children}
</div>
);
export const Group = forwardRef<HTMLDivElement, Props & React.HTMLAttributes<HTMLDivElement>>(function Group({
form,
center,
box,
...rest
}, ref) {
const divProps = filterContainerFakeProps(rest);
return <div ref={ref} {...divProps} />;
});
@@ -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<Props> = function GroupItem({ children, className }) {
return (
<div className={className}>
{children}
</div>
);
export const GroupItem: React.FC<ILayoutSizeProps & React.HTMLAttributes<HTMLDivElement>> = function GroupItem(props) {
const divProps = filterLayoutFakeProps(props);
return <div {...divProps} />;
};
@@ -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<Props> = function GroupTitle({ children, className }) {
return (
<h2 className={className}>
{children}
</h2>
);
export const GroupTitle: React.FC<ILayoutSizeProps & React.HTMLAttributes<HTMLHeadingElement>> = function GroupTitle(props) {
const divProps = filterLayoutFakeProps(props);
return <h2 {...divProps} />;
};
@@ -8,7 +8,7 @@
import type { ILayoutSizeProps } from './ILayoutSizeProps';
export interface IContainerProps extends ILayoutSizeProps, React.HTMLAttributes<HTMLDivElement> {
export interface IContainerProps extends ILayoutSizeProps {
baseline?: boolean;
center?: boolean;
vertical?: boolean;
@@ -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<T extends IContainerProps>(props: T): Omit<T, keyof IContainerProps> {
const {
baseline,
center,
vertical,
wrap,
overflow,
parent,
gap,
...rest
} = filterLayoutFakeProps(props);
return rest as Omit<T, keyof IContainerProps>;
}
@@ -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<T extends ILayoutSizeProps>(props: T): Omit<T, keyof ILayoutSizeProps> {
const {
keepSize,
tiny,
small,
medium,
large,
fill,
...rest
} = props;
return rest;
}
@@ -48,7 +48,7 @@ type ControlledProps = BaseProps & {
type ObjectProps<TKey extends keyof TState, TState> = BaseProps & {
name: TKey;
state: TState;
mapState?: (value: TState[TKey]) => TState[TKey];
mapState?: (value: TState[TKey]) => TState[TKey] | string | number;
mapValue?: (value: TState[TKey]) => TState[TKey];
onChange?: (value: TState[TKey], name: TKey) => any;
autoHide?: boolean;
@@ -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';
@@ -20,11 +22,11 @@ interface Props {
expanded?: boolean;
leaf?: boolean;
className?: string;
onExpand?: () => void;
onSelect?: (multiple?: boolean) => void;
onFilter?: (value: string) => void;
onExpand?: () => Promise<void> | void;
onSelect?: (multiple?: boolean, nested?: boolean) => Promise<void> | void;
onFilter?: (value: string) => Promise<void> | void;
filterValue?: string;
onOpen?: () => void;
onOpen?: () => Promise<void> | void;
}
export const TreeNode: React.FC<Props> = memo(function TreeNode({
@@ -37,41 +39,65 @@ export const TreeNode: React.FC<Props> = memo(function TreeNode({
children,
...handlers
}) {
const handlersRef = useRef(handlers);
handlersRef.current = handlers;
const handlersRef = useObjectRef(handlers);
const handleExpand = useCallback(() => {
handlersRef.current.onExpand?.();
}, []);
async function processAction(action: () => Promise<void>) {
const timeout = setTimeout(() => {
nodeContext.processing = true;
}, 250);
const handleSelect = useCallback(
(multiple?: boolean): void => handlersRef.current.onSelect?.(multiple),
[]
);
try {
await action();
} finally {
clearTimeout(timeout);
nodeContext.processing = false;
}
}
const handleFilter = useCallback(
(value: string): void => handlersRef.current.onFilter?.(value),
[]
);
const handleOpen = useCallback(() => {
handlersRef.current.onOpen?.();
}, []);
const nodeContext = useMemo<ITreeNodeContext>(() => ({
const nodeContext = useObjectRef<ITreeNodeContext>({
processing: false,
loading,
selected,
expanded,
leaf,
filterValue,
expand: handleExpand,
select: handleSelect,
filter: handleFilter,
open: handleOpen,
}), [loading, selected, expanded, leaf, filterValue, handleExpand, handleSelect, handleOpen, handleFilter]);
async expand() {
await processAction(async () => {
await handlersRef.onExpand?.();
});
},
async select(multiple?: boolean, nested?: boolean) {
await processAction(async () => {
await handlersRef.onSelect?.(multiple, nested);
});
},
async filter(value: string) {
await processAction(async () => {
await handlersRef.onFilter?.(value);
});
},
async open() {
await processAction(async () => {
await handlersRef.onOpen?.();
});
},
}, {
loading,
selected,
expanded,
leaf,
filterValue,
}, {
processing: observable.ref,
loading: observable.ref,
selected: observable.ref,
expanded: observable.ref,
leaf: observable.ref,
filterValue: observable.ref,
});
return styled(useStyles(TREE_NODE_STYLES))(
<node as="div" {...use({ expanded })} className={className}>
<node {...use({ expanded })} className={className}>
<TreeNodeContext.Provider value={nodeContext}>
{children}
</TreeNodeContext.Provider>
@@ -9,15 +9,16 @@
import { createContext } from 'react';
export interface ITreeNodeContext {
processing: boolean;
expanded: boolean;
loading: boolean;
selected: boolean;
leaf: boolean;
select: (multiple?: boolean) => void;
filter: (value: string) => void;
select: (multiple?: boolean, nested?: boolean) => Promise<void>;
filter: (value: string) => Promise<void>;
filterValue: string;
expand: () => void;
open: () => void;
expand: () => Promise<void>;
open: () => Promise<void>;
}
export const TreeNodeContext = createContext<ITreeNodeContext | null>(null);
@@ -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<Props> = function TreeNodeControl({
export const TreeNodeControl: React.FC<Props> = observer(function TreeNodeControl({
onClick,
className,
children,
@@ -61,4 +62,4 @@ export const TreeNodeControl: React.FC<Props> = function TreeNodeControl({
{children}
</div>
);
};
});
@@ -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<Props> = function TreeNodeExpand({
export const TreeNodeExpand: React.FC<Props> = observer(function TreeNodeExpand({
leaf,
big,
className,
@@ -51,11 +52,13 @@ export const TreeNodeExpand: React.FC<Props> = function TreeNodeExpand({
event.preventDefault();
};
const loading = context.loading || context.processing;
return styled(styles)(
<arrow as="div" className={className} onClick={handleExpand} onDoubleClick={preventDoubleClick}>
{context.loading && <Loader small fullSize />}
{!context.loading && !context.leaf && !leaf && big && <Icon name="angle" viewBox="0 0 15 8" />}
{!context.loading && !context.leaf && !leaf && !big && <Icon name="arrow" viewBox="0 0 16 16" />}
<arrow className={className} onClick={handleExpand} onDoubleClick={preventDoubleClick}>
{loading && <Loader small fullSize />}
{!loading && !context.leaf && !leaf && big && <Icon name="angle" viewBox="0 0 15 8" />}
{!loading && !context.leaf && !leaf && !big && <Icon name="arrow" viewBox="0 0 16 16" />}
</arrow>
);
};
});
@@ -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<Props> = function TreeNodeFilter({
export const TreeNodeFilter: React.FC<Props> = observer(function TreeNodeFilter({
disabled,
className,
}) {
@@ -99,10 +100,10 @@ export const TreeNodeFilter: React.FC<Props> = function TreeNodeFilter({
}
}, [filterEnabled]);
useEffect(() => () => context.filter(''), []);
useEffect(() => () => { context.filter(''); }, []);
return styled(useStyles(styles))(
<filter-container as='div' className={className} onClick={handleClick} onDoubleClick={preventPropagation}>
<filter-container className={className} onClick={handleClick} onDoubleClick={preventPropagation}>
<InputFieldNew
ref={inputRef}
style={innerInputStyle}
@@ -115,4 +116,4 @@ export const TreeNodeFilter: React.FC<Props> = function TreeNodeFilter({
<IconButton name='search' disabled={disabled} onClick={onFilterEnabledChange} {...use({ filterEnabled })} />
</filter-container>
);
};
});
@@ -27,7 +27,7 @@ export const TreeNodeIcon: React.FC<Props> = function TreeNodeIcon({
children,
}) {
return styled(styles)(
<icon as="div" className={className}>
<icon className={className}>
<StaticImage icon={icon} />
{children}
</icon>
@@ -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<Props> = function TreeNodeSelect({
export const TreeNodeSelect: React.FC<Props> = observer(function TreeNodeSelect({
onSelect,
group,
selected,
disabled,
className,
}) {
@@ -45,9 +42,13 @@ export const TreeNodeSelect: React.FC<Props> = function TreeNodeSelect({
event.preventDefault();
};
return styled(styles)(
return (
<div className={className} onClick={handleClick} onDoubleClick={preventPropagation}>
<Checkbox checked={context.selected} disabled={disabled} onChange={() => context.select(true)} />
<Checkbox
checked={selected ?? context.selected}
disabled={disabled}
onChange={onSelect ?? (() => context.select(true, group))}
/>
</div>
);
};
});
@@ -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<string, Connection
...this.getIncludesMap(),
});
runInAction(() => {
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 {
@@ -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;
@@ -44,7 +44,7 @@ const styles = composes(
css`
CommonDialogWrapper {
min-height: 400px;
min-width: 600px;
min-width: 500px;
}
SubmittingForm {
overflow: auto;
@@ -52,22 +52,24 @@ export const ConfirmationDialog: DialogComponent<ConfirmationDialogPayload> = fu
return styled(useStyles(commonDialogThemeStyle, commonDialogBaseStyle, style))(
<dialog className={className}>
<header>
<header-title as="div">
<icon-container>
{icon && <IconOrImage {...use({ bigIcon })} icon={icon} viewBox={viewBox} />}
<header-title-text as='div'>
<h3><Translate token={title} /></h3>
<sub-title as='div'>{subTitle}</sub-title>
</header-title-text>
<reject as="div">
<Icon name="cross" viewBox="0 0 16 16" onClick={rejectDialog} />
</reject>
</icon-container>
<header-title>
<h3><Translate token={title} /></h3>
{rejectDialog && (
<reject>
<Icon name="cross" viewBox="0 0 16 16" onClick={rejectDialog} />
</reject>
)}
</header-title>
{subTitle && <sub-title>{subTitle}</sub-title>}
</header>
<dialog-body as="div">
<dialog-body-content as='div'>
<dialog-body>
<dialog-body-content>
<Translate token={message} />
</dialog-body-content>
<dialog-body-overflow as='div' />
<dialog-body-overflow />
</dialog-body>
<footer>
<Button
@@ -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,
});
@@ -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);
}
@@ -32,7 +32,7 @@ const styles = composes(
css`
CommonDialogWrapper {
min-height: 400px;
min-width: 600px;
min-width: 500px;
}
SubmittingForm {
overflow: auto;
@@ -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;
@@ -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),
}
);
@@ -29,7 +29,6 @@ function getClasses(rawValue: any) {
export const CellFormatter: React.FC<FormatterProps> = observer(function CellFormatter({ rowIdx, row, column, isCellSelected }) {
const editorRef = useRef<IEditorRef>(null);
const cellRef = useRef<HTMLDivElement>(null);
const cellContext = useContext(CellContext);
const context = useContext(DataGridContext);
const editingContext = useContext(EditingContext);
@@ -66,7 +65,7 @@ export const CellFormatter: React.FC<FormatterProps> = observer(function CellFor
return styled()(
<>
<cell-formatter ref={cellRef} title={value} className={`cell-formatter ${classes}`}>
<cell-formatter title={value} className={`cell-formatter ${classes}`}>
{value}
</cell-formatter>
{(isCellSelected || cellContext?.mouse.state.mouseEnter) && context && (
@@ -50,11 +50,17 @@ implements IDatabaseDataActions<TOptions, TResult> {
}
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);
}
}
}
}
@@ -118,6 +118,7 @@ implements IDatabaseDataSource<TOptions, TResult> {
setResults(results: TResult[]): this {
this.editor?.cancelChanges();
this.actions.updateResults(results);
this.results = results;
return this;
}
@@ -194,8 +195,7 @@ implements IDatabaseDataSource<TOptions, TResult> {
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<TOptions, TResult> {
if (promise instanceof Promise) {
this.activeSave = promise;
}
this.results = await promise;
this.setResults(await promise);
} finally {
this.activeSave = null;
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@cloudbeaver/product-default",
"sideEffects": false,
"version": "21.0.1",
"version": "21.0.2",
"description": "",
"license": "Apache-2.0",
"main": "lib/index.js",