From e41ff8d28bdbb4c3f10d3030fff245dd5ef0b2fe Mon Sep 17 00:00:00 2001 From: serge-rider Date: Wed, 2 Feb 2022 20:38:01 +0300 Subject: [PATCH 1/8] Status servlet config fix --- .../src/io/cloudbeaver/server/jetty/CBJettyServer.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java index 6df37159d8..dba2a21cb2 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBJettyServer.java @@ -5,6 +5,7 @@ import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.graphql.GraphQLEndpoint; import io.cloudbeaver.server.servlets.CBImageServlet; import io.cloudbeaver.server.servlets.CBStaticServlet; +import io.cloudbeaver.server.servlets.CBStatusServlet; import io.cloudbeaver.service.DBWServiceBindingServlet; import org.eclipse.jetty.server.*; import org.eclipse.jetty.server.session.DefaultSessionCache; @@ -60,7 +61,7 @@ public class CBJettyServer { String rootURI = application.getRootURI(); servletContextHandler.setContextPath(rootURI); servletContextHandler.addServlet(new ServletHolder("static", new CBStaticServlet()), "/*"); - servletContextHandler.addServlet(new ServletHolder("status", new CBStaticServlet()), "/status"); + servletContextHandler.addServlet(new ServletHolder("status", new CBStatusServlet()), "/status"); servletContextHandler.addServlet(new ServletHolder("images", new CBImageServlet()), application.getServicesURI() + "images/*"); servletContextHandler.addServlet(new ServletHolder("graphql", new GraphQLEndpoint()), application.getServicesURI() + "gql/*"); servletContextHandler.addEventListener(new CBServerContextListener()); From 74076ba76ac005dbdedb1ea0ed127c7c62eeac9c Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 4 Feb 2022 16:30:03 +0300 Subject: [PATCH 2/8] feat: CB-1603 nav tree nodes limitation message --- .../src/NavigationTree/ElementsTree.tsx | 20 +++++-- .../elementsTreeLimitFilter.ts | 37 ++++++++++++ .../elementsTreeLimitRenderer.tsx | 41 ++++++++++++++ .../src/NavigationTree/NavigationTree.tsx | 2 +- .../NavigationTreeNode/useNavigationNode.ts | 3 +- .../NavigationTree/NavigationTreeService.ts | 2 +- .../INavigationTreeUserSettings.ts | 1 + .../NavigationTreeSettings.tsx | 14 +++++ .../createNavigationTreeUserSettings.ts | 2 + .../NavigationTree/elementsTreeNameFilter.ts | 36 ++++++------ .../src/NavigationTree/useElementsTree.ts | 56 ++++++++++++------- webapp/packages/core-app/src/locales/en.ts | 3 + webapp/packages/core-app/src/locales/it.ts | 3 + webapp/packages/core-app/src/locales/ru.ts | 3 + webapp/packages/core-app/src/locales/zh.ts | 3 + .../NavNodeView/NavNodeViewService.ts | 36 ++++++++++-- .../shared/NodesManager/NavTreeResource.ts | 2 +- .../core-app/src/shared/useChildren.ts | 8 ++- .../NavNodeMetadata/ObjectProperties.tsx | 3 +- .../VirtualFolder/VirtualFolderPanel.tsx | 6 +- .../ObjectPropertiesPagePanel.tsx | 3 - .../ObjectPropertyTable.tsx | 4 +- 22 files changed, 226 insertions(+), 62 deletions(-) create mode 100644 webapp/packages/core-app/src/NavigationTree/NavTreeLimitFilter/elementsTreeLimitFilter.ts create mode 100644 webapp/packages/core-app/src/NavigationTree/NavTreeLimitFilter/elementsTreeLimitRenderer.tsx diff --git a/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx b/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx index 03e713deb9..d44e25a7ca 100644 --- a/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx +++ b/webapp/packages/core-app/src/NavigationTree/ElementsTree.tsx @@ -26,6 +26,8 @@ import type { NavTreeControlComponent } from './NavigationNodeComponent'; import { NavigationNodeNested } from './NavigationTreeNode/NavigationNode/NavigationNodeNested'; import { NavigationNodeElement } from './NavigationTreeNode/NavigationNodeElement'; import { NavigationTreeService } from './NavigationTreeService'; +import { elementsTreeLimitFilter } from './NavTreeLimitFilter/elementsTreeLimitFilter'; +import { elementsTreeLimitRenderer } from './NavTreeLimitFilter/elementsTreeLimitRenderer'; import { ITreeContext, TreeContext } from './TreeContext'; import { IElementsTreeCustomRenderer, IElementsTreeFilter, ITreeNodeState, useElementsTree } from './useElementsTree'; @@ -65,11 +67,13 @@ const styles = composes( interface Props { root?: string; + limit?: number; keepData?: boolean; disabled?: boolean; selectionTree?: boolean; foldersTree?: boolean; filter?: boolean; + filterAll?: boolean; showFolderExplorerPath?: boolean; localState?: MetadataMap; control?: NavTreeControlComponent; @@ -91,6 +95,7 @@ interface Props { export const ElementsTree = observer(function ElementsTree({ root: baseRoot = ROOT_NODE_PATH, + limit, control, keepData = false, disabled, @@ -99,10 +104,11 @@ export const ElementsTree = observer(function ElementsTree({ showFolderExplorerPath = false, foldersTree = false, filter = false, + filterAll = false, emptyPlaceholder, navNodeFilterCompare, - filters, - renderers, + filters = [], + renderers = [], style, className, isGroup, @@ -165,6 +171,11 @@ export const ElementsTree = observer(function ElementsTree({ }, }); + const limitFilter = useMemo(() => elementsTreeLimitFilter( + navTreeResource, + limit + ), [navTreeResource, limit]); + const nameFilter = useMemo(() => elementsTreeNameFilter( navTreeResource, navNodeInfoResource, @@ -178,10 +189,11 @@ export const ElementsTree = observer(function ElementsTree({ showFolderExplorerPath, root, disabled, + filterAll, keepData, localState, - filters: [nameFilter, ...(filters || [])], - renderers, + filters: [nameFilter, ...filters, limitFilter], + renderers: [...renderers, elementsTreeLimitRenderer], isGroup, onFilter, beforeSelect, diff --git a/webapp/packages/core-app/src/NavigationTree/NavTreeLimitFilter/elementsTreeLimitFilter.ts b/webapp/packages/core-app/src/NavigationTree/NavTreeLimitFilter/elementsTreeLimitFilter.ts new file mode 100644 index 0000000000..551a81badc --- /dev/null +++ b/webapp/packages/core-app/src/NavigationTree/NavTreeLimitFilter/elementsTreeLimitFilter.ts @@ -0,0 +1,37 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 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 { NavNode } from '../../shared/NodesManager/EntityTypes'; +import type { NavTreeResource } from '../../shared/NodesManager/NavTreeResource'; +import type { IElementsTreeFilter } from '../useElementsTree'; + +export type NavNodeFilterCompareFn = (node: NavNode, filter: string)=> boolean; + +export const NAVIGATION_TREE_LIMIT = { + limit: 'nav-tree://limit', +}; + +export function elementsTreeLimitFilter( + navTreeResource: NavTreeResource, + limit?: number +): IElementsTreeFilter { + return ( + filter: string, + node: NavNode, + children: string[], + ) => { + limit = limit ?? navTreeResource.childrenLimit; + const nextChildren = children.slice(0, limit); + + if (children.length > limit) { + nextChildren.push(NAVIGATION_TREE_LIMIT.limit); + } + + return nextChildren; + }; +} diff --git a/webapp/packages/core-app/src/NavigationTree/NavTreeLimitFilter/elementsTreeLimitRenderer.tsx b/webapp/packages/core-app/src/NavigationTree/NavTreeLimitFilter/elementsTreeLimitRenderer.tsx new file mode 100644 index 0000000000..aa9242bd6a --- /dev/null +++ b/webapp/packages/core-app/src/NavigationTree/NavTreeLimitFilter/elementsTreeLimitRenderer.tsx @@ -0,0 +1,41 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 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 type { NavigationNodeRendererComponent } from '../NavigationNodeComponent'; +import { NAVIGATION_TREE_LIMIT } from './elementsTreeLimitFilter'; + +export function elementsTreeLimitRenderer(nodeId: string): NavigationNodeRendererComponent | undefined { + if (nodeId === NAVIGATION_TREE_LIMIT.limit) { + return ManageableGroup; + } + + return; +} + +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 32px; + } + ` +); + +const ManageableGroup: NavigationNodeRendererComponent = function ManageableGroup() { + return styled(useStyles(styles))(); +}; diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx index 2990f1370d..6a2954706c 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx @@ -39,7 +39,6 @@ const navigationTreeStyles = css` } ElementsTree { - padding-top: 8px; min-width: 100%; width: max-content; } @@ -110,6 +109,7 @@ export const NavigationTree = observer(function NavigationTree() { foldersTree={settings.folders} showFolderExplorerPath={settings.folders} filter={settings.filter} + filterAll={settings.filterAll} keepData={settings.saveExpanded} onOpen={handleOpen} /> diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts index 9b9b52770d..bb92244598 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts @@ -38,10 +38,9 @@ export function useNavigationNode(node: NavNode, path: string[]): INavigationNod }); const { isLoading } = useNode(node.id); const children = useChildren(node.id); - const state = contextRef.context?.tree.getNodeState(node.id); const loading = getComputed(() => isLoading() || children.isLoading()); - const isExpanded = getComputed(() => state?.expanded || false); + const isExpanded = getComputed(() => contextRef.context?.tree.isNodeExpanded(node.id) || false); const leaf = getComputed(() => isLeaf(node)); const group = getComputed(() => contextRef.context?.tree.isGroup?.(node) || false); const empty = getComputed(() => children.children?.length === 0); diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts index 58a06188c8..f7e4b584b5 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts @@ -60,7 +60,7 @@ export class NavigationTreeService extends View { } getChildren(id: string): string[] | undefined { - return this.navTreeResource.get(id); + return this.navNodeManagerService.getTree(id); } async navToNode(id: string, parentId: string): Promise { diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeSettings/INavigationTreeUserSettings.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeSettings/INavigationTreeUserSettings.ts index d34b0591be..6c2d96efdb 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeSettings/INavigationTreeUserSettings.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeSettings/INavigationTreeUserSettings.ts @@ -8,6 +8,7 @@ export interface INavigationTreeUserSettings { filter: boolean; + filterAll: boolean; saveExpanded: boolean; folders: boolean; } \ No newline at end of file diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeSettings/NavigationTreeSettings.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeSettings/NavigationTreeSettings.tsx index 7778aa7481..8fde16681e 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeSettings/NavigationTreeSettings.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeSettings/NavigationTreeSettings.tsx @@ -29,6 +29,10 @@ const expandStyles = css` transform: rotate(180deg); } } + Group { + min-width: 350px; + width: min-content; + } `; interface Props { @@ -69,6 +73,16 @@ export const NavigationTreeSettings = observer(function NavigationTreeSet > {translate('app_navigationTree_settings_filter_title')} + + {translate('app_navigationTree_settings_filter_all_title')} + ({ filter: false, + filterAll: false, saveExpanded: true, folders: false, }); @@ -21,6 +22,7 @@ export function createNavigationTreeUserSettings(): INavigationTreeUserSettings export function validateNavigationTreeUserSettings(data: any): boolean { return ( typeof data === 'object' + && typeof data.filterAll === 'boolean' && typeof data.filter === 'boolean' && typeof data.saveExpanded === 'boolean' && typeof data.folders === 'boolean' diff --git a/webapp/packages/core-app/src/NavigationTree/elementsTreeNameFilter.ts b/webapp/packages/core-app/src/NavigationTree/elementsTreeNameFilter.ts index de56efecf0..481bcc29cb 100644 --- a/webapp/packages/core-app/src/NavigationTree/elementsTreeNameFilter.ts +++ b/webapp/packages/core-app/src/NavigationTree/elementsTreeNameFilter.ts @@ -39,7 +39,7 @@ export function elementsTreeNameFilter( compare, filter, child, - state + // state )); return nodes.map(node => node.id); @@ -52,32 +52,32 @@ function filterNode( compare: NavNodeFilterCompareFn, filter: string, node: NavNode, - state: MetadataMap + // state: MetadataMap ): boolean { - const nodeState = state.get(node.id); + // const nodeState = state.get(node.id); if (compare(node, filter)) { return true; } - if (nodeState.expanded) { - const children = navTreeResource.get(node.id) || []; + // if (nodeState.expanded) { + const children = navTreeResource.get(node.id) || []; - return navNodeInfoResource - .get(resourceKeyList(children)) - .filter(isDefined) - .some(child => filterNode( - navTreeResource, - navNodeInfoResource, - compare, - filter, - child, - state - )); - } + return navNodeInfoResource + .get(resourceKeyList(children)) + .filter(isDefined) + .some(child => filterNode( + navTreeResource, + navNodeInfoResource, + compare, + filter, + child, + // state + )); + // } - return false; + // return false; } function compareNodes(node: NavNode, filter: string): boolean { diff --git a/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts b/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts index 92a0033ea4..99182a7231 100644 --- a/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts +++ b/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts @@ -47,6 +47,7 @@ interface IOptions { showFolderExplorerPath: boolean; disabled?: boolean; keepData?: boolean; + filterAll?: boolean; localState?: MetadataMap; filters?: IElementsTreeFilter[]; renderers?: IElementsTreeCustomRenderer[]; @@ -64,11 +65,13 @@ export interface IElementsTree { filter: string; loading: boolean; disabled: boolean; + filterAll: boolean; foldersTree: boolean; showFolderExplorerPath: boolean; renderers: IElementsTreeCustomRenderer[]; state: MetadataMap; getNodeState: (nodeId: string) => ITreeNodeState; + isNodeExpanded: (nodeId: string) => boolean; isNodeSelected: (nodeId: string) => boolean; getNodeChildren: (nodeId: string) => string[]; isGroup?: (node: NavNode) => boolean; @@ -131,8 +134,8 @@ export function useElementsTree(options: IOptions): IElementsTree { await navNodeInfoResource.waitLoad(); await navTreeResource.waitLoad(); - const nodeState = state.get(child); - if (!nodeState.expanded && child !== options.root) { + const expanded = elementsTree.isNodeExpanded(child); + if (!expanded && child !== options.root) { if (navNodeInfoResource.isOutdated(child)) { const node = navNodeInfoResource.get(child); @@ -161,28 +164,14 @@ export function useElementsTree(options: IOptions): IElementsTree { } } - 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(elementsTree.filter, node, children, state), - navTreeService.getChildren(node.id) || [] - ); - } - function getNestedChildren(nodeId: string): string [] { const nestedChildren: string[] = []; - const prevChildren = getNodeChildren(nodeId); + const prevChildren = elementsTree.getNodeChildren(nodeId); nestedChildren.push(...prevChildren); while (prevChildren.length) { const nodeKey = prevChildren.shift()!; - const children = getNodeChildren(nodeKey); + const children = elementsTree.getNodeChildren(nodeKey); prevChildren.push(...children); nestedChildren.push(...children); } @@ -227,7 +216,7 @@ export function useElementsTree(options: IOptions): IElementsTree { } if (options.isGroup?.(node)) { - const children = getNodeChildren(nodeId); + const children = elementsTree.getNodeChildren(nodeId); for (const child of children) { await setSelection(child, selected); @@ -252,11 +241,22 @@ export function useElementsTree(options: IOptions): IElementsTree { getNodeState(nodeId: string) { return this.state.get(nodeId); }, + isNodeExpanded(nodeId: string): boolean { + if (nodeId === this.root) { + return true; + } + + if (this.filter !== '' && this.filterAll) { + return this.getNodeChildren(nodeId).length > 0; + } + + return this.getNodeState(nodeId).expanded; + }, isNodeSelected(nodeId: string): boolean { const node = navNodeInfoResource.get(nodeId); if (node && elementsTree.isGroup?.(node)) { - const children = getNodeChildren(nodeId); + const children = this.getNodeChildren(nodeId); if (children.length > 0) { return children.every(child => this.isNodeSelected(child)); @@ -267,7 +267,19 @@ export function useElementsTree(options: IOptions): IElementsTree { return this.getNodeState(nodeId).selected; }, - getNodeChildren, + 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(elementsTree.filter, node, children, state), + navTreeService.getChildren(node.id) || [] + ); + }, async setFilter(value: string) { this.filter = value; @@ -320,6 +332,7 @@ export function useElementsTree(options: IOptions): IElementsTree { disabled: observable.ref, root: observable.ref, filter: observable.ref, + filterAll: observable.ref, loading: observable.ref, renderers: observable.ref, baseRoot: observable.ref, @@ -328,6 +341,7 @@ export function useElementsTree(options: IOptions): IElementsTree { disabled: options.disabled, root: options.root, foldersTree, + filterAll: options.filterAll, showFolderExplorerPath, baseRoot: options.baseRoot, renderers, diff --git a/webapp/packages/core-app/src/locales/en.ts b/webapp/packages/core-app/src/locales/en.ts index e1f344289f..a1231e9c91 100644 --- a/webapp/packages/core-app/src/locales/en.ts +++ b/webapp/packages/core-app/src/locales/en.ts @@ -12,9 +12,12 @@ export default [ ['app_topnavbar_connection_schema_manager_not_selected', '-- not selected --'], ['app_navigationTree_openNodeTab', 'Open'], ['app_navigationTree_search', 'Search elements...'], + ['app_navigationTree_limited', 'Some elements were hidden'], ['app_navigationTree_settings_title', 'Settings'], ['app_navigationTree_settings_filter_title', 'Filter'], ['app_navigationTree_settings_filter_description', 'Show filtering field'], + ['app_navigationTree_settings_filter_all_title', 'Show collapsed'], + ['app_navigationTree_settings_filter_all_description', 'Shows filtered elements in collapsed folders'], ['app_navigationTree_settings_state_title', 'Save state'], ['app_navigationTree_settings_state_description', 'Reopen expanded nodes after page refresh'], ['app_navigationTree_settings_folders_title', 'Folders'], diff --git a/webapp/packages/core-app/src/locales/it.ts b/webapp/packages/core-app/src/locales/it.ts index 624ed46caa..435c98edd2 100644 --- a/webapp/packages/core-app/src/locales/it.ts +++ b/webapp/packages/core-app/src/locales/it.ts @@ -12,9 +12,12 @@ export default [ ['app_topnavbar_connection_schema_manager_not_selected', '-- non selezionato --'], ['app_navigationTree_openNodeTab', 'Apri'], ['app_navigationTree_search', 'Search elements...'], + ['app_navigationTree_limited', 'Some elements were hidden'], ['app_navigationTree_settings_title', 'Settings'], ['app_navigationTree_settings_filter_title', 'Filter'], ['app_navigationTree_settings_filter_description', 'Show filtering field'], + ['app_navigationTree_settings_filter_all_title', 'Show collapsed'], + ['app_navigationTree_settings_filter_all_description', 'Shows filtered elements in collapsed folders'], ['app_navigationTree_settings_state_title', 'Save state'], ['app_navigationTree_settings_state_description', 'Reopen expanded nodes after page refresh'], ['app_navigationTree_settings_folders_title', 'Folders'], diff --git a/webapp/packages/core-app/src/locales/ru.ts b/webapp/packages/core-app/src/locales/ru.ts index f25aea7e52..d6d6345b09 100644 --- a/webapp/packages/core-app/src/locales/ru.ts +++ b/webapp/packages/core-app/src/locales/ru.ts @@ -15,10 +15,13 @@ export default [ ], ['app_navigationTree_openNodeTab', 'Открыть'], ['app_navigationTree_search', 'Искать элементы...'], + ['app_navigationTree_limited', 'Некоторые элементы были скрыты'], ['app_navigationTree_settings_title', 'Настройки'], ['app_navigationTree_settings_filter_title', 'Фильтр'], ['app_navigationTree_settings_filter_description', 'Показывать фильтр'], + ['app_navigationTree_settings_filter_all_title', 'Фильтровать скрытые элементы'], + ['app_navigationTree_settings_filter_all_description', 'Показывает скрытые элементы при использовании фильтра'], ['app_navigationTree_settings_state_title', 'Сохранять состояние'], ['app_navigationTree_settings_state_description', 'Восстанавливает открытые узлы после обновления страницы'], ['app_navigationTree_settings_folders_title', 'Папки'], diff --git a/webapp/packages/core-app/src/locales/zh.ts b/webapp/packages/core-app/src/locales/zh.ts index e46bb9b31e..6881f3aa1b 100644 --- a/webapp/packages/core-app/src/locales/zh.ts +++ b/webapp/packages/core-app/src/locales/zh.ts @@ -11,9 +11,12 @@ export default [ ['app_shared_settingsMenu_lang', '语言'], ['app_topnavbar_connection_schema_manager_not_selected', '-- 为选择 --'], ['app_navigationTree_search', 'Search elements...'], + ['app_navigationTree_limited', 'Some elements were hidden'], ['app_navigationTree_settings_title', 'Settings'], ['app_navigationTree_settings_filter_title', 'Filter'], ['app_navigationTree_settings_filter_description', 'Show filtering field'], + ['app_navigationTree_settings_filter_all_title', 'Show collapsed'], + ['app_navigationTree_settings_filter_all_description', 'Shows filtered elements in collapsed folders'], ['app_navigationTree_settings_state_title', 'Save state'], ['app_navigationTree_settings_state_description', 'Reopen expanded nodes after page refresh'], ['app_navigationTree_settings_folders_title', 'Folders'], diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeView/NavNodeViewService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeView/NavNodeViewService.ts index 8b9e0bee61..a26f349f9e 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeView/NavNodeViewService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeView/NavNodeViewService.ts @@ -19,6 +19,11 @@ export interface INodeDuplicateList { duplicates: string[]; } +export interface INodeLimitedList { + nodes: string[]; + truncated: number; +} + @injectable() export class NavNodeViewService { get tabs(): NavNodeTransformView[] { @@ -41,12 +46,12 @@ export class NavNodeViewService { .map(transform => transform.transformer); } - private transformers: INavNodeFolderTransform[]; - private duplicationNotify: Set; + private readonly transformers: INavNodeFolderTransform[]; + private readonly duplicationNotify: Set; constructor( - private navTreeResource: NavTreeResource, - private notificationService: NotificationService + private readonly navTreeResource: NavTreeResource, + private readonly notificationService: NotificationService ) { this.transformers = []; this.duplicationNotify = new Set(); @@ -72,9 +77,15 @@ export class NavNodeViewService { getFolders(nodeId: string): string[] | undefined { const children = this.navTreeResource.get(nodeId); + if (!children) { + return; + } + + const limited = this.limit(children); + return this.transformations.reduce( (children, transform) => transform(nodeId, children), - children + limited.nodes as string[] | undefined ); } @@ -82,6 +93,21 @@ export class NavNodeViewService { this.transformers.push(transform); } + limit(nodes: string[]): INodeLimitedList { + let truncated = 0; + + if (nodes.length > this.navTreeResource.childrenLimit) { + truncated = nodes.length - this.navTreeResource.childrenLimit; + } + + nodes = nodes.slice(0, this.navTreeResource.childrenLimit); + + return { + nodes, + truncated, + }; + } + filterDuplicates(nodes: string[]): INodeDuplicateList { const nextChildren: string[] = []; const duplicates: string[] = []; diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts index 0ffa1b5402..a9b2c4c9c1 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts @@ -473,6 +473,6 @@ export class NavTreeResource extends CachedMapResource { navNodeInfo.hasChildren = navNodeInfo.hasChildren && navNodeChildren.length > 0; - return { navNodeChildren: navNodeChildren.slice(0, this.childrenLimit), navNodeInfo, parentPath }; + return { navNodeChildren, navNodeInfo, parentPath }; } } diff --git a/webapp/packages/core-app/src/shared/useChildren.ts b/webapp/packages/core-app/src/shared/useChildren.ts index e6a68c4a02..9e426629cc 100644 --- a/webapp/packages/core-app/src/shared/useChildren.ts +++ b/webapp/packages/core-app/src/shared/useChildren.ts @@ -12,6 +12,7 @@ import { useService } from '@cloudbeaver/core-di'; import { NavigationTreeService } from '../NavigationTree/NavigationTreeService'; import { ROOT_NODE_PATH } from './NodesManager/NavNodeInfoResource'; +import { NavNodeViewService } from './NodesManager/NavNodeView/NavNodeViewService'; import { NavTreeResource } from './NodesManager/NavTreeResource'; interface Hook { @@ -23,11 +24,16 @@ interface Hook { } export function useChildren(navNodeId = ROOT_NODE_PATH): Hook { + const navNodeViewService = useService(NavNodeViewService); const navTreeService = useService(NavigationTreeService); const navTreeResource = useService(NavTreeResource); - const children = navTreeService.getChildren(navNodeId); + let children = navTreeService.getChildren(navNodeId); const exception = navTreeResource.getException(navNodeId); + if (children) { + children = navNodeViewService.limit(children).nodes; + } + const deps = [navNodeId]; const isLoading = useCallback(() => navTreeResource.isDataLoading(navNodeId), deps); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/NavNodeMetadata/ObjectProperties.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/NavNodeMetadata/ObjectProperties.tsx index 2f487cf974..683e4e2a7f 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/NavNodeMetadata/ObjectProperties.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/NavNodeMetadata/ObjectProperties.tsx @@ -10,8 +10,7 @@ import { observer } from 'mobx-react-lite'; import styled from 'reshadow'; import { DBObjectResource, NavTreeResource } from '@cloudbeaver/core-app'; -import { ColoredContainer, Loader, TextPlaceholder, useObjectPropertyCategories, GroupTitle, ObjectPropertyInfoForm, Group, useMapResource } from '@cloudbeaver/core-blocks'; -import { BASE_CONTAINERS_STYLES } from '@cloudbeaver/core-blocks'; +import { ColoredContainer, Loader, TextPlaceholder, useObjectPropertyCategories, GroupTitle, ObjectPropertyInfoForm, Group, useMapResource, BASE_CONTAINERS_STYLES } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx index 9976418be2..8cb74438e9 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderPanel.tsx @@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; -import { DBObject, DBObjectResource, NavNodeInfoResource, NavNodeTransformViewComponent, NavTreeResource } from '@cloudbeaver/core-app'; +import { DBObject, DBObjectResource, NavNodeInfoResource, NavNodeTransformViewComponent, NavNodeViewService, NavTreeResource } from '@cloudbeaver/core-app'; import { Loader, TextPlaceholder, useMapResource } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; @@ -33,12 +33,14 @@ export const VirtualFolderPanel: NavNodeTransformViewComponent = observer(functi }) { const translate = useTranslate(); const nodeType = VirtualFolderUtils.getNodeType(folderId); + const navNodeViewService = useService(NavNodeViewService); const navNodeInfoResource = useService(NavNodeInfoResource); const tree = useMapResource(VirtualFolderPanel, NavTreeResource, nodeId, { onLoad: async resource => !(await resource.preloadNodeParents(parents, nodeId)), }); - const key = resourceKeyList(tree.data || []); + const { nodes } = navNodeViewService.limit(tree.data || []); + const key = resourceKeyList(nodes); const dbObject = useMapResource(VirtualFolderPanel, DBObjectResource, key, { async onLoad(resource: DBObjectResource) { const preloaded = await tree.resource.preloadNodeParents(parents, nodeId); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx index b7499ff45e..e2debc765a 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertiesPagePanel.tsx @@ -9,9 +9,6 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; -import { useChildren } from '@cloudbeaver/core-app'; -import { Loader, TextPlaceholder } from '@cloudbeaver/core-blocks'; -import { useTranslate } from '@cloudbeaver/core-localization'; import { useStyles, composes } from '@cloudbeaver/core-theming'; import type { ObjectPagePanelComponent } from '../ObjectPage/ObjectPage'; diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx index b921ca0e4b..60cd7841bd 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx @@ -47,7 +47,9 @@ export const ObjectPropertyTable = observer(function O onLoad: async resource => !(await resource.preloadNodeParents(parents, objectId)), }); - const { nodes, duplicates } = navNodeViewService.filterDuplicates(tree.data || []); + const limited = navNodeViewService.limit(tree.data || []); + + const { nodes, duplicates } = navNodeViewService.filterDuplicates(limited.nodes); const key = resourceKeyList(nodes); const dbObject = useMapResource(ObjectPropertyTable, DBObjectResource, key, { From cc1a56dfb898c019f2a1a512f4ceb8d21e25b732 Mon Sep 17 00:00:00 2001 From: Wroud Date: Fri, 4 Feb 2022 16:56:29 +0300 Subject: [PATCH 3/8] fix: CB-1624 first render skipping use*Resource --- webapp/packages/core-blocks/src/useDataResource.ts | 2 +- webapp/packages/core-blocks/src/useMapResource.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/packages/core-blocks/src/useDataResource.ts b/webapp/packages/core-blocks/src/useDataResource.ts index 74edd6e5e5..f8c59cf1c8 100644 --- a/webapp/packages/core-blocks/src/useDataResource.ts +++ b/webapp/packages/core-blocks/src/useDataResource.ts @@ -193,7 +193,7 @@ export function useDataResource< })); useEffect(() => { - if (!outdated && !refObj.firstRender) { + if ((!outdated && !refObj.firstRender) || refObj.key === null) { return; } diff --git a/webapp/packages/core-blocks/src/useMapResource.ts b/webapp/packages/core-blocks/src/useMapResource.ts index 36b3774b0f..2b34cba9c1 100644 --- a/webapp/packages/core-blocks/src/useMapResource.ts +++ b/webapp/packages/core-blocks/src/useMapResource.ts @@ -298,7 +298,7 @@ export function useMapResource< const preloaded = refObj.preloaded; // make mobx subscription useEffect(() => { - if (!preloaded || (!outdated && !refObj.firstRender)) { + if (!preloaded || (!outdated && !refObj.firstRender) || refObj.key === null) { return; } From bc0f9b8e0461c6d800291b292c3abcbe72dfaf32 Mon Sep 17 00:00:00 2001 From: Ksenia <51405061+kseniiaguzeeva@users.noreply.github.com> Date: Mon, 7 Feb 2022 12:42:10 +0300 Subject: [PATCH 4/8] chore: update README --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 41dada2d14..215f2d0c01 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,15 @@ You can see live demo of CloudBeaver here: https://demo.cloudbeaver.io [Database access instructions](https://github.com/dbeaver/cloudbeaver/wiki/Demo-Server) ## Changelog +### CloudBeaver 21.3.4 - 2022-02-07 +- The Database Navigator menu allows you to: + - filter objects in the Database Navigator, + - set the Database Navigator view for every user separately. +- Parsing of scripts with delimiters has been improved in the SQL Editor. +- The dialog with available shortcuts can be opened in the top menu. +- The specific IP address can be set for the CloudBeaver instance in configuration files. +- Different small bugs have been fixed. + ### CloudBeaver 21.3.3 - 2022-01-25 - SSH public key support is added. - New tab's context menu allows users to close all opened tabs or a group of tabs. From 5f629fca9d53bcb802d2148f7a214f756ce68cbf Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Mon, 7 Feb 2022 17:40:06 +0300 Subject: [PATCH 5/8] =?UTF-8?q?fix(core-ui):=20CB-1578=20make=20handler=20?= =?UTF-8?q?function=20asyn=D1=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- webapp/packages/core-ui/src/Tabs/Tab/Tab.tsx | 7 ++++--- webapp/packages/core-ui/src/Tabs/Tab/useTab.ts | 14 +++++++------- webapp/packages/core-ui/src/Tabs/TabsContext.ts | 1 + webapp/packages/core-ui/src/Tabs/TabsState.tsx | 6 +++++- .../src/SqlResultTabs/SqlResultTab.tsx | 2 +- .../src/SqlResultTabs/SqlResultTabs.tsx | 9 +++++++-- 6 files changed, 25 insertions(+), 14 deletions(-) diff --git a/webapp/packages/core-ui/src/Tabs/Tab/Tab.tsx b/webapp/packages/core-ui/src/Tabs/Tab/Tab.tsx index f2e9b3f9fb..b39a309ec1 100644 --- a/webapp/packages/core-ui/src/Tabs/Tab/Tab.tsx +++ b/webapp/packages/core-ui/src/Tabs/Tab/Tab.tsx @@ -41,7 +41,7 @@ export const Tab = observer(function Tab({ const { state, getInfo, handleClose, handleOpen } = useTab(tabId, onOpen, onClose, onClick); const menu = useMenu({ menu: MENU_TAB, - context: menuContext, + context: menuContext, }); const info = getInfo(); @@ -51,7 +51,8 @@ export const Tab = observer(function Tab({ menu.context.set(DATA_CONTEXT_TAB_ID, tabId); const showMenu = menu.getItems().length > 0; - const actionsEnabled = !!onClose || showMenu; + const canClose = !!onClose || state.closable; + const actionsEnabled = canClose || showMenu; return styled(useStyles(style))( @@ -72,7 +73,7 @@ export const Tab = observer(function Tab({ {actionsEnabled && ( - {onClose && ( + {canClose && ( diff --git a/webapp/packages/core-ui/src/Tabs/Tab/useTab.ts b/webapp/packages/core-ui/src/Tabs/Tab/useTab.ts index dbfdda7408..90ab9b6beb 100644 --- a/webapp/packages/core-ui/src/Tabs/Tab/useTab.ts +++ b/webapp/packages/core-ui/src/Tabs/Tab/useTab.ts @@ -8,16 +8,16 @@ import { useContext } from 'react'; -import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; import { useExecutor, useObjectRef } from '@cloudbeaver/core-blocks'; +import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events'; import type { ITabData } from '../TabsContainer/ITabsContainer'; import { TabsContext } from '../TabsContext'; export function useTab( tabId: string, - onOpen?: (tab: ITabData) => void, - onClose?: (tab: ITabData) => void, + onOpen?: (tab: ITabData) => Promise | void, + onClose?: (tab: ITabData) => Promise | void, onClick?: (tabId: string) => void, ) { const state = useContext(TabsContext); @@ -27,21 +27,21 @@ export function useTab( useExecutor({ executor: state.openExecutor, - handlers: [function openHandler(data) { + handlers: [async function openHandler(data) { if (tabId !== data.tabId) { return; } - onOpen?.(data); + await onOpen?.(data); }], }); useExecutor({ executor: state.closeExecutor, - handlers: [function closeHandler(data) { + handlers: [async function closeHandler(data) { if (tabId !== data.tabId) { return; } - onClose?.(data); + await onClose?.(data); }], }); diff --git a/webapp/packages/core-ui/src/Tabs/TabsContext.ts b/webapp/packages/core-ui/src/Tabs/TabsContext.ts index af3dce1a30..a82a139583 100644 --- a/webapp/packages/core-ui/src/Tabs/TabsContext.ts +++ b/webapp/packages/core-ui/src/Tabs/TabsContext.ts @@ -24,6 +24,7 @@ export interface ITabsContext> { openExecutor: IExecutor>; closeExecutor: IExecutor>; lazy: boolean; + closable: boolean; tabList?: string[]; enabledBaseActions?: boolean; getTabInfo: (tabId: string) => ITabInfo | undefined; diff --git a/webapp/packages/core-ui/src/Tabs/TabsState.tsx b/webapp/packages/core-ui/src/Tabs/TabsState.tsx index b59a21fd7f..fd6d481bf6 100644 --- a/webapp/packages/core-ui/src/Tabs/TabsState.tsx +++ b/webapp/packages/core-ui/src/Tabs/TabsState.tsx @@ -62,6 +62,8 @@ export const TabsState = observer(function TabsState>({ } } + const closable = !!onClose; + const [localTabsState] = useState(() => new MetadataMap()); const tabsState = localState || localTabsState; const [closeExecutor] = useState(() => new Executor>()); @@ -82,7 +84,7 @@ export const TabsState = observer(function TabsState>({ tabsState, container, state, - tabList + tabList, }); if (currentTabId !== undefined) { @@ -197,6 +199,7 @@ export const TabsState = observer(function TabsState>({ openExecutor, closeExecutor, lazy, + closable, tabList, enabledBaseActions, getTabInfo, @@ -215,6 +218,7 @@ export const TabsState = observer(function TabsState>({ closeExecutor, openExecutor, lazy, + closable, tabList, enabledBaseActions, getTabInfo, diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTab.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTab.tsx index 98faa013f4..3c7fcf7de6 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTab.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTab.tsx @@ -21,7 +21,7 @@ interface Props { result: ISqlEditorResultTab; className?: string; style?: ComponentStyle; - onClose: (tab: ITabData) => Promise; + onClose?: (tab: ITabData) => Promise; } export const SqlResultTab = observer(function SqlResultTab({ diff --git a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx index cc4451bf5c..ef1cfc3286 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlResultTabs/SqlResultTabs.tsx @@ -98,14 +98,19 @@ export const SqlResultTabs = observer(function SqlDataResult({ state, onT return styled(style)( - + {orderedTabs.map(result => ( ))} From d779ff1f11b71d97be0b3d05706b23b7064c8b5d Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 7 Feb 2022 18:00:48 +0300 Subject: [PATCH 6/8] chore: update .gitignore --- webapp/.gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webapp/.gitignore b/webapp/.gitignore index d9ea6b69ed..520d040a02 100644 --- a/webapp/.gitignore +++ b/webapp/.gitignore @@ -8,4 +8,5 @@ lib coverage ts-out npm-debug.log -debug.log \ No newline at end of file +debug.log +tsconfig.tsbuildinfo \ No newline at end of file From 177ec6575c3928ee12ca36b5605645630cf41f3a Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 7 Feb 2022 18:49:50 +0300 Subject: [PATCH 7/8] feat: CB-1607 automated federated authentication for a single configuration --- .../schema/service.core.graphqls | 1 + .../io/cloudbeaver/model/WebServerConfig.java | 5 +++ .../src/AuthProviderService.ts | 2 +- .../core-root/src/ServerConfigResource.ts | 10 +++-- .../src/queries/session/serverConfig.gql | 1 + webapp/packages/core-sdk/src/sdk.ts | 4 +- .../src/AuthenticationService.ts | 41 +++++++++++++++---- .../src/Dialog/AuthDialogService.ts | 16 ++++++-- 8 files changed, 63 insertions(+), 17 deletions(-) diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index d4d37a789a..7227ada04f 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -133,6 +133,7 @@ type ServerConfig { configurationMode: Boolean developmentMode: Boolean + redirectOnFederatedAuth: Boolean enabledFeatures: [ID!]! enabledAuthProviders: [ID!]! 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 6409824f57..51b9f060ed 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 @@ -130,6 +130,11 @@ public class WebServerConfig { return application.isDevelMode(); } + @Property + public boolean isRedirectOnFederatedAuth() { + return application.getAppConfiguration().isRedirectOnFederatedAuth(); + } + @Property public long getSessionExpireTime() { return application.getMaxSessionIdleTime(); diff --git a/webapp/packages/core-authentication/src/AuthProviderService.ts b/webapp/packages/core-authentication/src/AuthProviderService.ts index 4d2ab0b7fa..536286d51e 100644 --- a/webapp/packages/core-authentication/src/AuthProviderService.ts +++ b/webapp/packages/core-authentication/src/AuthProviderService.ts @@ -33,7 +33,7 @@ interface IServiceDescriptionLink extends IServiceDescriptionLinkOptions { export class AuthProviderService { readonly requestAuthProvider: IExecutor; - private serviceDescriptionLinker: IServiceDescriptionLink[]; // TODO: probably should be replaced by PlaceholderContainer + private readonly serviceDescriptionLinker: IServiceDescriptionLink[]; // TODO: probably should be replaced by PlaceholderContainer constructor( private readonly authProvidersResource: AuthProvidersResource diff --git a/webapp/packages/core-root/src/ServerConfigResource.ts b/webapp/packages/core-root/src/ServerConfigResource.ts index 67ba2f8262..4dff992653 100644 --- a/webapp/packages/core-root/src/ServerConfigResource.ts +++ b/webapp/packages/core-root/src/ServerConfigResource.ts @@ -15,12 +15,12 @@ import { isArraysEqual } from '@cloudbeaver/core-utils'; import { isNavigatorViewSettingsEqual } from './ConnectionNavigatorViewSettings'; @injectable() -export class ServerConfigResource extends CachedDataResource { +export class ServerConfigResource extends CachedDataResource { update: ServerConfigInput; navigatorSettingsUpdate: NavigatorSettingsInput; constructor( - private graphQLService: GraphQLService + private readonly graphQLService: GraphQLService ) { super(null); @@ -43,6 +43,10 @@ export class ServerConfigResource extends CachedDataResource; + redirectOnFederatedAuth?: Maybe; rootURI: Scalars['String']; serverURL: Scalars['String']; services?: Maybe>>; @@ -2106,7 +2107,7 @@ export type ReadSessionLogQuery = { log: Array<{ time?: any; type: string; messa export type ServerConfigQueryVariables = Exact<{ [key: string]: never }>; -export type ServerConfigQuery = { serverConfig: { name: string; version: string; workspaceId: string; serverURL: string; rootURI: string; hostName: string; productConfiguration: any; supportsCustomConnections?: boolean; supportsConnectionBrowser?: boolean; supportsWorkspaces?: boolean; sessionExpireTime?: number; anonymousAccessEnabled?: boolean; adminCredentialsSaveEnabled?: boolean; publicCredentialsSaveEnabled?: boolean; licenseRequired: boolean; licenseValid: boolean; configurationMode?: boolean; developmentMode?: boolean; enabledFeatures: Array; enabledAuthProviders: Array; supportedLanguages: Array<{ isoCode: string; displayName?: string; nativeName?: string }>; defaultNavigatorSettings: { showSystemObjects: boolean; showUtilityObjects: boolean; showOnlyEntities: boolean; mergeEntities: boolean; hideFolders: boolean; hideSchemas: boolean; hideVirtualModel: boolean }; productInfo: { id: string; version: string; latestVersionInfo?: string; name: string; description?: string; buildTime: string; releaseTime: string; licenseInfo?: string } } }; +export type ServerConfigQuery = { serverConfig: { name: string; version: string; workspaceId: string; serverURL: string; rootURI: string; hostName: string; productConfiguration: any; supportsCustomConnections?: boolean; supportsConnectionBrowser?: boolean; supportsWorkspaces?: boolean; sessionExpireTime?: number; anonymousAccessEnabled?: boolean; adminCredentialsSaveEnabled?: boolean; publicCredentialsSaveEnabled?: boolean; licenseRequired: boolean; licenseValid: boolean; configurationMode?: boolean; developmentMode?: boolean; redirectOnFederatedAuth?: boolean; enabledFeatures: Array; enabledAuthProviders: Array; supportedLanguages: Array<{ isoCode: string; displayName?: string; nativeName?: string }>; defaultNavigatorSettings: { showSystemObjects: boolean; showUtilityObjects: boolean; showOnlyEntities: boolean; mergeEntities: boolean; hideFolders: boolean; hideSchemas: boolean; hideVirtualModel: boolean }; productInfo: { id: string; version: string; latestVersionInfo?: string; name: string; description?: string; buildTime: string; releaseTime: string; licenseInfo?: string } } }; export type SessionPermissionsQueryVariables = Exact<{ [key: string]: never }>; @@ -3338,6 +3339,7 @@ export const ServerConfigDocument = ` licenseValid configurationMode developmentMode + redirectOnFederatedAuth enabledFeatures enabledAuthProviders supportedLanguages { diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index ec89729a6c..bb8ccd7a3e 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -9,9 +9,10 @@ import { AdministrationScreenService } from '@cloudbeaver/core-administration'; import { AppAuthService, AuthInfoService, AuthProviderContext, AuthProviderService, AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID, UserInfoResource } from '@cloudbeaver/core-authentication'; import { injectable, Bootstrap } from '@cloudbeaver/core-di'; +import type { DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; import type { IExecutorHandler } from '@cloudbeaver/core-executor'; -import { SessionDataResource } from '@cloudbeaver/core-root'; +import { ServerConfigResource, SessionDataResource } from '@cloudbeaver/core-root'; import { ScreenService } from '@cloudbeaver/core-routing'; import type { ObjectOrigin } from '@cloudbeaver/core-sdk'; import { openCenteredPopup } from '@cloudbeaver/core-utils'; @@ -23,7 +24,7 @@ export class AuthenticationService extends Bootstrap { configureAuthProvider: (() => void) | null; configureIdentityProvider: (() => void) | null; - private authPromise: Promise | null; + private authPromise: Promise | null; constructor( private readonly screenService: ScreenService, @@ -35,7 +36,8 @@ export class AuthenticationService extends Bootstrap { private readonly authProviderService: AuthProviderService, private readonly authProvidersResource: AuthProvidersResource, private readonly sessionDataResource: SessionDataResource, - private readonly authInfoService: AuthInfoService + private readonly authInfoService: AuthInfoService, + private readonly serverConfigResource: ServerConfigResource, ) { super(); this.authPromise = null; @@ -56,10 +58,12 @@ export class AuthenticationService extends Bootstrap { } async logout(): Promise { - const userAuthConfiguration = this.authInfoService.userAuthConfigurations[0]; + if (this.authInfoService.userAuthConfigurations.length > 0) { + const userAuthConfiguration = this.authInfoService.userAuthConfigurations[0]; - if (userAuthConfiguration?.signOutLink) { - this.logoutConfiguration(userAuthConfiguration.id, true); + if (userAuthConfiguration.signOutLink) { + this.logoutConfiguration(userAuthConfiguration.id, true); + } } try { @@ -103,9 +107,30 @@ export class AuthenticationService extends Bootstrap { if (this.authPromise) { return this.authPromise; } + this.authPromise = this.authDialogService.showLoginForm(persistent, provider, link); + + if (this.serverConfigResource.redirectOnFederatedAuth) { + await this.authProvidersResource.loadAll(); + + const providers = this.authProvidersResource + .getEnabledProviders(); + + if (providers.length === 1) { + const configurableProvider = providers.find(provider => provider.configurable); + + if (configurableProvider?.configurations?.length === 1) { + const user = await this.authInfoService.sso(configurableProvider.id, configurableProvider.configurations[0]); + + if (user) { + this.authDialogService.closeLoginForm(this.authPromise); + } + } + } + } + try { - await this.authPromise; + return this.authPromise; } finally { this.authPromise = null; } @@ -140,7 +165,7 @@ export class AuthenticationService extends Bootstrap { load(): void { } - private requestAuthProviderHandler: IExecutorHandler = async (data, contexts) => { + private readonly requestAuthProviderHandler: IExecutorHandler = async (data, contexts) => { if (data.type === AUTH_PROVIDER_LOCAL_ID) { const provider = contexts.getContext(AuthProviderContext); provider.auth(); diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts index 17685d1f87..20392710c2 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts @@ -7,17 +7,25 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService } from '@cloudbeaver/core-dialogs'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { AuthDialog } from './AuthDialog'; @injectable() export class AuthDialogService { constructor( - private commonDialogService: CommonDialogService + private readonly commonDialogService: CommonDialogService ) { } - async showLoginForm(persistent = false, providerId: string | null = null, link?: boolean): Promise { - await this.commonDialogService.open(AuthDialog, { providerId, link }, { persistent }); + showLoginForm( + persistent = false, + providerId: string | null = null, + link?: boolean + ): Promise { + return this.commonDialogService.open(AuthDialog, { providerId, link }, { persistent }); + } + + closeLoginForm(promise: Promise): void { + this.commonDialogService.rejectDialog(promise); } } From 6e9fff2fd59267b29d42b4db6ee18503270e7af1 Mon Sep 17 00:00:00 2001 From: Wroud Date: Mon, 7 Feb 2022 20:07:41 +0300 Subject: [PATCH 8/8] feat: CB-1629 value validation in renaming dialogue --- .../NodesManager/NavNodeContextMenuService.ts | 1 + .../FormControls/Checkboxes/FieldCheckbox.tsx | 4 +- .../src/FormControls/Checkboxes/Switch.tsx | 3 +- .../core-blocks/src/FormControls/Combobox.tsx | 22 +++++------ .../src/FormControls/FormFieldDescription.tsx | 4 +- .../src/FormControls/InputField.tsx | 11 +++++- .../core-blocks/src/FormControls/Textarea.tsx | 4 +- .../src/FormControls/baseFormControlStyles.ts | 21 ++++++++-- .../src/CommonDialog/RenameDialog.tsx | 38 ++++++++++++++----- .../core-localization/src/locales/en.ts | 1 + .../core-localization/src/locales/it.ts | 1 + .../core-localization/src/locales/ru.ts | 1 + .../core-localization/src/locales/zh.ts | 1 + .../core-theming/src/styles/_theme-dark.scss | 2 + .../core-theming/src/styles/_theme-light.scss | 2 + .../src/SqlEditorBootstrap.ts | 14 ++++--- .../src/SqlEditorTabService.ts | 31 +++++++++++---- 17 files changed, 116 insertions(+), 45 deletions(-) diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts index f03b53fbc5..3193f5da27 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts @@ -103,6 +103,7 @@ export class NavNodeContextMenuService extends Bootstrap { subTitle: name, objectName: node.nodeType || 'Object', icon: node.icon, + validation: name => name.trim().length > 0, }); if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) { diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckbox.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckbox.tsx index 1ea656a8b6..6a58fb321b 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckbox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckbox.tsx @@ -10,7 +10,7 @@ import styled, { css } from 'reshadow'; import { useStyles } from '@cloudbeaver/core-theming'; -import { baseFormControlStyles } from '../baseFormControlStyles'; +import { baseFormControlStyles, baseValidFormControlStyles } from '../baseFormControlStyles'; import { isControlPresented } from '../isControlPresented'; import { Checkbox, CheckboxBaseProps, CheckboxType, ICheckboxControlledProps, ICheckboxObjectProps } from './Checkbox'; @@ -40,7 +40,7 @@ export const FieldCheckbox: CheckboxType = function FieldCheckbox({ className, ...rest }: CheckboxBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps)) { - const styles = useStyles(baseFormControlStyles, style); + const styles = useStyles(baseFormControlStyles, baseValidFormControlStyles, style); if (rest.autoHide && !isControlPresented(rest.name, rest.state)) { return null; diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Switch.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Switch.tsx index 648b17dac0..3c0e75a6fe 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Switch.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Switch.tsx @@ -12,7 +12,7 @@ import styled, { css } from 'reshadow'; import { useStyles, composes } from '@cloudbeaver/core-theming'; import { filterLayoutFakeProps } from '../../Containers/filterLayoutFakeProps'; -import { baseFormControlStyles } from '../baseFormControlStyles'; +import { baseFormControlStyles, baseValidFormControlStyles } from '../baseFormControlStyles'; import { isControlPresented } from '../isControlPresented'; import type { ICheckboxControlledProps, ICheckboxObjectProps } from './Checkbox'; import { useCheckboxState } from './useCheckboxState'; @@ -122,6 +122,7 @@ export const Switch: SwitchType = observer(function Switch({ rest = filterLayoutFakeProps(rest); const styles = useStyles( baseFormControlStyles, + baseValidFormControlStyles, switchStyles, ...mod.map(mod => switchMod[mod]), disabled && switchState.disabled, diff --git a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx index 77f2fd1a8f..f58257b86f 100644 --- a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx @@ -18,7 +18,7 @@ import { filterLayoutFakeProps } from '../Containers/filterLayoutFakeProps'; import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps'; import { Icon } from '../Icon'; import { IconOrImage } from '../IconOrImage'; -import { baseFormControlStyles } from './baseFormControlStyles'; +import { baseFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles'; import { FormContext } from './FormContext'; const styles = composes( @@ -321,7 +321,7 @@ export const Combobox: ComboboxType = observer(function Combobox({ const icon = selectedItem && iconSelector?.(selectedItem); - return styled(useStyles(baseFormControlStyles, styles))( + return styled(useStyles(baseFormControlStyles, baseValidFormControlStyles, styles))( {children && {children}{rest.required && ' *'}} @@ -357,15 +357,15 @@ export const Combobox: ComboboxType = observer(function Combobox({ > {!filteredItems.length ? ( - - {translate('combobox_no_options_placeholder')} - - ) + + {translate('combobox_no_options_placeholder')} + + ) : (filteredItems.map((item, index) => { - const icon = iconSelector?.(item); - const title = titleSelector?.(item); + const icon = iconSelector?.(item); + const title = titleSelector?.(item); - return ( + return ( {valueSelector(item)} - ); - }))} + ); + }))} diff --git a/webapp/packages/core-blocks/src/FormControls/FormFieldDescription.tsx b/webapp/packages/core-blocks/src/FormControls/FormFieldDescription.tsx index 1cbf0b9f1a..73e11d4de9 100644 --- a/webapp/packages/core-blocks/src/FormControls/FormFieldDescription.tsx +++ b/webapp/packages/core-blocks/src/FormControls/FormFieldDescription.tsx @@ -10,7 +10,7 @@ import styled, { css } from 'reshadow'; import { useStyles } from '@cloudbeaver/core-theming'; -import { baseFormControlStyles } from './baseFormControlStyles'; +import { baseFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles'; const style = css` field-label { @@ -31,7 +31,7 @@ export const FormFieldDescription: React.FC = function FormFieldDescripti children, className, }) { - const styles = useStyles(baseFormControlStyles, style); + const styles = useStyles(baseFormControlStyles, baseValidFormControlStyles, style); return styled(styles)( diff --git a/webapp/packages/core-blocks/src/FormControls/InputField.tsx b/webapp/packages/core-blocks/src/FormControls/InputField.tsx index a4e52daed5..4207f3b315 100644 --- a/webapp/packages/core-blocks/src/FormControls/InputField.tsx +++ b/webapp/packages/core-blocks/src/FormControls/InputField.tsx @@ -15,7 +15,7 @@ import { ComponentStyle, composes, useStyles } from '@cloudbeaver/core-theming'; import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps'; import { Icon } from '../Icon'; -import { baseFormControlStyles } from './baseFormControlStyles'; +import { baseFormControlStyles, baseInvalidFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles'; import { FormContext } from './FormContext'; import { isControlPresented } from './isControlPresented'; @@ -61,6 +61,7 @@ const INPUT_FIELD_STYLES = composes( `); type BaseProps = Omit, 'onChange' | 'name' | 'value' | 'style'> & ILayoutSizeProps & { + error?: boolean; description?: string; labelTooltip?: string; mod?: 'surface'; @@ -105,6 +106,7 @@ export const InputField: InputFieldType = observer(function InputField({ mapValue, children, className, + error, description, labelTooltip, mod, @@ -120,7 +122,12 @@ export const InputField: InputFieldType = observer(function InputField({ }: ControlledProps | ObjectProps, ref: React.Ref) { const [passwordRevealed, setPasswordRevealed] = useState(false); const translate = useTranslate(); - const styles = useStyles(baseFormControlStyles, INPUT_FIELD_STYLES, style); + const styles = useStyles( + baseFormControlStyles, + error ? baseInvalidFormControlStyles : baseValidFormControlStyles, + INPUT_FIELD_STYLES, + style + ); const context = useContext(FormContext); const revealPassword = useCallback(() => { diff --git a/webapp/packages/core-blocks/src/FormControls/Textarea.tsx b/webapp/packages/core-blocks/src/FormControls/Textarea.tsx index 000d36d3ed..53d24a3b0c 100644 --- a/webapp/packages/core-blocks/src/FormControls/Textarea.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Textarea.tsx @@ -13,7 +13,7 @@ import styled, { css, use } from 'reshadow'; import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming'; import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps'; -import { baseFormControlStyles } from './baseFormControlStyles'; +import { baseFormControlStyles, baseValidFormControlStyles } from './baseFormControlStyles'; import { FormContext } from './FormContext'; const styles = css` @@ -106,7 +106,7 @@ export const Textarea: TextareaType = observer(function Textarea({ const value = state ? state[name] : controlledValue; - return styled(useStyles(baseFormControlStyles, styles, style))( + return styled(useStyles(baseFormControlStyles, baseValidFormControlStyles, styles, style))( Promise | boolean; } -export const RenameDialog: DialogComponent = function RenameDialog({ +export const RenameDialog: DialogComponent = observer(function RenameDialog({ payload, resolveDialog, rejectDialog, @@ -48,7 +51,20 @@ export const RenameDialog: DialogComponent = functi const { icon, subTitle, bigIcon, viewBox, value, objectName, confirmActionText } = payload; const title = `${translate('ui_rename')} ${objectName}`; - const [name, setName] = useState(value); + const state = useObservableRef(() => ({ + value, + valid: true, + validate: throttleAsync(async () => { + state.valid = (await state.payload.validation?.(state.value)) ?? true; + }, 300), + }), { + value: observable.ref, + valid: observable.ref, + }, { + payload, + }); + + const errorMessage = state.valid ? ' ' : translate('ui_rename_taken_or_invalid'); return styled(useStyles(style, BASE_CONTAINERS_STYLES))( = functi @@ -82,11 +99,14 @@ export const RenameDialog: DialogComponent = functi fixedWidth onReject={rejectDialog} > - resolveDialog(name)}> + resolveDialog(state.value)}> setName(String(value))} + name='value' + state={state} + error={!state.valid} + description={errorMessage} + onChange={() => state.validate()} > {translate('ui_name') + ':'} @@ -94,4 +114,4 @@ export const RenameDialog: DialogComponent = functi ); -}; +}); diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index 9d12629725..24ea01179f 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -1,5 +1,6 @@ export const defaultENLocale = [ ['name', 'en'], + ['ui_rename_taken_or_invalid', 'Name taken or invalid'], ['ui_tag_new', 'New'], ['ui_light_theme', 'Light'], ['ui_dark_theme', 'Dark'], diff --git a/webapp/packages/core-localization/src/locales/it.ts b/webapp/packages/core-localization/src/locales/it.ts index dbf35b6dad..78c474cfc2 100644 --- a/webapp/packages/core-localization/src/locales/it.ts +++ b/webapp/packages/core-localization/src/locales/it.ts @@ -1,5 +1,6 @@ export const defaultITLocale = [ ['name', 'it'], + ['ui_rename_taken_or_invalid', 'Name taken or invalid'], ['ui_tag_new', 'Nuovo'], ['ui_light_theme', 'Light'], ['ui_dark_theme', 'Dark'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index 4a394f4b99..e3f341d185 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -1,5 +1,6 @@ export const defaultRULocale = [ ['name', 'ru'], + ['ui_rename_taken_or_invalid', 'Имя занято или недопустимо'], ['ui_tag_new', 'Новый'], ['ui_light_theme', 'Светлая'], ['ui_dark_theme', 'Темная'], diff --git a/webapp/packages/core-localization/src/locales/zh.ts b/webapp/packages/core-localization/src/locales/zh.ts index dbc7b05495..69c53e91d0 100644 --- a/webapp/packages/core-localization/src/locales/zh.ts +++ b/webapp/packages/core-localization/src/locales/zh.ts @@ -1,5 +1,6 @@ export const defaultZHLocale = [ ['name', 'zh'], + ['ui_rename_taken_or_invalid', 'Name taken or invalid'], ['ui_tag_new', '新建'], ['ui_light_theme', '明亮'], ['ui_dark_theme', '黑暗'], diff --git a/webapp/packages/core-theming/src/styles/_theme-dark.scss b/webapp/packages/core-theming/src/styles/_theme-dark.scss index eaa27b02cf..420a642b69 100644 --- a/webapp/packages/core-theming/src/styles/_theme-dark.scss +++ b/webapp/packages/core-theming/src/styles/_theme-dark.scss @@ -44,6 +44,7 @@ $mdc-theme-text-colors: ( hint: rgba(white, 0.45), disabled: rgba(white, 0.45), icon: rgba(white, 0.45), + negative: $color-negative, ), light: ( primary: rgba(black, 0.87), @@ -51,6 +52,7 @@ $mdc-theme-text-colors: ( hint: rgba(black, 0.45), disabled: rgba(black, 0.2), icon: rgba(black, 0.38), + negative: $color-negative, ), ); diff --git a/webapp/packages/core-theming/src/styles/_theme-light.scss b/webapp/packages/core-theming/src/styles/_theme-light.scss index f97b47146f..62c4179eb1 100644 --- a/webapp/packages/core-theming/src/styles/_theme-light.scss +++ b/webapp/packages/core-theming/src/styles/_theme-light.scss @@ -43,6 +43,7 @@ $mdc-theme-text-colors: ( hint: rgba(white, 0.45), disabled: rgba(white, 0.45), icon: rgba(white, 0.45), + negative: $color-negative, ), dark: ( primary: rgba(black, 0.87), @@ -50,6 +51,7 @@ $mdc-theme-text-colors: ( hint: rgba(black, 0.45), disabled: rgba(black, 0.2), icon: rgba(black, 0.38), + negative: $color-negative, ), ); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts index 710514eab6..8d8777b75c 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts @@ -12,7 +12,7 @@ import { ConnectionSchemaManagerService, isObjectCatalogProvider, isObjectSchemaProvider, DATA_CONTEXT_NAV_NODE, NavigationTabsService } from '@cloudbeaver/core-app'; -import { ConnectionInfoResource, isConnectionProvider } from '@cloudbeaver/core-connections'; +import { isConnectionProvider } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, DialogueStateResult, RenameDialog } from '@cloudbeaver/core-dialogs'; import type { IExecutorHandler } from '@cloudbeaver/core-executor'; @@ -20,12 +20,13 @@ import { ExtensionUtils } from '@cloudbeaver/core-extensions'; import { ISessionAction, sessionActionContext, SessionActionService } from '@cloudbeaver/core-root'; import { ActionService, ACTION_RENAME, DATA_CONTEXT_MENU_NESTED, menuExtractActions, MenuService, ViewService } from '@cloudbeaver/core-view'; import { DATA_CONTEXT_CONNECTION } from '@cloudbeaver/plugin-connections'; -import { DATA_CONTEXT_SQL_EDITOR_STATE, getSqlEditorName } from '@cloudbeaver/plugin-sql-editor'; +import { DATA_CONTEXT_SQL_EDITOR_STATE } from '@cloudbeaver/plugin-sql-editor'; import { ACTION_SQL_EDITOR_OPEN } from './ACTION_SQL_EDITOR_OPEN'; import { DATA_CONTEXT_SQL_EDITOR_TAB } from './DATA_CONTEXT_SQL_EDITOR_TAB'; import { isSessionActionOpenSQLEditor } from './sessionActionOpenSQLEditor'; import { SqlEditorNavigatorService } from './SqlEditorNavigatorService'; +import { SqlEditorTabService } from './SqlEditorTabService'; @injectable() export class SqlEditorBootstrap extends Bootstrap { @@ -39,7 +40,7 @@ export class SqlEditorBootstrap extends Bootstrap { private readonly menuService: MenuService, private readonly sessionActionService: SessionActionService, private readonly commonDialogService: CommonDialogService, - private readonly connectionInfoResource: ConnectionInfoResource + private readonly sqlEditorTabService: SqlEditorTabService ) { super(); } @@ -110,14 +111,17 @@ export class SqlEditorBootstrap extends Bootstrap { switch (action) { case ACTION_RENAME: { const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE); - const connection = this.connectionInfoResource.get(state.executionContext?.connectionId || ''); - const name = getSqlEditorName(state, connection); + const name = this.sqlEditorTabService.getName(state); const result = await this.commonDialogService.open(RenameDialog, { value: name, objectName: name, icon: '/icons/sql_script_m.svg', + validation: name => !this.sqlEditorTabService.sqlEditorTabs.some(tab => ( + tab.handlerState.order !== state.order + && this.sqlEditorTabService.getName(tab.handlerState) === name.trim() + )), }); if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) { diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts index 2af43d967f..ddbade01a6 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts @@ -6,6 +6,8 @@ * you may not use this file except in compliance with the License. */ +import { computed, makeObservable } from 'mobx'; + import { NavigationTabsService, TabHandler, @@ -26,7 +28,7 @@ import { import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { CachedMapAllKey, ResourceKey, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; -import { SqlResultTabsService, ISqlEditorTabState, SqlEditorService } from '@cloudbeaver/plugin-sql-editor'; +import { SqlResultTabsService, ISqlEditorTabState, SqlEditorService, getSqlEditorName } from '@cloudbeaver/plugin-sql-editor'; import { SqlEditorPanel } from './SqlEditorPanel'; import { SqlEditorTab } from './SqlEditorTab'; @@ -34,6 +36,10 @@ import { sqlEditorTabHandlerKey } from './sqlEditorTabHandlerKey'; @injectable() export class SqlEditorTabService extends Bootstrap { + get sqlEditorTabs(): ITab[] { + return Array.from(this.navigationTabsService.findTabs(isSQLEditorTab)); + } + readonly tabHandler: TabHandler; constructor( @@ -43,7 +49,7 @@ export class SqlEditorTabService extends Bootstrap { private readonly sqlResultTabsService: SqlResultTabsService, private readonly connectionExecutionContextService: ConnectionExecutionContextService, private readonly connectionExecutionContextResource: ConnectionExecutionContextResource, - private readonly connectionInfo: ConnectionInfoResource, + private readonly connectionInfoResource: ConnectionInfoResource, ) { super(); @@ -64,6 +70,10 @@ export class SqlEditorTabService extends Bootstrap { objectSchemaSetter(this.setObjectSchemaId.bind(this)), ], }); + + makeObservable(this, { + sqlEditorTabs: computed, + }); } register(): void { @@ -73,6 +83,12 @@ export class SqlEditorTabService extends Bootstrap { load(): void {} + getName(tabState: ISqlEditorTabState): string { + const connection = this.connectionInfoResource.get(tabState.executionContext?.connectionId || ''); + + return getSqlEditorName(tabState, connection); + } + createNewEditor( name?: string, connectionId?: string, @@ -99,7 +115,7 @@ export class SqlEditorTabService extends Bootstrap { const executionContext = this.connectionExecutionContextService.get(tab.handlerState.executionContext!.id); if (!executionContext?.context) { - if (!this.connectionInfo.has(tab.handlerState.executionContext?.connectionId || '')) { + if (!this.connectionInfoResource.has(tab.handlerState.executionContext?.connectionId || '')) { this.resetConnectionInfo(tab.handlerState); } } else { @@ -116,7 +132,7 @@ export class SqlEditorTabService extends Bootstrap { for (const tab of tabs) { if ( ResourceKeyUtils.includes(key, tab.handlerState.executionContext!.id) - && !this.connectionInfo.has(tab.handlerState.executionContext!.connectionId) + && !this.connectionInfoResource.has(tab.handlerState.executionContext!.connectionId) ) { this.resetConnectionInfo(tab.handlerState); } @@ -124,8 +140,7 @@ export class SqlEditorTabService extends Bootstrap { } private getFreeEditorId() { - const editorTabs = this.navigationTabsService.findTabs(isSQLEditorTab); - const ordered = Array.from(editorTabs).map(tab => tab.handlerState.order); + const ordered = this.sqlEditorTabs.map(tab => tab.handlerState.order); return findMinimalFree(ordered, 1); } @@ -150,9 +165,9 @@ export class SqlEditorTabService extends Bootstrap { } if (tab.handlerState.executionContext) { - await this.connectionInfo.load(CachedMapAllKey); + await this.connectionInfoResource.load(CachedMapAllKey); - if (!this.connectionInfo.has(tab.handlerState.executionContext.connectionId)) { + if (!this.connectionInfoResource.has(tab.handlerState.executionContext.connectionId)) { this.resetConnectionInfo(tab.handlerState); } }