From 315401fd587deb4e0548359377577d0abc28b521 Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 3 Mar 2021 18:37:08 +0300 Subject: [PATCH 01/11] feat: CB-668 folder children selection for navigation tree --- .../src/NavigationTree/ElementsTree.tsx | 9 +- .../NavigationTreeContextMenuService.ts | 4 +- .../NavigationNode/NavigationNodeNested.tsx | 9 +- .../NavigationTreeNode/useNavigationNode.ts | 14 ++- .../NavigationTree/NavigationTreeService.ts | 118 +++++++++++++----- .../src/NavigationTree/TreeContext.ts | 2 +- .../src/NavigationTree/useNavigationTree.ts | 2 +- .../NodesManager/NavNodeManagerService.ts | 18 ++- .../core-app/src/shared/useChildren.ts | 4 +- .../src/FormControls/Checkboxes/Checkbox.tsx | 10 +- .../src/Tree/TreeNode/TreeNode.tsx | 4 +- .../src/Tree/TreeNode/TreeNodeContext.ts | 2 +- .../src/Tree/TreeNode/TreeNodeExpand.tsx | 3 +- .../src/Tree/TreeNode/TreeNodeSelect.tsx | 4 +- .../src/AuthenticationService.ts | 3 +- 15 files changed, 136 insertions(+), 70 deletions(-) diff --git a/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx b/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx index 92d2cb6cea..8591e26aa2 100644 --- a/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx +++ b/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx @@ -6,15 +6,12 @@ * you may not use this file except in compliance with the License. */ -import { computed } from 'mobx'; import { observer } from 'mobx-react-lite'; import { useMemo } from 'react'; import styled, { css } from 'reshadow'; import { Loader } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { CoreSettingsService } from '../CoreSettingsService'; import type { NavNode } from '../shared/NodesManager/EntityTypes'; import { useChildren } from '../shared/useChildren'; import { NavigationNodeElement } from './NavigationTreeNode/NavigationNodeElement'; @@ -42,7 +39,7 @@ interface Props { emptyPlaceholder: React.FC; className?: string; onOpen?: (node: NavNode) => Promise | void; - onSelect?: (node: NavNode, multiple: boolean) => boolean; + onSelect?: (node: NavNode, multiple: boolean) => void; isSelected?: (node: NavNode) => boolean; } @@ -55,10 +52,8 @@ export const ElementsTree: React.FC = observer(function ElementsTree({ onSelect, isSelected, }) { - const config = useService(CoreSettingsService); const nodeChildren = useChildren(root); const Placeholder = emptyPlaceholder; - const limit = useMemo(() => computed(() => config.settings.getValue('app.navigationTree.childrenLimit')), [config]); const context = useMemo( () => ({ control, onOpen, onSelect, isSelected }), @@ -80,7 +75,7 @@ export const ElementsTree: React.FC = observer(function ElementsTree({ return styled(styles)( - {nodeChildren.children.slice(0, limit.get()).map(id => ( + {nodeChildren.children.map(id => ( ))} diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts index 6e82e7f876..1357a6ecd3 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts @@ -29,11 +29,11 @@ export class NavigationTreeContextMenuService { private connectionInfoResource: ConnectionInfoResource ) { } - getMenuToken() { + getMenuToken(): string { return NavigationTreeContextMenuService.menuToken; } - getNodeViewMenuItemToken() { + getNodeViewMenuItemToken(): string { return NavigationTreeContextMenuService.nodeViewMenuItemToken; } diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx index 3b469ca521..24ae84d5e9 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx @@ -6,16 +6,13 @@ * you may not use this file except in compliance with the License. */ -import { computed } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { useContext, useMemo } from 'react'; +import { useContext } from 'react'; import styled from 'reshadow'; import { TreeNodeContext, TreeNodeNested, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; import { useStyles } from '@cloudbeaver/core-theming'; -import { CoreSettingsService } from '../../../CoreSettingsService'; import { useChildren } from '../../../shared/useChildren'; interface Props { @@ -29,11 +26,9 @@ export const NavigationNodeNested: React.FC = observer(function Navigatio nodeId, component, }) { - const config = useService(CoreSettingsService); const styles = useStyles(TREE_NODE_STYLES); const context = useContext(TreeNodeContext); const children = useChildren(nodeId); - const limit = useMemo(() => computed(() => config.settings.getValue('app.navigationTree.childrenLimit')), [config]); if (!children.children || !context?.expanded) { return null; @@ -43,7 +38,7 @@ export const NavigationNodeNested: React.FC = observer(function Navigatio return styled(styles)( - {children.children.slice(0, limit.get()).map(child => )} + {children.children.map(child => )} ); }); diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts index 80d5e4bf48..ed0754f932 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts @@ -27,18 +27,18 @@ interface INavigationNode { leaf: boolean; handleExpand: () => void; handleOpen: () => void; - handleSelect: (isMultiple?: boolean) => boolean; + handleSelect: (isMultiple?: boolean) => void; } export function useNavigationNode(node: NavNode): INavigationNode { const context = useContext(TreeContext); const navigationTreeService = useService(NavigationTreeService); const [processing, setProcessing] = useState(false); - const [isExpanded, switchExpand] = useState(false); const { isLoading, isOutdated } = useNode(node.id); const children = useChildren(node.id); const loading = isLoading() || children.isLoading() || processing; + const isExpanded = navigationTreeService.isNodeExpanded(node.id); let leaf = isLeaf(node) || (children.children?.length === 0 && !children.isOutdated()); let expanded = isExpanded && !leaf; @@ -60,11 +60,11 @@ export function useNavigationNode(node: NavNode): INavigationNode { clearTimeout(timeout); setProcessing(false); if (!state) { - switchExpand(false); + navigationTreeService.expandNode(node.id, false); return; } } - switchExpand(!expanded); + navigationTreeService.expandNode(node.id, !expanded); }; const handleOpen = async () => { @@ -76,7 +76,9 @@ export function useNavigationNode(node: NavNode): INavigationNode { } }; - const handleSelect = (multiple = false) => context?.onSelect?.(node, multiple) || false; + const handleSelect = (multiple = false) => { + context?.onSelect?.(node, multiple); + }; // TODO: probably should be refactored useEffect(() => { @@ -87,7 +89,7 @@ export function useNavigationNode(node: NavNode): INavigationNode { .then(state => { setProcessing(false); if (!state) { - switchExpand(false); + navigationTreeService.expandNode(node.id, false); } }); } diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts index 3eeee5654f..75211ac378 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts @@ -6,43 +6,86 @@ * you may not use this file except in compliance with the License. */ -import { observable } from 'mobx'; -import { Subject, Observable } from 'rxjs'; +import { action, computed, makeObservable } from 'mobx'; 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 { CoreSettingsService } from '../CoreSettingsService'; import { EObjectFeature } from '../shared/NodesManager/EObjectFeature'; import { NavNodeExtensionsService } from '../shared/NodesManager/NavNodeExtensionsService'; -import { ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource'; +import { NavNodeInfoResource, 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'; +export interface INavigationNodeMetadata { + selected: boolean; + expanded: boolean; +} + +export interface INavigationNodeSelectionData { + id: string; + selected: boolean; +} + @injectable() export class NavigationTreeService { - readonly selectedNodes = observable.array([]); - readonly onNodeSelect: Observable<[string, boolean]>; + readonly navigationTreeMetadata: MetadataMap; + readonly nodeSelectionTask: IExecutor; - private nodeSelectSubject: Subject<[string, boolean]>; + get childrenLimit(): number { + return this.coreSettingsService.settings.getValue('app.navigationTree.childrenLimit'); + } constructor( private navNodeManagerService: NavNodeManagerService, private notificationService: NotificationService, private connectionAuthService: ConnectionAuthService, - private navNodeExtensionsService: NavNodeExtensionsService + private navNodeExtensionsService: NavNodeExtensionsService, + private navTreeResource: NavTreeResource, + private coreSettingsService: CoreSettingsService, + private navNodeInfoResource: NavNodeInfoResource ) { - this.nodeSelectSubject = new Subject(); - this.onNodeSelect = this.nodeSelectSubject.asObservable(); + makeObservable(this, { + childrenLimit: computed, + unselectAll: action, + }); + + this.nodeSelectionTask = new Executor(); + this.navigationTreeMetadata = new MetadataMap(() => ({ + selected: false, + expanded: false, + })); this.getView = this.getView.bind(this); + + this.navNodeInfoResource.onItemDelete.addHandler(key => { + ResourceKeyUtils.forEach(key, key => { + this.navigationTreeMetadata.delete(key); + }); + }); } - async navToNode(id: string, parentId: string) { + getChildren(id: string): string[] | undefined { + const children = this.navTreeResource.get(id); + + if (children) { + return children.slice(0, this.childrenLimit); + } + + return children; + } + + async navToNode(id: string, parentId: string): Promise { await this.navNodeManagerService.navToNode(id, parentId); } - async loadNestedNodes(id = ROOT_NODE_PATH) { + async loadNestedNodes(id = ROOT_NODE_PATH): Promise { try { if (this.isConnectionNode(id) && !await this.tryInitConnection(id)) { return false; @@ -55,39 +98,56 @@ export class NavigationTreeService { return false; } - selectNode(id: string, multiple?: boolean): boolean { + async selectNode(id: string, multiple?: boolean): Promise { if (!multiple) { - for (const id of this.selectedNodes) { - this.nodeSelectSubject.next([id, false]); - } - this.selectedNodes.clear(); + await this.unselectAll(); } - if (!this.isNodeSelected(id)) { - this.selectedNodes.push(id); - this.nodeSelectSubject.next([id, true]); - return true; - } - this.selectedNodes.remove(id); - this.nodeSelectSubject.next([id, false]); - return false; + + const metadata = this.navigationTreeMetadata.get(id); + metadata.selected = !metadata.selected; + + await this.nodeSelectionTask.execute({ + id, + selected: metadata.selected, + }); } - isNodeSelected(navNodeId: string) { - return this.selectedNodes.includes(navNodeId); + isNodeExpanded(navNodeId: string): boolean { + return this.navigationTreeMetadata.get(navNodeId).expanded; + } + + isNodeSelected(navNodeId: string): boolean { + return this.navigationTreeMetadata.get(navNodeId).selected; + } + + expandNode(navNodeId: string, state: boolean): void { + const metadata = this.navigationTreeMetadata.get(navNodeId); + metadata.expanded = state; } getView(): IActiveView | null { - if (!this.selectedNodes.length) { + const element = Array.from(this.navigationTreeMetadata).find(([key, metadata]) => metadata.selected); + + if (!element) { return null; } - const context = this.selectedNodes[0]; return { - context, + context: element[0], extensions: this.navNodeExtensionsService.extensions, }; } + private async unselectAll() { + for (const [id, metadata] of this.navigationTreeMetadata) { + metadata.selected = false; + await this.nodeSelectionTask.execute({ + id, + selected: false, + }); + } + } + private isConnectionNode(navNodeId: string) { const node = this.navNodeManagerService.getNode(navNodeId); return node?.objectFeatures.includes(EObjectFeature.dataSource); diff --git a/webapp/packages/core-app/src/NavigationTree/TreeContext.ts b/webapp/packages/core-app/src/NavigationTree/TreeContext.ts index 97e7e217c1..0abbdec834 100644 --- a/webapp/packages/core-app/src/NavigationTree/TreeContext.ts +++ b/webapp/packages/core-app/src/NavigationTree/TreeContext.ts @@ -15,7 +15,7 @@ export interface ITreeContext { node: NavNode; }>; onOpen?: (node: NavNode) => Promise | void; - onSelect?: (node: NavNode, multiple: boolean) => boolean; + onSelect?: (node: NavNode, multiple: boolean) => void; isSelected?: (node: NavNode) => boolean; } diff --git a/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts b/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts index 14d08e89e7..8883ec0a03 100644 --- a/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts +++ b/webapp/packages/core-app/src/NavigationTree/useNavigationTree.ts @@ -16,7 +16,7 @@ import { NavigationTreeService } from './NavigationTreeService'; interface INavigationTree { isSelected: (node: NavNode) => boolean; handleOpen: (node: NavNode) => Promise; - handleSelect: (node: NavNode, isMultiple: boolean) => boolean; + handleSelect: (node: NavNode, isMultiple: boolean) => void; } export function useNavigationTree(): INavigationTree { diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts index 082eb22bc3..452bd334d6 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts @@ -332,20 +332,23 @@ export class NavNodeManagerService extends Bootstrap { }; async updateRoot(): Promise { - if (await this.isNavTreeEnabled()) { + const enabled = await this.isNavTreeEnabled(); + if (enabled) { await this.navTree.refresh(ROOT_NODE_PATH); } } async refreshRoot(): Promise { this.navTree.delete(ROOT_NODE_PATH); - if (await this.isNavTreeEnabled()) { + const enabled = await this.isNavTreeEnabled(); + if (enabled) { await this.navTree.refresh(ROOT_NODE_PATH); } } private async connectionCreateHandler(connection: Connection) { - if (!await this.isNavTreeEnabled()) { + const enabled = await this.isNavTreeEnabled(); + if (!enabled) { return; } @@ -360,7 +363,8 @@ export class NavNodeManagerService extends Bootstrap { } private async connectionRefreshHandler(connections: Connection[]) { - if (!await this.isNavTreeEnabled()) { + const enabled = await this.isNavTreeEnabled(); + if (!enabled) { return; } @@ -368,7 +372,8 @@ export class NavNodeManagerService extends Bootstrap { } private async connectionUpdateHandler(key: ResourceKey) { - if (!await this.isNavTreeEnabled()) { + const enabled = await this.isNavTreeEnabled(); + if (!enabled) { return; } @@ -430,7 +435,8 @@ export class NavNodeManagerService extends Bootstrap { } private async isNavTreeEnabled() { - if (!await this.permissionsService.hasAsync(EPermission.public)) { + const active = await this.permissionsService.hasAsync(EPermission.public); + if (!active) { return false; } diff --git a/webapp/packages/core-app/src/shared/useChildren.ts b/webapp/packages/core-app/src/shared/useChildren.ts index 1fef9d8b33..0cf5d40acf 100644 --- a/webapp/packages/core-app/src/shared/useChildren.ts +++ b/webapp/packages/core-app/src/shared/useChildren.ts @@ -10,6 +10,7 @@ import { useCallback } from 'react'; import { useService } from '@cloudbeaver/core-di'; +import { NavigationTreeService } from '../NavigationTree/NavigationTreeService'; import { ROOT_NODE_PATH } from './NodesManager/NavNodeInfoResource'; import { NavTreeResource } from './NodesManager/NavTreeResource'; @@ -21,8 +22,9 @@ interface Hook { } export function useChildren(navNodeId = ROOT_NODE_PATH): Hook { + const navTreeService = useService(NavigationTreeService); const navTreeResource = useService(NavTreeResource); - const children = navTreeResource.get(navNodeId); + const children = navTreeService.getChildren(navNodeId); const deps = [navNodeId]; diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx index 10f271678c..6022a5ba50 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx @@ -7,8 +7,8 @@ */ import { observer } from 'mobx-react-lite'; -import { useCallback, useContext } from 'react'; -import styled, { use } from 'reshadow'; +import { useCallback, useContext, useState } from 'react'; +import { use } from 'reshadow'; import { FormContext } from '../FormContext'; import { isControlPresented } from '../isControlPresented'; @@ -61,6 +61,7 @@ export const Checkbox: CheckboxType = observer(function Checkbox({ onChange, ...rest }: CheckboxControlledProps | CheckboxObjectProps) { + const [count, refresh] = useState(0); const context = useContext(FormContext); const handleChange = useCallback((event: React.ChangeEvent) => { if (state) { @@ -83,7 +84,8 @@ export const Checkbox: CheckboxType = observer(function Checkbox({ if (context) { context.onChange(event.target.checked, name); } - }, [state, name, value, onChange, context]); + refresh(count + 1); + }, [state, name, value, onChange, context, count]); if (autoHide && !isControlPresented(name, state)) { return null; @@ -103,7 +105,7 @@ export const Checkbox: CheckboxType = observer(function Checkbox({ checked = checked.includes(value); } - return styled()( + return ( void; - onSelect?: (multiple?: boolean) => boolean; + onSelect?: (multiple?: boolean) => void; onOpen?: () => void; } @@ -42,7 +42,7 @@ export const TreeNode: React.FC = memo(function TreeNode({ }, []); const handleSelect = useCallback( - (multiple?: boolean): boolean => handlersRef.current.onSelect?.(multiple) || false, + (multiple?: boolean): void => handlersRef.current.onSelect?.(multiple), [] ); diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts index 6f79062579..c3ed80eb82 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts @@ -13,7 +13,7 @@ export interface ITreeNodeContext { loading: boolean; selected: boolean; leaf: boolean; - select: (multiple?: boolean) => boolean; + select: (multiple?: boolean) => void; expand: () => void; open: () => void; } diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx index 7a3455f23a..2fe997e0d7 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx @@ -43,7 +43,8 @@ export const TreeNodeExpand: React.FC = function TreeNodeExpand({ throw new Error('Context not provided'); } - const handleExpand = () => { + const handleExpand = (event: React.MouseEvent) => { + event.stopPropagation(); if (!context.leaf && !leaf) { context.expand(); } diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx index 06a98ceb06..74c16bbc1b 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeSelect.tsx @@ -22,10 +22,12 @@ const styles = css` `; interface Props { + disabled?: boolean; className?: string; } export const TreeNodeSelect: React.FC = function TreeNodeSelect({ + disabled, className, }) { const context = useContext(TreeNodeContext); @@ -45,7 +47,7 @@ export const TreeNodeSelect: React.FC = function TreeNodeSelect({ return styled(styles)(
- context.select(true)} /> + context.select(true)} />
); }; diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index ab1f208a62..8e991dcb5e 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -60,7 +60,8 @@ export class AuthenticationService extends Bootstrap { } private async requireAuthentication() { - if (!await this.appAuthService.isAuthNeeded()) { + const authNeeded = await this.appAuthService.isAuthNeeded(); + if (!authNeeded) { return; } From eccd811b9cb8251df1eab58a5eb0e512406523e4 Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 3 Mar 2021 18:41:04 +0300 Subject: [PATCH 02/11] fix: CB-639 request confirmation for SQL editor actions in connection edit form --- .../plugin-sql-editor/src/SqlEditorNavigatorService.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorNavigatorService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorNavigatorService.ts index 0b0b587ed0..dc211bd900 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorNavigatorService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorNavigatorService.ts @@ -7,10 +7,11 @@ */ import { NavigationTabsService } from '@cloudbeaver/core-app'; -import { ConnectionsManagerService, ConnectionInfoResource } from '@cloudbeaver/core-connections'; +import { ConnectionsManagerService } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { IExecutor, Executor, IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { NavigationService } from '@cloudbeaver/core-ui'; import type { ISqlEditorTabState } from './ISqlEditorTabState'; import { SqlEditorTabService, isSQLEditorTab } from './SqlEditorTabService'; @@ -49,13 +50,14 @@ export class SqlEditorNavigatorService { private navigationTabsService: NavigationTabsService, private connectionsManagerService: ConnectionsManagerService, private notificationService: NotificationService, - private connectionInfoResource: ConnectionInfoResource, - private sqlEditorTabService: SqlEditorTabService + private sqlEditorTabService: SqlEditorTabService, + navigationService: NavigationService ) { this.navigator = new Executor( null, (active, current) => active.type === current.type ) + .before(navigationService.navigationTask) .addHandler(this.navigateHandler.bind(this)); this.connectionsManagerService.onCloseConnection.subscribe(this.handleConnectionClose.bind(this)); } From 1cb2e81cd722733d6a46451d9a659020c57f5ebb Mon Sep 17 00:00:00 2001 From: Wroud Date: Wed, 3 Mar 2021 19:06:20 +0300 Subject: [PATCH 03/11] fix: CB-782 table text edition modal --- .../src/shared/InlineEditor/InlineEditor.tsx | 38 +++++++++++-------- .../src/DataGrid/CellEditor/CellEditor.tsx | 6 ++- .../DatabaseDataModel/DatabaseDataEditor.ts | 2 +- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx b/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx index 2953adc0f4..bbf7af7906 100644 --- a/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx +++ b/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx @@ -13,9 +13,9 @@ import { import { useImperativeHandle } from 'react'; import styled, { use } from 'reshadow'; -import { Icon } from '@cloudbeaver/core-blocks'; +import { Icon, useObjectRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { CommonDialogService } from '@cloudbeaver/core-dialogs'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { useStyles } from '@cloudbeaver/core-theming'; import { EditorDialog } from './EditorDialog'; @@ -64,33 +64,39 @@ export const InlineEditor = observer onReject, className, }, ref) { + const props = useObjectRef({ + onChange, + onReject, + onSave, + value, + }); + const commonDialogService = useService(CommonDialogService); - const handleChange = useCallback((event: ChangeEvent| string) => { - const newValue = typeof event === 'string' ? event : event.target.value; - onChange(newValue); - }, [onChange]); + const handleChange = useCallback((event: ChangeEvent) => { + props.onChange(event.target.value); + }, []); const handlePopup = useCallback(async () => { - const newValue = await commonDialogService.open(EditorDialog, value); - if (typeof newValue === 'string') { - handleChange(newValue); - onSave(); - } else if (onReject) { - onReject(); + const newValue = await commonDialogService.open(EditorDialog, props.value); + if (newValue === DialogueStateResult.Rejected || newValue === DialogueStateResult.Resolved) { + props.onReject?.(); + } else { + props.onChange(newValue); + props.onSave(); } - }, [value, commonDialogService, onSave, onReject, handleChange]); + }, []); const handleKeyDown = useCallback((event: React.KeyboardEvent) => { switch (event.key) { case 'Enter': - onSave(); + props.onSave(); break; case 'Escape': - onReject?.(); + props.onReject?.(); break; } - }, [onSave, onReject]); + }, []); const inputRef = useRef(null); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx index 09a7474826..84055631db 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellEditor/CellEditor.tsx @@ -89,8 +89,10 @@ export const CellEditor = observer, 'rowIdx' | 'row' const type = typeof tableDataContext?.getCellValue(rowIdx, column.key) === 'number' ? 'number' : 'text'; const handleSave = () => { - dataGridContext.model.source.getEditor(dataGridContext.resultIndex) - .setCell(rowIdx, Number(column.key), Number(value)); + if (type === 'number') { + const editor = dataGridContext.model.source.getEditor(dataGridContext.resultIndex); + editor.setCell(rowIdx, Number(column.key), Number(editor.getCell(rowIdx, Number(column.key)))); + } onClose(false); }; const handleReject = () => { diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataEditor.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataEditor.ts index ace8d45d4b..32ff30339a 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataEditor.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataEditor.ts @@ -69,7 +69,7 @@ export class DatabaseDataEditor implements set: this.set.bind(this, result), setCell: this.setCell.bind(this, result), get: this.get.bind(this, result), - getCell: this.get.bind(this, result), + getCell: this.getCell.bind(this, result), isCellEdited: this.isCellEdited.bind(this, result), isEdited: this.isResultEdited.bind(this, result), isRowEdited: this.isRowEdited.bind(this, result), From a188a34bf910e4b4a705791fb47cea7eb1eb68ce Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 4 Mar 2021 15:27:36 +0300 Subject: [PATCH 04/11] fix(plugin-connections): CB-781 close connection form after save --- .../src/PublicConnectionForm/PublicConnectionForm.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx index dbdb73567d..9e798355a6 100644 --- a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx @@ -62,13 +62,7 @@ export const PublicConnectionForm: React.FC = observer(function PublicConnection const service = useService(PublicConnectionFormService); const close = useCallback(() => service.close(), []); - const save = useCallback((config: ConnectionConfig) => { - if (config.connectionId) { - service.change({ connectionId: config.connectionId }); - } else { - service.close(true); - } - }, []); + const save = useCallback(() => service.close(true), []); return styled(styles)( From 441192e0fb442eb4a7c578b7aaf0ae35b6c3a453 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 4 Mar 2021 16:19:08 +0300 Subject: [PATCH 05/11] fix(plugin-authentication) users details layout --- .../src/Administration/Users/UsersTable/User.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx index 82d04fba17..dc3b43b611 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx @@ -45,7 +45,7 @@ export const User: React.FC = observer(function User({ user, selectable } {user.userId} {user.grantedRoles.join(', ')} - + From 23a462d5a73a2ea1e019a0683592101cab76cbc3 Mon Sep 17 00:00:00 2001 From: Serge Rider Date: Thu, 4 Mar 2021 18:04:31 +0300 Subject: [PATCH 06/11] CB-776 Product info API --- .../schema/service.core.graphqls | 11 +++ .../io/cloudbeaver/model/WebProductInfo.java | 76 +++++++++++++++++++ .../io/cloudbeaver/model/WebServerConfig.java | 5 ++ .../io/cloudbeaver/server/CBApplication.java | 2 +- 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductInfo.java diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index b0efafba55..0c2ab0ff49 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -75,6 +75,16 @@ type WebServiceConfig { bundleVersion: String! } +type ProductInfo { + id: ID! + version: String! + name: String! + description: String + buildTime: String! + releaseTime: String! + licenseInfo: String +} + type ServerConfig { name: String! version: String! @@ -100,6 +110,7 @@ type ServerConfig { supportedLanguages: [ ServerLanguage! ]! services: [ WebServiceConfig ] productConfiguration: Object! + productInfo: ProductInfo! defaultNavigatorSettings: NavigatorSettings! } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductInfo.java new file mode 100644 index 0000000000..1121b7c81b --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductInfo.java @@ -0,0 +1,76 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.model; + +import io.cloudbeaver.server.CBApplication; +import org.eclipse.core.runtime.Platform; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.utils.GeneralUtils; +import org.jkiss.utils.CommonUtils; + +import java.text.DateFormat; +import java.util.Date; + +/** + * Web server configuration + */ +public class WebProductInfo { + + public WebProductInfo() { + + } + + @Property + public String getId() { + return CommonUtils.notEmpty(Platform.getProduct().getId()); + } + + @Property + public String getVersion() { + return GeneralUtils.getProductVersion().toString(); + } + + @Property + public String getName() { + return CommonUtils.notEmpty(Platform.getProduct().getName()); + } + + @Property + public String getDescription() { + return CommonUtils.notEmpty(Platform.getProduct().getDescription()); + } + + @Property + public String getBuildTime() { + Date buildTime = GeneralUtils.getProductBuildTime(); + if (buildTime == null) { + buildTime = new Date(); + } + return DateFormat.getDateInstance(DateFormat.LONG).format(buildTime); + } + + @Property + public String getReleaseTime() { + return DateFormat.getDateInstance(DateFormat.LONG).format(GeneralUtils.getProductReleaseDate()); + } + + @Property + public String getLicenseInfo() { + return CBApplication.getInstance().getInfoDetails(); + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java index 0976101877..2e8d055377 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java @@ -146,4 +146,9 @@ public class WebServerConfig { return application.getAppConfiguration().getDefaultNavigatorSettings(); } + @Property + public WebProductInfo getProductInfo() { + return new WebProductInfo(); + } + } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index 71d9ead53c..8e910da128 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -465,7 +465,7 @@ public class CBApplication extends BaseApplicationImpl { @Override public String getInfoDetails() { - return serverName; + return ""; } @Override From 50ebd7ba8696a1545f46798a765ccdb006ecb55d Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 4 Mar 2021 18:23:41 +0300 Subject: [PATCH 07/11] feat(core-connections): CB-639 localization --- .../src/ConnectionForm/Options/Options.tsx | 14 +++++++++----- .../PublicConnectionFormBootstrap.ts | 2 +- .../packages/plugin-connections/src/locales/en.ts | 1 + .../packages/plugin-connections/src/locales/ru.ts | 1 + 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx index 179f16747f..18c963b53d 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx @@ -82,7 +82,7 @@ export const Options: TabContainerPanelComponent = obse data.config.connectionId = data.info.id; - data.config.name = data.info.name.trim(); + data.config.name = data.info.name; data.config.description = data.info.description; data.config.template = data.info.template; data.config.driverId = data.info.driverId; @@ -99,13 +99,17 @@ export const Options: TabContainerPanelComponent = obse data.config.authModelId = data.info.authModel; data.config.saveCredentials = data.info.saveCredentials; - for (const property of data.info.authProperties) { - if (!property.features.includes('password')) { - data.config.credentials[property.id!] = property.value; + if (data.info.authProperties) { + for (const property of data.info.authProperties) { + if (!property.features.includes('password')) { + data.config.credentials[property.id!] = property.value; + } } } - data.config.providerProperties = { ...data.info.providerProperties }; + if (data.info.providerProperties) { + data.config.providerProperties = { ...data.info.providerProperties }; + } })); const optionsHook = useOptions({ data, form: form.form, options }); const { credentialsSavingEnabled } = useAdministrationSettings(); diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormBootstrap.ts b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormBootstrap.ts index 7e6f041abd..1d293b63bc 100644 --- a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormBootstrap.ts +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormBootstrap.ts @@ -33,7 +33,7 @@ export class PublicConnectionFormBootstrap extends Bootstrap { return context.contextType === NavigationTreeContextMenuService.nodeContextType && context.data.objectFeatures.includes(EObjectFeature.dataSource); }, - title: 'Edit', + title: 'connections_public_connection_edit_menu_item_title', order: 2, onClick: context => { const node = context.data; diff --git a/webapp/packages/plugin-connections/src/locales/en.ts b/webapp/packages/plugin-connections/src/locales/en.ts index 72f0878f0a..f807e88c7d 100644 --- a/webapp/packages/plugin-connections/src/locales/en.ts +++ b/webapp/packages/plugin-connections/src/locales/en.ts @@ -1,4 +1,5 @@ export default [ + ['connections_public_connection_edit_menu_item_title', 'Edit Connection'], ['connections_public_connection_edit_cancel_title', 'Cancel confirmation'], ['connections_public_connection_edit_cancel_message', 'You\'re going to cancel connection changes. Unsaved changes will be lost. Are you sure?'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/ru.ts b/webapp/packages/plugin-connections/src/locales/ru.ts index c20aebadec..1ed9674006 100644 --- a/webapp/packages/plugin-connections/src/locales/ru.ts +++ b/webapp/packages/plugin-connections/src/locales/ru.ts @@ -1,4 +1,5 @@ export default [ + ['connections_public_connection_edit_menu_item_title', 'Изменить подключение'], ['connections_public_connection_edit_cancel_title', 'Отмена редактирования'], ['connections_public_connection_edit_cancel_message', 'Вы собираетесь закрыть редактор, несохраненные изменения не будут применены. Вы уверены?'], ]; From bff2369572416846d33a84a75abe855da3de0c74 Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 4 Mar 2021 20:17:58 +0300 Subject: [PATCH 08/11] fix(core-connections): CB-639 unnecessary data loading in connection form --- .../core-connections/src/ConnectionForm/Options/Options.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx index 18c963b53d..eb332027cf 100644 --- a/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/core-connections/src/ConnectionForm/Options/Options.tsx @@ -118,11 +118,7 @@ export const Options: TabContainerPanelComponent = obse DBDriverResource, { key: data.config.driverId || null, includes: ['includeProviderProperties'] }, { - onLoad: async resource => { - if (data.availableDrivers && data.availableDrivers.length > 1) { - await resource.load(resourceKeyList(data.availableDrivers), ['includeProviderProperties']); - } - + onLoad: async () => { if (!data.config.driverId && data.info) { data.info.authModel = undefined; } From 91f8d2617e69879d5f76353e1092378a3c632270 Mon Sep 17 00:00:00 2001 From: Wroud Date: Thu, 4 Mar 2021 22:12:13 +0300 Subject: [PATCH 09/11] fix(plugin-license): CB-718 license import behavior --- webapp/packages/core-blocks/src/Button.tsx | 40 ++++++++++++++++++- .../src/FormControls/SubmittingForm.tsx | 10 +++-- .../core-executor/src/useFormValidator.ts | 30 ++++++++------ .../core-theming/src/styles/_button.scss | 4 ++ .../src/styles/_form-controls.scss | 10 +++++ 5 files changed, 77 insertions(+), 17 deletions(-) diff --git a/webapp/packages/core-blocks/src/Button.tsx b/webapp/packages/core-blocks/src/Button.tsx index e2f1ac650e..72930be907 100644 --- a/webapp/packages/core-blocks/src/Button.tsx +++ b/webapp/packages/core-blocks/src/Button.tsx @@ -7,9 +7,12 @@ */ import styled, { css } from 'reshadow'; +import { use } from 'reshadow'; import { useStyles, composes } from '@cloudbeaver/core-theming'; +import { Loader } from './Loader/Loader'; + const buttonStyles = composes( css` Button { @@ -22,7 +25,31 @@ const buttonStyles = composes( css` Button { display: flex; + + & Loader, & button-label { + transition: opacity cubic-bezier(0.4, 0.0, 0.2, 1) 0.3s; + } + + & Loader { + position: absolute; + opacity: 0; + } + + & button-label { + opacity: 1; + } + + &[|loading] { + & Loader { + opacity: 1; + } + + & button-label { + opacity: 0; + } + } } + ` ); @@ -61,6 +88,7 @@ type ButtonProps = ( React.ButtonHTMLAttributes & React.LinkHTMLAttributes ) & { + loading?: boolean; mod?: Array; tag?: 'button' | 'a'; href?: string; @@ -72,10 +100,20 @@ export const Button: React.FC = function Button({ children, mod, tag = 'button', + disabled = false, + loading, ...rest }) { + if (loading) { + disabled = true; + } + const Button = tag; return styled(useStyles(buttonStyles, ...(mod || []).map(mod => buttonMod[mod])))( - + ); }; diff --git a/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx b/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx index bd040bc014..cd29009f32 100644 --- a/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx +++ b/webapp/packages/core-blocks/src/FormControls/SubmittingForm.tsx @@ -11,11 +11,13 @@ import { useCallback, forwardRef, useMemo } from 'react'; import { FormContext } from './FormContext'; type FormDetailedProps = Omit, HTMLFormElement>, 'onChange'> & { + disabled?: boolean; onChange?: (value: string | number | boolean | null | undefined, name: string | undefined) => void; }; export const SubmittingForm = forwardRef(function SubmittingForm( { + disabled, children, onSubmit, onChange = () => {}, @@ -34,9 +36,11 @@ export const SubmittingForm = forwardRef(fun return (
- - {children} - +
+ + {children} + +