Merge remote-tracking branch 'origin/devel' into devel

This commit is contained in:
serge-rider
2021-05-13 22:28:42 +03:00
10 changed files with 242 additions and 14 deletions
@@ -22,7 +22,7 @@ import { elementsTreeNameFilter } from './elementsTreeNameFilter';
import { NavigationNodeNested } from './NavigationTreeNode/NavigationNode/NavigationNodeNested';
import { NavigationNodeElement } from './NavigationTreeNode/NavigationNodeElement';
import { ITreeContext, TreeContext } from './TreeContext';
import { IElementsTreeFilter, ITreeNodeState, useElementsTree } from './useElementsTree';
import { IElementsTreeCustomRenderer, IElementsTreeFilter, ITreeNodeState, useElementsTree } from './useElementsTree';
const styles = css`
tree {
@@ -48,6 +48,7 @@ interface Props {
emptyPlaceholder: React.FC;
className?: string;
filters?: IElementsTreeFilter[];
renderers?: IElementsTreeCustomRenderer[];
customSelect?: (node: NavNode, multiple: boolean) => void;
isGroup?: (node: NavNode) => boolean;
onExpand?: (node: NavNode, state: boolean) => Promise<void> | void;
@@ -63,6 +64,7 @@ export const ElementsTree: React.FC<Props> = observer(function ElementsTree({
selectionTree = false,
emptyPlaceholder,
filters,
renderers,
className,
isGroup,
customSelect,
@@ -83,6 +85,7 @@ export const ElementsTree: React.FC<Props> = observer(function ElementsTree({
root,
localState,
filters: [nameFilter, ...(filters || [])],
renderers,
isGroup,
onFilter,
customSelect,
@@ -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,11 +59,19 @@ const navigationTreeStyles = css`
export const NavigationTree = observer(function NavigationTree() {
const navTreeService = useService(NavigationTreeService);
const navNodeInfoResource = useService(NavNodeInfoResource);
const connectionInfoResource = useService(ConnectionInfoResource);
const [onFocus, onBlur] = useActiveView(navTreeService.getView);
const [ref] = useFocus<HTMLDivElement>({ onFocus, onBlur });
const isEnabled = usePermission(EPermission.public);
const { handleOpen, handleSelect } = useNavigationTree();
const connectionGroupFilter = useMemo(() => navigationTreeConnectionGroupFilter(
connectionInfoResource,
navNodeInfoResource
), [connectionInfoResource, navNodeInfoResource]);
if (!isEnabled) {
return null;
}
@@ -69,6 +81,8 @@ export const NavigationTree = observer(function NavigationTree() {
<ElementsTree
root={ROOT_NODE_PATH}
localState={navTreeService.treeState}
filters={[connectionGroupFilter]}
renderers={[navigationTreeConnectionGroupRenderer]}
emptyPlaceholder={() => styled(navigationTreeStyles)(
<center as="div">
<message as="div">
@@ -10,19 +10,12 @@ import { observer } from 'mobx-react-lite';
import { TreeNode } from '@cloudbeaver/core-blocks';
import type { NavNode } from '../../shared/NodesManager/EntityTypes';
import type { NavigationNodeComponent } from '../NavigationNodeComponent';
import { NavigationNodeControl } from './NavigationNode/NavigationNodeControl';
import { NavigationNodeNested } from './NavigationNode/NavigationNodeNested';
import { useNavigationNode } from './useNavigationNode';
interface Props {
node: NavNode;
component: React.FC<{
nodeId: string;
}>;
}
export const NavigationNode: React.FC<Props> = observer(function NavigationNode({
export const NavigationNode: NavigationNodeComponent = observer(function NavigationNode({
node,
component,
}) {
@@ -7,8 +7,12 @@
*/
import { observer } from 'mobx-react-lite';
import { useContext } from 'react';
import { useNode } from '../../shared/NodesManager/useNode';
import { useService } from '@cloudbeaver/core-di';
import { NavNodeInfoResource } from '../../shared/NodesManager/NavNodeInfoResource';
import { TreeContext } from '../TreeContext';
import { NavigationNode } from './NavigationNode';
interface NavigationTreeNodeProps {
@@ -18,7 +22,22 @@ interface NavigationTreeNodeProps {
export const NavigationNodeElement = observer(function NavigationNodeElement({
nodeId,
}: NavigationTreeNodeProps) {
const { node } = useNode(nodeId);
const context = useContext(TreeContext);
const navNodeInfoResource = useService(NavNodeInfoResource);
if (context?.tree.renderers) {
for (const renderer of context.tree.renderers) {
const CustomRenderer = renderer(nodeId);
if (CustomRenderer === undefined) {
continue;
}
return <CustomRenderer nodeId={nodeId} component={NavigationNodeElement} />;
}
}
const node = navNodeInfoResource.get(nodeId);
if (!node) {
return null;
@@ -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>);
};
@@ -6,6 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { observable } from 'mobx';
import { useState } from 'react';
import { useExecutor, useObjectRef } from '@cloudbeaver/core-blocks';
@@ -18,6 +19,15 @@ import { NavNodeInfoResource } from '../shared/NodesManager/NavNodeInfoResource'
import { NavTreeResource } from '../shared/NodesManager/NavTreeResource';
import { NavigationTreeService } from './NavigationTreeService';
export type ElementsTreeCustomRendererComponent = React.FC<{
nodeId: string;
component: React.FC<{
nodeId: string;
}>;
}>;
export type IElementsTreeCustomRenderer = (nodeId: string) => ElementsTreeCustomRendererComponent | undefined;
export type IElementsTreeFilter = (
node: NavNode,
children: string[],
@@ -34,6 +44,7 @@ interface IOptions {
root: string;
localState?: MetadataMap<string, ITreeNodeState>;
filters?: IElementsTreeFilter[];
renderers?: IElementsTreeCustomRenderer[];
customSelect?: (node: NavNode, multiple: boolean, nested: boolean) => void;
isGroup?: (node: NavNode) => boolean;
onExpand?: (node: NavNode, state: boolean) => void;
@@ -42,6 +53,8 @@ interface IOptions {
}
export interface IElementsTree {
root: string;
renderers: IElementsTreeCustomRenderer[];
state: MetadataMap<string, ITreeNodeState>;
getNodeState: (nodeId: string) => ITreeNodeState;
getNodeChildren: (nodeId: string) => string[];
@@ -140,6 +153,8 @@ export function useElementsTree(options: IOptions): IElementsTree {
}
const elementsTree = useObjectRef<IElementsTree>({
root: options.root,
renderers: options.renderers || [],
state,
getNodeState(nodeId: string) {
return this.state.get(nodeId);
@@ -175,7 +190,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
setSelection(node.id, !treeNodeState.selected);
},
});
}, undefined, { root: observable.ref, renderers: observable.ref });
async function refreshNode(nodeId: string) {
let children = [nodeId];
@@ -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', 'Отменить'],