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 514bb0238b.

* 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 <iamsergeyteleshev@gmail.com>
Co-authored-by: Alexey Potsetsuev <wrouds@gmail.com>
Co-authored-by: Daria Marutkina <125263541+dariamarutkina@users.noreply.github.com>
This commit is contained in:
Ainur
2025-08-12 12:03:21 +02:00
committed by GitHub
co-authored by sergeyteleshev Alexey Potsetsuev Daria Marutkina
parent ddf94efef0
commit fea2d78ef2
3 changed files with 105 additions and 53 deletions
@@ -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<? extends DBPDataSourceFolder> getAllFolders() {
return dataSourceRegistry.getAllFolders();
if (webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) {
return dataSourceRegistry.getAllFolders();
}
Set<DBPDataSourceFolder> 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<? extends DBPDataSourceFolder> 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
@@ -50,6 +50,7 @@ public class WebSessionGlobalProjectImpl extends WebSessionProjectImpl {
@Override
protected DBPDataSourceRegistry createDataSourceRegistry() {
return new WebGlobalProjectRegistryProxy(
webSession,
createRegistryWithCredentialsProvider(),
this::isDataSourceAccessible
);
@@ -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<IConnectionInfoParams>) {
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<IConnectionInfoParams>) {
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) {