From fea2d78ef255f37bb79013c49ea2dfb24de2b78a Mon Sep 17 00:00:00 2001 From: Ainur <59531286+yagudin10@users.noreply.github.com> Date: Tue, 12 Aug 2025 13:03:21 +0300 Subject: [PATCH] dbeaver/cloudbeaver#3495 filter empty folder in global project (#3636) * dbeaver/cloudbeaver#3495 filter empty folder in global project * fixes loading nested folder-nodes during connection creation * Revert "fixes loading nested folder-nodes during connection creation" This reverts commit 514bb0238b3db56fa7c1a8e3c40cbd232c086468. * uses more correct solution for preloading folders * build fix * cleanup * fixes deletion of the connections so it removes empty folders * fix: outdate only one node --------- Co-authored-by: sergeyteleshev Co-authored-by: Alexey Potsetsuev Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com> --- .../WebGlobalProjectRegistryProxy.java | 35 ++++- .../WebSessionGlobalProjectImpl.java | 1 + .../src/NavNodes/ConnectionNavNodeService.ts | 122 +++++++++++------- 3 files changed, 105 insertions(+), 53 deletions(-) diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebGlobalProjectRegistryProxy.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebGlobalProjectRegistryProxy.java index bf7584ba78..1c7fcf5cc5 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebGlobalProjectRegistryProxy.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebGlobalProjectRegistryProxy.java @@ -17,6 +17,7 @@ package io.cloudbeaver; +import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; @@ -37,9 +38,7 @@ import org.jkiss.dbeaver.registry.DataSourceConfigurationManager; import org.jkiss.dbeaver.registry.DataSourcePersistentRegistry; import org.jkiss.dbeaver.registry.DataSourceRegistry; -import java.util.Collection; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -48,12 +47,19 @@ import java.util.stream.Collectors; * We need to filter some data sources in case of inaccessibility (not enough permissions). */ public class WebGlobalProjectRegistryProxy implements DBPDataSourceRegistry, DataSourcePersistentRegistry, DBPDataSourceRegistryCache { + @NotNull + private final WebSession webSession; @NotNull private final DataSourceFilter dataSourceFilter; @NotNull private final DataSourceRegistry dataSourceRegistry; - public WebGlobalProjectRegistryProxy(@NotNull DataSourceRegistry dataSourceRegistry, @NotNull DataSourceFilter filter) { + public WebGlobalProjectRegistryProxy( + @NotNull WebSession webSession, + @NotNull DataSourceRegistry dataSourceRegistry, + @NotNull DataSourceFilter filter + ) { + this.webSession = webSession; this.dataSourceRegistry = dataSourceRegistry; this.dataSourceFilter = filter; } @@ -181,13 +187,30 @@ public class WebGlobalProjectRegistryProxy implements DBPDataSourceRegistry, Dat @NotNull @Override public List getAllFolders() { - return dataSourceRegistry.getAllFolders(); + if (webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) { + return dataSourceRegistry.getAllFolders(); + } + Set set = new LinkedHashSet<>(); + for (DBPDataSourceContainer container : getDataSources()) { + DBPDataSourceFolder folder = container.getFolder(); + while (folder != null) { + set.add(folder); + folder = folder.getParent(); + } + } + return new ArrayList<>(set); } @NotNull @Override public List getRootFolders() { - return dataSourceRegistry.getRootFolders(); + if (webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) { + return dataSourceRegistry.getRootFolders(); + } + return getDataSources().stream() + .map(DBPDataSourceContainer::getFolder) + .filter(folder -> folder != null && folder.getParent() == null) + .toList(); } @NotNull diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionGlobalProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionGlobalProjectImpl.java index 403e8f4178..f8185624a7 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionGlobalProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebSessionGlobalProjectImpl.java @@ -50,6 +50,7 @@ public class WebSessionGlobalProjectImpl extends WebSessionProjectImpl { @Override protected DBPDataSourceRegistry createDataSourceRegistry() { return new WebGlobalProjectRegistryProxy( + webSession, createRegistryWithCredentialsProvider(), this::isDataSourceAccessible ); diff --git a/webapp/packages/plugin-connections/src/NavNodes/ConnectionNavNodeService.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionNavNodeService.ts index f78251d7cd..d58da70f7e 100644 --- a/webapp/packages/plugin-connections/src/NavNodes/ConnectionNavNodeService.ts +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionNavNodeService.ts @@ -5,7 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, makeObservable } from 'mobx'; +import { action, makeObservable, runInAction } from 'mobx'; import { Dependency, injectable } from '@cloudbeaver/core-di'; import { ExecutorInterrupter, type IAsyncContextLoader, type IExecutionContextProvider } from '@cloudbeaver/core-executor'; @@ -136,65 +136,80 @@ export class ConnectionNavNodeService extends Dependency { } private connectionUpdateHandler(key: ResourceKey) { - let connectionInfos = this.connectionInfoResource.get(key); - const outdatedTrees: string[] = []; - const closedConnections: string[] = []; + runInAction(() => { + let connectionInfos = this.connectionInfoResource.get(key); + const outdatedTrees: string[] = []; + const closedConnections: string[] = []; - connectionInfos = Array.isArray(connectionInfos) ? connectionInfos : [connectionInfos]; - for (const connectionInfo of connectionInfos) { - if (!connectionInfo?.nodePath) { - return; + connectionInfos = Array.isArray(connectionInfos) ? connectionInfos : [connectionInfos]; + for (const connectionInfo of connectionInfos) { + if (!connectionInfo?.nodePath) { + return; + } + + const node = this.navNodeInfoResource.get(connectionInfo.nodePath); + const parentId = getConnectionParentId(connectionInfo.projectId, connectionInfo.folder); // new parent + + if (!connectionInfo.connected) { + closedConnections.push(connectionInfo.nodePath); + outdatedTrees.push(connectionInfo.nodePath); + } + + const folderId = node?.parentId; // current parent + + if (folderId && !outdatedTrees.includes(folderId)) { + outdatedTrees.push(folderId); + } + + if (!outdatedTrees.includes(parentId)) { + outdatedTrees.push(parentId); + } } - const node = this.navNodeInfoResource.get(connectionInfo.nodePath); - const parentId = getConnectionParentId(connectionInfo.projectId, connectionInfo.folder); // new parent + if (closedConnections.length > 0) { + const key = resourceKeyList(closedConnections); - if (!connectionInfo.connected) { - closedConnections.push(connectionInfo.nodePath); - outdatedTrees.push(connectionInfo.nodePath); + if (this.navTreeResource.has(key)) { + this.navTreeResource.delete(key); + } } - const folderId = node?.parentId; // current parent - - if (folderId && !outdatedTrees.includes(folderId)) { - outdatedTrees.push(folderId); + if (outdatedTrees.length > 0) { + const key = resourceKeyList(outdatedTrees); + this.navTreeResource.markOutdated(key); } - - if (!outdatedTrees.includes(parentId)) { - outdatedTrees.push(parentId); - } - } - - if (closedConnections.length > 0) { - const key = resourceKeyList(closedConnections); - - if (this.navTreeResource.has(key)) { - this.navTreeResource.delete(key); - } - } - - if (outdatedTrees.length > 0) { - const key = resourceKeyList(outdatedTrees); - this.navTreeResource.markOutdated(key); - } + }); } private connectionRemoveHandler(key: ResourceKeySimple) { - ResourceKeyUtils.forEach(key, key => { - const connectionInfo = this.connectionInfoResource.get(key); + runInAction(() => { + ResourceKeyUtils.forEach(key, key => { + const connectionInfo = this.connectionInfoResource.get(key); - if (!connectionInfo) { - return; - } + if (!connectionInfo) { + return; + } - const nodePath = connectionInfo.nodePath ?? NodeManagerUtils.connectionIdToConnectionNodeId(key.connectionId); + const nodePath = connectionInfo.nodePath ?? NodeManagerUtils.connectionIdToConnectionNodeId(key.connectionId); - const node = this.navNodeInfoResource.get(nodePath); - const folder = node?.parentId ?? getProjectNodeId(key.projectId); + const node = this.navNodeInfoResource.get(nodePath); + const folder = node?.parentId ?? getProjectNodeId(key.projectId); + const parents = this.navNodeInfoResource.getParents(folder); - if (nodePath) { - this.navTreeResource.deleteInNode(folder, [nodePath]); - } + for (let i = parents.length - 1; i >= 0; i--) { + const parent = parents[i]!; + const children = this.navTreeResource.get(parent) ?? []; + + if (children.length > 1) { + this.navTreeResource.markOutdated(parent); + break; + } + } + + if (nodePath) { + this.navTreeResource.deleteInNode(folder, [nodePath]); + } + }); }); } @@ -212,7 +227,20 @@ export class ConnectionNavNodeService extends Dependency { await this.navNodeInfoResource.loadNodeParents(parentId); const parents = this.navNodeInfoResource.getParents(parentId); - this.navTreeResource.markOutdated(parents[parents.length - 1]); + // allows to preload missing folders if the did not load + for (let i = 0; i < parents.length; i++) { + const parent = parents[i]!; + const nextParent = parents[i + 1]; + const children = this.navTreeResource.get(parent) ?? []; + const isEmptyRoot = parents.length <= 2 && !children.length; // for case ['', 'g_GlobalConfiguration'] + const hasNoChildren = nextParent && !children.includes(nextParent); + + if (isEmptyRoot || hasNoChildren) { + this.navTreeResource.markOutdated(parent); + break; + } + } + const preloaded = await this.navTreeResource.preloadNodeParents(parents, parentId); if (!preloaded) {