mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-10-16 06:40:05 +08:00
Merge remote-tracking branch 'origin/devel' into devel
This commit is contained in:
@@ -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,
|
||||
}) {
|
||||
|
||||
+21
-2
@@ -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', 'Отменить'],
|
||||
|
||||
Reference in New Issue
Block a user