Merge remote-tracking branch 'origin/devel' into tech/CB-2229

This commit is contained in:
Konstantin Vasilev
2022-08-31 17:51:39 +03:00
52 changed files with 806 additions and 181 deletions
@@ -20,6 +20,7 @@ import io.cloudbeaver.model.rm.RMUtils;
import org.eclipse.core.resources.IProject;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
import org.jkiss.dbeaver.model.auth.SMSessionContext;
import org.jkiss.dbeaver.model.rm.RMProject;
import org.jkiss.dbeaver.registry.BaseProjectImpl;
@@ -55,7 +56,7 @@ public class VirtualProjectImpl extends BaseProjectImpl {
@NotNull
@Override
public String getName() {
return project.getName();
return project.getDisplayName();
}
@NotNull
@@ -93,7 +94,7 @@ public class VirtualProjectImpl extends BaseProjectImpl {
@NotNull
@Override
protected DataSourceRegistry createDataSourceRegistry() {
return new WebDataSourceRegistryProxy(super.createDataSourceRegistry(), dataSourceFilter);
protected DBPDataSourceRegistry createDataSourceRegistry() {
return new WebDataSourceRegistryProxy(new DataSourceRegistry(this), dataSourceFilter);
}
}
@@ -7,30 +7,40 @@ import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.*;
import org.jkiss.dbeaver.model.access.DBAAuthProfile;
import org.jkiss.dbeaver.model.access.DBACredentialsProvider;
import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
import org.jkiss.dbeaver.model.app.DBPProject;
import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration;
import org.jkiss.dbeaver.model.connection.DBPDriver;
import org.jkiss.dbeaver.model.net.DBWNetworkProfile;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.struct.DBSObjectFilter;
import org.jkiss.dbeaver.registry.DataSourceDescriptor;
import org.jkiss.dbeaver.registry.DataSourceFolder;
import org.jkiss.dbeaver.registry.DataSourceConfigurationManager;
import org.jkiss.dbeaver.registry.DataSourcePersistentRegistry;
import org.jkiss.dbeaver.registry.DataSourceRegistry;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
public class WebDataSourceRegistryProxy extends DataSourceRegistry {
public class WebDataSourceRegistryProxy implements DBPDataSourceRegistry, DataSourcePersistentRegistry {
private final DataSourceFilter dataSourceFilter;
private final DataSourceRegistry dataSourceRegistry;
public WebDataSourceRegistryProxy(DataSourceRegistry dataSourceRegistry, DataSourceFilter filter) {
super(dataSourceRegistry.getProject());
this.dataSourceRegistry = dataSourceRegistry;
this.dataSourceFilter = filter;
}
@Override
public DBPProject getProject() {
return dataSourceRegistry.getProject();
}
@Nullable
@Override
public DataSourceDescriptor getDataSource(String id) {
DataSourceDescriptor dataSource = super.getDataSource(id);
public DBPDataSourceContainer getDataSource(String id) {
DBPDataSourceContainer dataSource = dataSourceRegistry.getDataSource(id);
if (dataSourceFilter != null && !dataSourceFilter.filter(dataSource)) {
return null;
}
@@ -39,17 +49,17 @@ public class WebDataSourceRegistryProxy extends DataSourceRegistry {
@Nullable
@Override
public DataSourceDescriptor getDataSource(DBPDataSource dataSource) {
public DBPDataSourceContainer getDataSource(DBPDataSource dataSource) {
if (dataSourceFilter != null && !dataSourceFilter.filter(dataSource.getContainer())) {
return null;
}
return super.getDataSource(dataSource);
return dataSourceRegistry.getDataSource(dataSource);
}
@Nullable
@Override
public DataSourceDescriptor findDataSourceByName(String name) {
var dataSource = super.findDataSourceByName(name);
public DBPDataSourceContainer findDataSourceByName(String name) {
var dataSource = dataSourceRegistry.findDataSourceByName(name);
if (dataSource != null) {
if (dataSourceFilter == null || dataSourceFilter.filter(dataSource)) {
return dataSource;
@@ -61,7 +71,7 @@ public class WebDataSourceRegistryProxy extends DataSourceRegistry {
@NotNull
@Override
public List<? extends DBPDataSourceContainer> getDataSourcesByProfile(@NotNull DBWNetworkProfile profile) {
return super.getDataSourcesByProfile(profile)
return dataSourceRegistry.getDataSourcesByProfile(profile)
.stream()
.filter(dataSourceFilter::filter)
.collect(Collectors.toList());
@@ -69,10 +79,233 @@ public class WebDataSourceRegistryProxy extends DataSourceRegistry {
@NotNull
@Override
public List<DataSourceDescriptor> getDataSources() {
return super.getDataSources()
public List<DBPDataSourceContainer> getDataSources() {
return dataSourceRegistry.getDataSources()
.stream()
.filter(dataSourceFilter::filter)
.collect(Collectors.toList());
}
@NotNull
@Override
public DBPDataSourceContainer createDataSource(DBPDriver driver, DBPConnectionConfiguration connConfig) {
return dataSourceRegistry.createDataSource(driver, connConfig);
}
@NotNull
@Override
public DBPDataSourceContainer createDataSource(DBPDataSourceContainer source) {
return dataSourceRegistry.createDataSource(source);
}
@Override
public void addDataSourceListener(@NotNull DBPEventListener listener) {
dataSourceRegistry.addDataSourceListener(listener);
}
@Override
public boolean removeDataSourceListener(@NotNull DBPEventListener listener) {
return dataSourceRegistry.removeDataSourceListener(listener);
}
@Override
public void addDataSource(@NotNull DBPDataSourceContainer dataSource) {
dataSourceRegistry.addDataSource(dataSource);
}
@Override
public void removeDataSource(@NotNull DBPDataSourceContainer dataSource) {
dataSourceRegistry.removeDataSource(dataSource);
}
@Override
public void updateDataSource(@NotNull DBPDataSourceContainer dataSource) {
dataSourceRegistry.updateDataSource(dataSource);
}
@NotNull
@Override
public List<? extends DBPDataSourceFolder> getAllFolders() {
return dataSourceRegistry.getAllFolders();
}
@NotNull
@Override
public List<? extends DBPDataSourceFolder> getRootFolders() {
return dataSourceRegistry.getRootFolders();
}
@Override
public DBPDataSourceFolder getFolder(String path) {
return dataSourceRegistry.getFolder(path);
}
@Override
public DBPDataSourceFolder addFolder(DBPDataSourceFolder parent, String name) {
return dataSourceRegistry.addFolder(parent, name);
}
@Override
public void removeFolder(DBPDataSourceFolder folder, boolean dropContents) {
dataSourceRegistry.removeFolder(folder, dropContents);
}
@Nullable
@Override
public DBSObjectFilter getSavedFilter(String name) {
return dataSourceRegistry.getSavedFilter(name);
}
@NotNull
@Override
public List<DBSObjectFilter> getSavedFilters() {
return dataSourceRegistry.getSavedFilters();
}
@Override
public void updateSavedFilter(DBSObjectFilter filter) {
dataSourceRegistry.updateSavedFilter(filter);
}
@Override
public void removeSavedFilter(String filterName) {
dataSourceRegistry.removeSavedFilter(filterName);
}
@Nullable
@Override
public DBWNetworkProfile getNetworkProfile(String name) {
return dataSourceRegistry.getNetworkProfile(name);
}
@NotNull
@Override
public List<DBWNetworkProfile> getNetworkProfiles() {
return dataSourceRegistry.getNetworkProfiles();
}
@Override
public void updateNetworkProfile(DBWNetworkProfile profile) {
dataSourceRegistry.updateNetworkProfile(profile);
}
@Override
public void removeNetworkProfile(DBWNetworkProfile profile) {
dataSourceRegistry.removeNetworkProfile(profile);
}
@Nullable
@Override
public DBAAuthProfile getAuthProfile(String id) {
return dataSourceRegistry.getAuthProfile(id);
}
@NotNull
@Override
public List<DBAAuthProfile> getAllAuthProfiles() {
return dataSourceRegistry.getAllAuthProfiles();
}
@NotNull
@Override
public List<DBAAuthProfile> getApplicableAuthProfiles(@Nullable DBPDriver driver) {
return dataSourceRegistry.getApplicableAuthProfiles(driver);
}
@Override
public void updateAuthProfile(DBAAuthProfile profile) {
dataSourceRegistry.updateAuthProfile(profile);
}
@Override
public void removeAuthProfile(DBAAuthProfile profile) {
dataSourceRegistry.removeAuthProfile(profile);
}
@Override
public void flushConfig() {
dataSourceRegistry.flushConfig();
}
@Override
public void refreshConfig() {
dataSourceRegistry.refreshConfig();
}
@Override
public Throwable getLastError() {
return dataSourceRegistry.getLastError();
}
@Override
public boolean hasError() {
return dataSourceRegistry.hasError();
}
@Override
public void checkForErrors() throws DBException {
dataSourceRegistry.checkForErrors();
}
@Override
public void notifyDataSourceListeners(DBPEvent event) {
dataSourceRegistry.notifyDataSourceListeners(event);
}
@NotNull
@Override
public ISecurePreferences getSecurePreferences() {
return dataSourceRegistry.getSecurePreferences();
}
@Nullable
@Override
public DBACredentialsProvider getAuthCredentialsProvider() {
return dataSourceRegistry.getAuthCredentialsProvider();
}
@Override
public void dispose() {
dataSourceRegistry.dispose();
}
@Override
public void setAuthCredentialsProvider(DBACredentialsProvider authCredentialsProvider) {
dataSourceRegistry.setAuthCredentialsProvider(authCredentialsProvider);
}
@Override
public Set<DBPDataSourceFolder> getTemporaryFolders() {
return dataSourceRegistry.getTemporaryFolders();
}
@Override
public void loadDataSources(
@NotNull List<DBPDataSourceConfigurationStorage> storages,
@NotNull DataSourceConfigurationManager manager,
boolean refresh,
boolean purgeUntouched
) {
dataSourceRegistry.loadDataSources(storages, manager, refresh, purgeUntouched);
}
@Override
public void saveDataSources() {
dataSourceRegistry.saveDataSources();
}
@Override
public DataSourceConfigurationManager getConfigurationManager() {
return dataSourceRegistry.getConfigurationManager();
}
@Override
public void saveConfigurationToManager(
@NotNull DBRProgressMonitor monitor,
@NotNull DataSourceConfigurationManager configurationManager,
@Nullable Predicate<DBPDataSourceContainer> filter
) {
dataSourceRegistry.saveConfigurationToManager(monitor, configurationManager, filter);
}
}
@@ -22,6 +22,7 @@ import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.app.DBPProject;
import org.jkiss.dbeaver.model.auth.*;
import java.time.LocalDateTime;
import java.util.Map;
public abstract class AbstractSessionFederated implements SMSessionFederated {
@@ -56,6 +57,12 @@ public abstract class AbstractSessionFederated implements SMSessionFederated {
return parentSession.getSessionPrincipal();
}
@NotNull
@Override
public LocalDateTime getSessionStart() {
return parentSession.getSessionStart();
}
@Override
public boolean isApplicationSession() {
return false;
@@ -27,6 +27,7 @@ import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.DBPDataSourceConfigurationStorage;
import org.jkiss.dbeaver.model.DBPDataSourceContainer;
import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
import org.jkiss.dbeaver.model.app.DBPProject;
import org.jkiss.dbeaver.model.auth.SMCredentials;
@@ -259,7 +260,7 @@ public class LocalResourceController implements RMController {
DBPDataSourceRegistry registry = projectMetadata.getDataSourceRegistry();
registry.checkForErrors();
DataSourceConfigurationManagerBuffer buffer = new DataSourceConfigurationManagerBuffer();
((DataSourceRegistry)registry).saveConfigurationToManager(new VoidProgressMonitor(), buffer, null);
((DataSourcePersistentRegistry) registry).saveConfigurationToManager(new VoidProgressMonitor(), buffer, null);
registry.checkForErrors();
return new String(buffer.getData(), StandardCharsets.UTF_8);
}
@@ -267,22 +268,22 @@ public class LocalResourceController implements RMController {
@Override
public void saveProjectDataSources(@NotNull String projectId, @NotNull String configuration) throws DBException {
final DBPProject project = getProjectMetadata(projectId);
final DataSourceRegistry registry = (DataSourceRegistry) project.getDataSourceRegistry();
final DBPDataSourceRegistry registry = project.getDataSourceRegistry();
final DBPDataSourceConfigurationStorage storage = new DataSourceMemoryStorage(configuration.getBytes(StandardCharsets.UTF_8));
final DataSourceConfigurationManager manager = new DataSourceConfigurationManagerBuffer();
registry.loadDataSources(List.of(storage), manager, true, false);
((DataSourcePersistentRegistry) registry).loadDataSources(List.of(storage), manager, true, false);
registry.checkForErrors();
registry.saveDataSources();
((DataSourcePersistentRegistry) registry).saveDataSources();
registry.checkForErrors();
}
@Override
public void deleteProjectDataSources(@NotNull String projectId, @NotNull String[] dataSourceIds) throws DBException {
final DBPProject project = getProjectMetadata(projectId);
final DataSourceRegistry registry = (DataSourceRegistry) project.getDataSourceRegistry();
final DBPDataSourceRegistry registry = project.getDataSourceRegistry();
for (String dataSourceId : dataSourceIds) {
final DataSourceDescriptor dataSource = registry.getDataSource(dataSourceId);
final DBPDataSourceContainer dataSource = registry.getDataSource(dataSourceId);
if (dataSource != null) {
registry.removeDataSource(dataSource);
@@ -16,10 +16,7 @@
*/
package io.cloudbeaver.model.session;
import io.cloudbeaver.DBWConstants;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.DataSourceFilter;
import io.cloudbeaver.VirtualProjectImpl;
import io.cloudbeaver.*;
import io.cloudbeaver.model.WebAsyncTaskInfo;
import io.cloudbeaver.model.WebConnectionInfo;
import io.cloudbeaver.model.WebServerMessage;
@@ -70,6 +67,9 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.lang.reflect.InvocationTargetException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Function;
@@ -160,6 +160,12 @@ public class WebSession extends AbstractSessionPersistent implements SMSession,
return id;
}
@NotNull
@Override
public LocalDateTime getSessionStart() {
return LocalDateTime.ofInstant(Instant.ofEpochMilli(createTime), ZoneId.systemDefault());
}
public WebApplication getApplication() {
return application;
}
@@ -357,7 +363,7 @@ public class WebSession extends AbstractSessionPersistent implements SMSession,
this,
filter);
DBPDataSourceRegistry dataSourceRegistry = sessionProject.getDataSourceRegistry();
((DataSourceRegistry) dataSourceRegistry).setAuthCredentialsProvider(this);
dataSourceRegistry.setAuthCredentialsProvider(this);
addSessionProject(sessionProject);
if (!project.isShared() || application.isConfigurationMode()) {
this.defaultProject = sessionProject;
@@ -21,6 +21,10 @@ import io.cloudbeaver.model.WebConnectionFolderInfo;
import io.cloudbeaver.model.session.WebSession;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.DBPDataSourceFolder;
import org.jkiss.dbeaver.model.navigator.DBNLocalFolder;
import org.jkiss.dbeaver.model.navigator.DBNNode;
import org.jkiss.dbeaver.model.navigator.DBNProject;
import org.jkiss.dbeaver.model.navigator.DBNRoot;
import org.jkiss.utils.CommonUtils;
public class WebConnectionFolderUtils {
@@ -44,4 +48,14 @@ public class WebConnectionFolderUtils {
throw new DBWebException("Folder name '" + folderName + "' contains illegal characters: /");
}
}
public static DBPDataSourceFolder getParentFolder(DBNNode folderNode) throws DBWebException {
if (folderNode instanceof DBNRoot || folderNode instanceof DBNProject) {
return null;
} else if (folderNode instanceof DBNLocalFolder) {
return ((DBNLocalFolder) folderNode).getFolder();
} else {
throw new DBWebException("Navigator node '" + folderNode.getNodeItemPath() + "' is not a folder node");
}
}
}
@@ -29,6 +29,7 @@ import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class WebTestUtils {
@@ -42,12 +43,24 @@ public class WebTestUtils {
public static Map<String, Object> doPost(String apiUrl, String input, HttpClient client) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
return doPostWithHeaders(apiUrl, input, client, List.of());
}
public static Map<String, Object> doPostWithHeaders(
String apiUrl,
String input,
HttpClient client,
List<String> headers
) throws Exception {
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.uri(URI.create(apiUrl))
.POST(HttpRequest.BodyPublishers.ofString(input))
.header("Content-Type", "application/json")
.build();
.header("Content-Type", "application/json");
if (!headers.isEmpty()) {
requestBuilder.headers(headers.toArray(String[]::new));
}
HttpRequest request = requestBuilder.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
@@ -198,6 +198,7 @@ type DriverInfo {
embedded: Boolean
enabled: Boolean!
requiresServerName: Boolean
requiresDatabaseName: Boolean
licenseRequired: Boolean
license: String
@@ -112,7 +112,7 @@ extend type Query {
limit: Int,
onlyFolders: Boolean): [ NavigatorNodeInfo! ]!
# Get child nodes
# Get node's parents
navNodeParents( nodePath: ID! ): [ NavigatorNodeInfo! ]!
navNodeInfo( nodePath: ID! ): NavigatorNodeInfo!
@@ -34,6 +34,7 @@ import org.jkiss.dbeaver.model.DBPDataSourceContainer;
import org.jkiss.dbeaver.model.DBPDataSourceFolder;
import org.jkiss.dbeaver.model.access.DBAAuthCredentials;
import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry;
import org.jkiss.dbeaver.model.app.DBPProject;
import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration;
import org.jkiss.dbeaver.model.connection.DBPDriver;
import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNativeCredentials;
@@ -42,7 +43,6 @@ import org.jkiss.dbeaver.model.navigator.DBNModel;
import org.jkiss.dbeaver.model.navigator.DBNProject;
import org.jkiss.dbeaver.model.net.DBWHandlerConfiguration;
import org.jkiss.dbeaver.model.net.ssh.SSHConstants;
import org.jkiss.dbeaver.model.rm.RMProject;
import org.jkiss.dbeaver.registry.DataSourceDescriptor;
import org.jkiss.dbeaver.registry.DataSourceNavigatorSettings;
import org.jkiss.dbeaver.registry.DataSourceProviderDescriptor;
@@ -169,7 +169,6 @@ public class WebServiceUtils extends WebCommonUtils {
if (config.getServerName() != null) {
dsConfig.setServerName(config.getServerName());
}
dsConfig.setUrl(driver.getConnectionURL(dsConfig));
}
if (config.getProperties() != null) {
Map<String, String> newProps = new LinkedHashMap<>();
@@ -197,6 +196,9 @@ public class WebServiceUtils extends WebCommonUtils {
if (config.getConfigurationType() != null) {
dsConfig.setConfigurationType(config.getConfigurationType());
}
if (CommonUtils.isEmpty(config.getUrl())) {
dsConfig.setUrl(driver.getConnectionURL(dsConfig));
}
// Save network handlers
if (config.getNetworkHandlersConfig() != null) {
for (WebNetworkHandlerConfigInput nhc : config.getNetworkHandlersConfig()) {
@@ -340,6 +342,9 @@ public class WebServiceUtils extends WebCommonUtils {
DBNModel.updateConfigAndRefreshDatabases(projectNode.getDatabases());
}
public static boolean isGlobalProject(DBPProject project) {
return CommonUtils.equalObjects(CBApplication.getInstance().getDefaultProjectName(), project.getName());
}
}
@@ -44,6 +44,7 @@ import java.util.Map;
public class WebDatabaseDriverConfig {
public static final String URL_SERVER_FIELD = "{server}";
public static final String URL_DATABASE_FIELD = ".*(?:\\{(?:database|file|folder)}).*";
private final WebSession webSession;
private final DBPDriver driver;
private String id;
@@ -249,4 +250,13 @@ public class WebDatabaseDriverConfig {
public DBPDriverConfigurationType[] getConfigurationTypes() {
return driver.getSupportedConfigurationTypes().toArray(DBPDriverConfigurationType[]::new);
}
@Property
public boolean getRequiresDatabaseName() {
if (driver.getSampleURL() == null) {
return false;
}
return driver.getSampleURL().matches(URL_DATABASE_FIELD);
}
}
@@ -35,7 +35,9 @@ import java.util.Map;
* Application configuration
*/
public class CBAppConfig extends BaseAuthWebAppConfiguration implements WebAuthConfiguration {
public static final DataSourceNavigatorSettings DEFAULT_VIEW_SETTINGS = DataSourceNavigatorSettings.PRESET_FULL.getSettings();
public static final DataSourceNavigatorSettings.Preset PRESET_WEB = new DataSourceNavigatorSettings.Preset("web", "Web", "Default view");
public static final DataSourceNavigatorSettings DEFAULT_VIEW_SETTINGS = PRESET_WEB.getSettings();
private boolean supportsCustomConnections;
private boolean supportsConnectionBrowser;
@@ -21,17 +21,14 @@ import io.cloudbeaver.DBWConstants;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.WebServiceUtils;
import io.cloudbeaver.model.*;
import io.cloudbeaver.model.app.BaseWebApplication;
import io.cloudbeaver.model.app.WebAppConfiguration;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.registry.WebHandlerRegistry;
import io.cloudbeaver.registry.WebSessionHandlerDescriptor;
import io.cloudbeaver.server.CBAppConfig;
import io.cloudbeaver.server.CBApplication;
import io.cloudbeaver.server.CBPlatform;
import io.cloudbeaver.service.core.DBWServiceCore;
import io.cloudbeaver.utils.WebDataSourceUtils;
import io.cloudbeaver.utils.WebConnectionFolderUtils;
import io.cloudbeaver.utils.WebDataSourceUtils;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
@@ -55,7 +52,6 @@ import org.jkiss.dbeaver.registry.DataSourceDescriptor;
import org.jkiss.dbeaver.registry.DataSourceProviderRegistry;
import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor;
import org.jkiss.dbeaver.registry.network.NetworkHandlerRegistry;
import org.jkiss.dbeaver.runtime.DBWorkbench;
import org.jkiss.dbeaver.runtime.jobs.ConnectionTestJob;
import org.jkiss.dbeaver.utils.RuntimeUtils;
import org.jkiss.utils.CommonUtils;
@@ -294,6 +290,8 @@ public class WebServiceCore implements DBWServiceCore {
connectionInfo.clearSavedCredentials();
}
// Mark all specified network configs as saved
boolean[] saveConfig = new boolean[1];
if (networkCredentials != null) {
networkCredentials.forEach(c -> {
if (CommonUtils.toBoolean(c.isSavePassword()) && !CommonUtils.isEmpty(c.getUserName())) {
@@ -302,7 +300,7 @@ public class WebServiceCore implements DBWServiceCore {
handlerCfg.setUserName(c.getUserName());
handlerCfg.setPassword(c.getPassword());
handlerCfg.setSavePassword(true);
dataSourceContainer.persistConfiguration();
saveConfig[0] = true;
}
}
});
@@ -316,6 +314,13 @@ public class WebServiceCore implements DBWServiceCore {
true);
WebDataSourceUtils.saveCredentialsInDataSource(connectionInfo, dataSourceContainer, dataSourceContainer.getConnectionConfiguration());
saveConfig[0] = true;
}
if (WebServiceUtils.isGlobalProject(dataSourceContainer.getProject())) {
// Do not flush config for global project (only admin can do it - CB-2415)
saveConfig[0] = false;
}
if (saveConfig[0]) {
dataSourceContainer.persistConfiguration();
}
@@ -160,7 +160,7 @@ public class WebNavigatorNodeInfo {
} else if (node instanceof DBNLocalFolder) {
DataSourceFolder folder = (DataSourceFolder) ((DBNLocalFolder) node).getFolder();
String projectName = folder.getDataSourceRegistry().getProject().getName();
Set<DBPDataSourceFolder> tempFolders = ((DataSourceRegistry) folder.getDataSourceRegistry()).getTemporaryFolders();
Set<DBPDataSourceFolder> tempFolders = folder.getDataSourceRegistry().getTemporaryFolders();
isShared = !projectName.equals(session.getUserId()) || tempFolders.contains(folder);
}
if (isShared) {
@@ -466,17 +466,22 @@ public class WebServiceNavigator implements DBWServiceNavigator {
}
checkProjectEditAccess(node, session);
if (node instanceof DBNDataSource) {
DBPDataSourceFolder folder;
if (folderNode instanceof DBNRoot || folderNode instanceof DBNProject) {
folder = null;
} else if (folderNode instanceof DBNLocalFolder) {
folder = ((DBNLocalFolder) folderNode).getFolder();
} else {
throw new DBWebException("Navigator node '" + folderNodePath + "' is not a folder node");
}
DBPDataSourceFolder folder = WebConnectionFolderUtils.getParentFolder(folderNode);
((DBNDataSource) node).moveToFolder(folderNode.getOwnerProject(), folder);
node.getOwnerProject().getDataSourceRegistry().updateDataSource(
((DBNDataSource) node).getDataSourceContainer());
} else if (node instanceof DBNLocalFolder) {
DBPDataSourceFolder folder = WebConnectionFolderUtils.getParentFolder(folderNode);
if (folder != null) {
List<String> siblings = Arrays.stream(folder.getChildren())
.map(DBPDataSourceFolder::getName)
.collect(Collectors.toList());
if (siblings.contains(node.getName())) {
throw new DBWebException("Node " + folderNodePath + " contains folder with name '" + node.getName() + "'");
}
}
((DBNLocalFolder) node).getFolder().setParent(folder);
WebServiceUtils.updateConfigAndRefreshDatabases(session, node.getOwnerProject().getId());
} else if (node instanceof DBNResourceManagerResource) {
boolean rmNewNode = folderNode instanceof DBNAbstractResourceManagerNode;
DBNResourceManagerResource rmOldNode = (DBNResourceManagerResource) node;
@@ -660,7 +660,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
String connectionId
) throws DBWebException {
DBPProject globalProject = webSession.getProjectById(projectId);
if (!CommonUtils.equalObjects(globalProject.getName(), CBApplication.getInstance().getDefaultProjectName())) {
if (!WebServiceUtils.isGlobalProject(globalProject)) {
throw new DBWebException("Project '" + projectId + "'is not global");
}
try {
@@ -685,7 +685,7 @@ public class WebServiceAdmin implements DBWServiceAdmin {
@NotNull List<String> subjects
) throws DBWebException {
DBPProject globalProject = webSession.getProjectById(projectId);
if (!CommonUtils.equalObjects(globalProject.getName(), CBApplication.getInstance().getDefaultProjectName())) {
if (!WebServiceUtils.isGlobalProject(globalProject)) {
throw new DBWebException("Project '" + projectId + "'is not global");
}
DBPDataSourceContainer dataSource = getGlobalRegistry(webSession).getDataSource(connectionId);
@@ -16,31 +16,26 @@
*/
package io.cloudbeaver.service.auth;
import io.cloudbeaver.DBWUserIdentity;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.auth.SMAuthProviderExternal;
import io.cloudbeaver.auth.provider.rp.RPAuthProvider;
import io.cloudbeaver.model.session.WebAuthInfo;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.model.user.WebUser;
import io.cloudbeaver.model.session.WebSessionAuthProcessor;
import io.cloudbeaver.server.CBApplication;
import io.cloudbeaver.service.DBWSessionHandler;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.auth.SMAuthInfo;
import org.jkiss.dbeaver.model.auth.SMSession;
import org.jkiss.dbeaver.model.security.SMConstants;
import org.jkiss.dbeaver.model.security.SMController;
import org.jkiss.dbeaver.model.security.exception.SMException;
import org.jkiss.dbeaver.registry.auth.AuthProviderDescriptor;
import org.jkiss.dbeaver.registry.auth.AuthProviderRegistry;
import org.jkiss.utils.CommonUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.time.OffsetDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -70,7 +65,6 @@ public class RPSessionHandler implements DBWSessionHandler {
String userName = request.getHeader(RPAuthProvider.X_USER);
String roles = request.getHeader(RPAuthProvider.X_ROLE);
List<String> userRoles = roles == null ? Collections.emptyList() : List.of(roles.split("\\|"));
SMSession authSession;
if (userName != null) {
try {
Map<String, Object> credentials = new HashMap<>();
@@ -79,39 +73,18 @@ public class RPSessionHandler implements DBWSessionHandler {
sessionParameters.put(SMConstants.SESSION_PARAM_TRUSTED_USER_ROLES, userRoles);
Map<String, Object> userCredentials = authProviderExternal.authExternalUser(
webSession.getProgressMonitor(), sessionParameters, credentials);
String currentSmSessionId = webSession.getUser() == null ? null : webSession.getUserContext().getSmSessionId();
try {
SMAuthInfo smAuthInfo = securityController.authenticate(
webSession.getSessionId(),
webSession.getUserContext().getSmSessionId(),
currentSmSessionId,
sessionParameters,
WebSession.CB_SESSION_TYPE, authProvider.getId(), null, userCredentials);
webSession.updateSMAuthInfo(smAuthInfo);
new WebSessionAuthProcessor(webSession, smAuthInfo, false).authenticateSession();
} catch (SMException e) {
log.debug("Error during user authentication", e);
throw e;
}
WebUser user = webSession.getUser();
DBWUserIdentity userIdentity = authProviderExternal.getUserIdentity(
webSession.getProgressMonitor(), sessionParameters, credentials);
if (CommonUtils.isEmpty(user.getDisplayName())) {
user.setDisplayName(userIdentity.getDisplayName());
}
authSession = authProviderExternal.openSession(
webSession.getProgressMonitor(),
webSession,
sessionParameters,
userCredentials);
WebAuthInfo authInfo = new WebAuthInfo(
webSession,
user,
authProvider,
userIdentity,
authSession,
OffsetDateTime.now());
authInfo.setMessage("Authenticated with " + authProvider.getLabel() + " provider");
webSession.addAuthInfo(authInfo);
} catch (Exception e) {
throw new DBWebException("Error", e);
}
@@ -24,6 +24,8 @@ import org.jkiss.dbeaver.model.auth.SMSession;
import org.jkiss.dbeaver.model.auth.SMSessionContext;
import org.jkiss.dbeaver.model.auth.SMSessionPrincipal;
import java.time.LocalDateTime;
/**
* Local auth provider
*/
@@ -65,6 +67,12 @@ public class LocalAuthSession implements SMSession {
return webSession.getSessionId();
}
@NotNull
@Override
public LocalDateTime getSessionStart() {
return webSession.getSessionStart();
}
@Override
public boolean isApplicationSession() {
return false;
@@ -1,15 +1,23 @@
package io.cloudbeaver.test.platform;
import io.cloudbeaver.auth.provider.rp.RPAuthProvider;
import io.cloudbeaver.utils.WebTestUtils;
import org.jkiss.dbeaver.model.auth.SMAuthStatus;
import org.jkiss.dbeaver.model.data.json.JSONUtils;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class AuthenticationTest {
public static final String GQL_TEMPLATE_OPEN_SESSION = "openSession.json";
public static final String GQL_TEMPLATE_ACTIVE_USER = "activeUser.json";
public static final String REVERSE_PROXY_TEST_USER = "reverseProxyTestUser";
@Test
public void testLoginUser() throws Exception {
@@ -18,4 +26,38 @@ public class AuthenticationTest {
client, CEServerTestSuite.getScriptsPath(), CEServerTestSuite.GQL_API_URL);
Assert.assertEquals(SMAuthStatus.SUCCESS.name(), JSONUtils.getString(authInfo, "authStatus"));
}
@Test
public void testReverseProxyAnonymousModeLogin() throws Exception {
HttpClient client = CEServerTestSuite.getClient();
Map<String, Object> sessionInfo = openSession(client);
Assert.assertTrue(JSONUtils.getBoolean(sessionInfo, "valid"));
Map<String, Object> activeUser = getActiveUser(client);
Assert.assertEquals(REVERSE_PROXY_TEST_USER, JSONUtils.getString(activeUser, "userId"));
}
private Map<String, Object> openSession(HttpClient client) throws Exception {
Map<String, Object> data = doPostQuery(client, GQL_TEMPLATE_OPEN_SESSION);
if (data != null) {
return JSONUtils.getObject(data, "session");
}
return Collections.emptyMap();
}
private Map<String, Object> getActiveUser(HttpClient client) throws Exception {
Map<String, Object> data = doPostQuery(client, GQL_TEMPLATE_ACTIVE_USER);
if (data != null) {
return JSONUtils.getObject(data, "user");
}
return Collections.emptyMap();
}
private Map<String, Object> doPostQuery(HttpClient client, String gqlScript) throws Exception {
String input = WebTestUtils.readScriptTemplate(gqlScript, CEServerTestSuite.getScriptsPath());
List<String> headers = List.of(RPAuthProvider.X_USER, REVERSE_PROXY_TEST_USER, RPAuthProvider.X_ROLE, "user");
Map<String, Object> map = WebTestUtils.doPostWithHeaders(CEServerTestSuite.GQL_API_URL, input, client, headers);
return JSONUtils.getObjectOrNull(map, "data");
}
}
@@ -1,6 +1,7 @@
{
server: {
serverPort: "${CLOUDBEAVER_TEST_PORT:18978}",
serverName: "CloudBeaver CE Test Server",
workspaceLocation: "workspace",
contentRoot: "workspace/web",
@@ -36,8 +37,10 @@
anonymousAccessEnabled: true,
anonymousUserRole: "user",
supportsCustomConnections: true,
enableReverseProxyAuth: true,
enabledAuthProviders: [
"local"
"local",
"reverseProxy"
],
enabledFeatures: [
@@ -0,0 +1,4 @@
{
"query": "\n query activeUser {\n user: activeUser {\n userId\n }\n}\n ",
"operationName": "activeUser"
}
@@ -0,0 +1,7 @@
{
"query": "\n mutation openSession($defaultLocale: String) {\n session: openSession(defaultLocale: $defaultLocale) {\n valid\n }\n}\n ",
"variables": {
"defaultLocale": "en"
},
"operationName": "openSession"
}
@@ -90,7 +90,7 @@ const styles = css`
width: 100%;
height: 100%;
}
}
}
}
}
Icon {
@@ -112,12 +112,12 @@ const styles = css`
width: 16px;
height: 16px;
margin-left: 12px;
& IconOrImage {
width: 100%;
height: 100%;
}
&:not(:empty) + input {
padding-left: 34px !important;
}
@@ -131,8 +131,8 @@ type BaseProps<TKey, TValue> = Omit<React.InputHTMLAttributes<HTMLInputElement>,
searchable?: boolean;
defaultValue?: TKey;
loading?: boolean;
keySelector: (item: TValue, index: number) => TKey;
valueSelector: (item: TValue) => string;
keySelector?: (item: TValue, index: number) => TKey;
valueSelector?: (item: TValue) => string;
titleSelector?: (item: TValue) => string | undefined;
iconSelector?: (item: TValue) => string | React.ReactElement | undefined;
isDisabled?: (item: TValue) => boolean;
@@ -11,7 +11,7 @@ export function isControlPresented(
): boolean {
if (state !== undefined && name !== undefined) {
if (name in state) {
return state[name] !== null;
return state[name] !== null && state[name] !== undefined;
}
return defaultValue !== undefined;
}
@@ -39,6 +39,7 @@ export default [
['connections_connection_access_filter_placeholder', 'Search for user or role name'],
['connections_connection_access_admin_info', 'Administrators see all connections.'],
['connections_connection_description', 'Description'],
['connections_connection_project', 'Project'],
['connections_connection_driver', 'Driver'],
['connections_connection_configuration', 'Configuration'],
['connections_connection_host', 'Host'],
@@ -42,6 +42,7 @@ export default [
['connections_connection_access_admin_info', 'Gli amministratori vedono tutte le connessioni.'],
['connections_connection_description', 'Descrizione'],
['connections_connection_driver', 'Driver'],
['connections_connection_project', 'Project'],
['connections_connection_configuration', 'Configuration'],
['connections_connection_host', 'Host'],
['connections_connection_port', 'Porta'],
@@ -36,6 +36,7 @@ export default [
['connections_connection_access_filter_placeholder', 'Поиск по имени пользователя или роли'],
['connections_connection_access_admin_info', 'Администраторы видят все подключения.'],
['connections_connection_description', 'Описание'],
['connections_connection_project', 'Проект'],
['connections_connection_driver', 'Драйвер'],
['connections_connection_configuration', 'Конфигурация'],
['connections_connection_host', 'Хост'],
@@ -39,6 +39,7 @@ export default [
['connections_connection_access_filter_placeholder', '搜索用户或角色名称'],
['connections_connection_access_admin_info', '管理员查看所有连接。'],
['connections_connection_description', '描述'],
['connections_connection_project', 'Project'],
['connections_connection_driver', '驱动'],
['connections_connection_configuration', 'Configuration'],
['connections_connection_host', '主机'],
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { action, makeObservable } from 'mobx';
import { action, makeObservable, runInAction } from 'mobx';
import { injectable } from '@cloudbeaver/core-di';
import { SessionPermissionsResource, EPermission } from '@cloudbeaver/core-root';
@@ -24,6 +24,7 @@ import {
import { MetadataMap } from '@cloudbeaver/core-utils';
import type { NavNode } from './EntityTypes';
import { NodeManagerUtils } from './NodeManagerUtils';
type NavNodeInfo = NavNodeInfoFragment;
@@ -99,10 +100,14 @@ export class NavNodeInfoResource extends CachedMapResource<string, NavNode> {
const parents: string[] = [];
let current = this.get(key);
if (!current) {
return NodeManagerUtils.parentsFromPath(key);
}
while (
current
&& current.parentId !== current.id
&& current.parentId !== ROOT_NODE_PATH
// && current.parentId !== ROOT_NODE_PATH
) {
parents.unshift(current.parentId);
current = this.get(current.parentId);
@@ -174,13 +179,42 @@ export class NavNodeInfoResource extends CachedMapResource<string, NavNode> {
}
private async loadNodeInfo(nodePath: string): Promise<NavNode> {
if (this.has(nodePath)) {
const metadata = this.metadata.get(nodePath);
const { navNodeInfo } = await this.graphQLService.sdk.navNodeInfo({
nodePath,
withDetails: metadata.withDetails,
});
return this.navNodeInfoToNavNode(navNodeInfo);
} else {
return await this.loadNodeParents(nodePath);
}
}
private async loadNodeParents(nodePath: string): Promise<NavNode> {
const metadata = this.metadata.get(nodePath);
const { navNodeInfo } = await this.graphQLService.sdk.navNodeInfo({
const { node, parents } = await this.graphQLService.sdk.getNodeParents({
nodePath,
withDetails: metadata.withDetails,
});
return this.navNodeInfoToNavNode(navNodeInfo);
return runInAction(() => {
const navNode = this.navNodeInfoToNavNode(node, parents[0]?.id ?? ROOT_NODE_PATH);
this.updateNode(
resourceKeyList(parents.map(node => node.id), node.id),
[
...parents.reduce((list, node, index, array) => {
list.push(this.navNodeInfoToNavNode(node, array[index + 1]?.id ?? ROOT_NODE_PATH));
return list;
}, [] as NavNode[]),
navNode,
]
);
return navNode;
});
}
}
@@ -362,21 +362,7 @@ export class NavNodeManagerService extends Bootstrap {
canOpen = true;
};
const getParents = () => {
const parents: string[] = [];
let parent = this.getNode(nodeId);
if (!parent) {
return NodeManagerUtils.parentsFromPath(nodeId);
}
while (parent && parent.id !== parent.parentId) {
parents.unshift(parent.parentId);
parent = this.getNode(parent.parentId);
}
return parents;
};
const getParents = () => this.navNodeInfoResource.getParents(nodeId);
const loadParents = async (parents: string[]) => {
let parentId = ROOT_NODE_PATH;
@@ -11,14 +11,14 @@ query navGetStructContainers(
) {
catalogList {
catalog {
...NavNodeInfo
...NavNodeInfo
}
schemaList {
...NavNodeInfo
...NavNodeInfo
}
}
schemaList {
...NavNodeInfo
...NavNodeInfo
}
supportsCatalogChange
supportsSchemaChange
@@ -0,0 +1,11 @@
query getNodeParents(
$nodePath: ID!
$withDetails: Boolean!
) {
node: navNodeInfo(nodePath: $nodePath) {
...NavNodeInfo
}
parents: navNodeParents(nodePath: $nodePath) {
...NavNodeInfo
}
}
+21
View File
@@ -2330,6 +2330,14 @@ export type GetNavNodeFullNameQueryVariables = Exact<{
export type GetNavNodeFullNameQuery = { navNodeInfo: { fullName?: string } };
export type GetNodeParentsQueryVariables = Exact<{
nodePath: Scalars['ID'];
withDetails: Scalars['Boolean'];
}>;
export type GetNodeParentsQuery = { node: { id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }, parents: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array<string>, object?: { features?: Array<string> }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array<string>, value?: any, order: number }> }> };
export type NavDeleteNodesMutationVariables = Exact<{
nodePaths: Array<Scalars['ID']> | Scalars['ID'];
}>;
@@ -3779,6 +3787,16 @@ export const GetNavNodeFullNameDocument = `
}
}
`;
export const GetNodeParentsDocument = `
query getNodeParents($nodePath: ID!, $withDetails: Boolean!) {
node: navNodeInfo(nodePath: $nodePath) {
...NavNodeInfo
}
parents: navNodeParents(nodePath: $nodePath) {
...NavNodeInfo
}
}
${NavNodeInfoFragmentDoc}`;
export const NavDeleteNodesDocument = `
mutation navDeleteNodes($nodePaths: [ID!]!) {
navDeleteNodes(nodePaths: $nodePaths)
@@ -4356,6 +4374,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
getNavNodeFullName(variables: GetNavNodeFullNameQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetNavNodeFullNameQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetNavNodeFullNameQuery>(GetNavNodeFullNameDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getNavNodeFullName', 'query');
},
getNodeParents(variables: GetNodeParentsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<GetNodeParentsQuery> {
return withWrapper((wrappedRequestHeaders) => client.request<GetNodeParentsQuery>(GetNodeParentsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getNodeParents', 'query');
},
navDeleteNodes(variables: NavDeleteNodesMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise<NavDeleteNodesMutation> {
return withWrapper((wrappedRequestHeaders) => client.request<NavDeleteNodesMutation>(NavDeleteNodesDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'navDeleteNodes', 'mutation');
},
@@ -23,11 +23,12 @@ import { AuthDialogService } from './Dialog/AuthDialogService';
import type { IAuthOptions } from './IAuthOptions';
import { isAutoLoginSessionAction } from './isAutoLoginSessionAction';
export type LogoutEventType = 'before' | 'after';
type AuthEventType = 'before' | 'after';
@injectable()
export class AuthenticationService extends Bootstrap {
readonly onLogout: Executor<LogoutEventType>;
readonly onLogout: Executor<AuthEventType>;
readonly onLogin: Executor<AuthEventType>;
configureAuthProvider: (() => void) | null;
configureIdentityProvider: (() => void) | null;
@@ -52,6 +53,7 @@ export class AuthenticationService extends Bootstrap {
super();
this.onLogout = new Executor();
this.onLogin = new Executor();
this.authPromise = null;
this.configureAuthProvider = null;
@@ -131,13 +133,24 @@ export class AuthenticationService extends Bootstrap {
}
private async auth(persistent: boolean, options: IAuthOptions) {
const contexts = await this.onLogin.execute('before');
if (ExecutorInterrupter.isInterrupted(contexts)) {
return;
}
if (this.authPromise) {
return this.authPromise;
await this.authPromise;
return;
}
options = observable(options);
this.authPromise = this.authDialogService.showLoginForm(persistent, options);
this.authPromise = this.authDialogService.showLoginForm(persistent, options)
.then(async state => {
await this.onLogin.execute('after');
return state;
});
if (this.serverConfigResource.redirectOnFederatedAuth) {
await this.authProvidersResource.loadAll();
@@ -158,7 +171,7 @@ export class AuthenticationService extends Bootstrap {
}
try {
return await this.authPromise;
await this.authPromise;
} finally {
this.authPromise = null;
}
@@ -13,13 +13,11 @@ import { DATA_CONTEXT_MENU, MenuBaseItem, MenuService } from '@cloudbeaver/core-
import { TOP_NAV_BAR_SETTINGS_MENU } from '@cloudbeaver/plugin-settings-menu';
import { AuthenticationService } from './AuthenticationService';
import { AuthDialogService } from './Dialog/AuthDialogService';
@injectable()
export class PluginBootstrap extends Bootstrap {
constructor(
private readonly serverConfigResource: ServerConfigResource,
private readonly authDialogService: AuthDialogService,
private readonly authenticationService: AuthenticationService,
private readonly authInfoService: AuthInfoService,
private readonly menuService: MenuService,
@@ -40,7 +38,7 @@ export class PluginBootstrap extends Bootstrap {
label: 'authentication_login',
tooltip: 'authentication_login',
},
{ onSelect: () => this.authDialogService.showLoginForm(false, { providerId: null, linkUser: false }) }
{ onSelect: () => this.authenticationService.authUser(null, false) }
),
];
}
@@ -54,7 +52,7 @@ export class PluginBootstrap extends Bootstrap {
label: 'authentication_logout',
tooltip: 'authentication_logout',
},
{ onSelect: this.authenticationService.logout.bind(this.authenticationService) }
{ onSelect: () => this.authenticationService.logout() }
),
];
}
@@ -9,8 +9,7 @@
import { observer } from 'mobx-react-lite';
import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication';
import { Button, PlaceholderComponent, useMapResource } from '@cloudbeaver/core-blocks';
import { DBDriverResource } from '@cloudbeaver/core-connections';
import { Button, PlaceholderComponent } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { useAuthenticationAction } from '@cloudbeaver/core-ui';
@@ -21,16 +20,12 @@ export const ConnectionFormBaseActions: PlaceholderComponent<IConnectionFormProp
onCancel,
}) {
const translate = useTranslate();
const driverMap = useMapResource(
ConnectionFormBaseActions,
DBDriverResource,
state.config.driverId || null
);
const driver = driverMap.data;
const authentication = useAuthenticationAction({
origin: state.info?.origin ?? { type: AUTH_PROVIDER_LOCAL_ID, displayName: 'Local' },
});
const authorized = authentication.type === AUTH_PROVIDER_LOCAL_ID || authentication.authorized;
return (
<>
{onCancel && (
@@ -45,7 +40,7 @@ export const ConnectionFormBaseActions: PlaceholderComponent<IConnectionFormProp
)}
<Button
type="button"
disabled={state.disabled || !(!driver?.anonymousAccess && authentication.authorized)}
disabled={state.disabled || !authorized}
mod={['outlined']}
loader
onClick={state.test}
@@ -152,6 +152,7 @@ export class ConnectionFormState implements IConnectionFormState {
.next(this.formStateTask);
makeObservable<IConnectionFormState, '_availableDrivers' | 'stateInfo' | 'updateFormState'>(this, {
projectId: observable,
mode: observable,
type: observable,
config: observable,
@@ -208,12 +209,17 @@ export class ConnectionFormState implements IConnectionFormState {
}
setConfig(projectId: string, config: ConnectionConfig): this {
this.projectId = projectId;
this.setProject(projectId);
this.config = config;
this.reset();
return this;
}
setProject(projectId: string): this {
this.projectId = projectId;
return this;
}
setAvailableDrivers(drivers: string[]): this {
this._availableDrivers = drivers;
this.reset();
@@ -46,6 +46,7 @@ export interface IConnectionFormState {
type: ConnectionFormType
) => this;
readonly setConfig: (projectId: string, config: ConnectionConfig) => this;
readonly setProject: (projectId: string) => this;
readonly setAvailableDrivers: (drivers: string[]) => this;
readonly save: () => Promise<void>;
readonly test: () => Promise<void>;
@@ -11,6 +11,7 @@ import { action, makeObservable, runInAction, toJS } from 'mobx';
import { createConnectionParam, DatabaseAuthModelsResource, DatabaseConnection, DBDriverResource } from '@cloudbeaver/core-connections';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import type { IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { CachedMapAllKey, DriverConfigurationType, isObjectPropertyInfoStateEqual, ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import { getUniqueName, isValuesEqual } from '@cloudbeaver/core-utils';
@@ -25,6 +26,7 @@ import { Options } from './Options';
@injectable()
export class ConnectionOptionsTabService extends Bootstrap {
constructor(
private readonly projectsResource: ProjectsResource,
private readonly connectionFormService: ConnectionFormService,
private readonly dbDriverResource: DBDriverResource,
private readonly databaseAuthModelsResource: DatabaseAuthModelsResource,
@@ -120,14 +122,22 @@ export class ConnectionOptionsTabService extends Bootstrap {
const validation = contexts.getContext(this.connectionFormService.connectionValidationContext);
if (!state.config.name?.length) {
validation.error("Field 'name' can't be empty");
validation.error('plugin_connections_connection_form_name_invalid');
}
if (state.config.driverId && state.config.configurationType) {
const driver = await this.dbDriverResource.load(state.config.driverId, ['includeProviderProperties']);
if (!driver.configurationTypes.includes(state.config.configurationType)) {
validation.error('Configuration is not supported');
validation.error('plugin_connections_connection_form_host_configuration_invalid');
}
}
if (state.projectId !== null && state.mode === 'create') {
const project = this.projectsResource.get(state.projectId);
if (!project?.canCreateConnections) {
validation.error('plugin_connections_connection_form_project_invalid');
}
}
@@ -226,7 +236,11 @@ export class ConnectionOptionsTabService extends Bootstrap {
}
tempConfig.description = state.config.description;
tempConfig.template = state.config.template;
if (state.projectId === PROJECT_GLOBAL_ID) {
tempConfig.template = state.config.template;
}
tempConfig.driverId = state.config.driverId;
if (!state.config.template && state.config.folder) {
@@ -246,7 +260,7 @@ export class ConnectionOptionsTabService extends Bootstrap {
tempConfig.databaseName = state.config.databaseName;
}
if (state.config.authModelId || driver.defaultAuthModel) {
if ((state.config.authModelId || driver.defaultAuthModel) && !driver.anonymousAccess) {
tempConfig.authModelId = state.config.authModelId || driver.defaultAuthModel;
tempConfig.saveCredentials = state.config.saveCredentials;
@@ -34,9 +34,9 @@ import {
import { DatabaseAuthModelsResource, DBDriverResource, isLocalConnection } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { useTranslate } from '@cloudbeaver/core-localization';
import { PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { Project, ProjectsResource, PROJECT_GLOBAL_ID } from '@cloudbeaver/core-projects';
import { usePermission } from '@cloudbeaver/core-root';
import { CachedMapEmptyKey, DriverConfigurationType, resourceKeyList } from '@cloudbeaver/core-sdk';
import { CachedMapAllKey, CachedMapEmptyKey, DriverConfigurationType, resourceKeyList } from '@cloudbeaver/core-sdk';
import { useStyles } from '@cloudbeaver/core-theming';
import type { TabContainerPanelComponent } from '@cloudbeaver/core-ui';
import { useAuthenticationAction } from '@cloudbeaver/core-ui';
@@ -116,6 +116,17 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
optionsHook.setAuthModel(model);
}, []);
const projectsLoader = useMapResource(Options, ProjectsResource, CachedMapAllKey);
const projects = projectsLoader.data as Project[];
function handleProjectSelect(projectId: string) {
const project = projectsLoader.resource.get(projectId);
if (project?.canCreateConnections) {
state.setProject(projectId);
}
}
const driverMap = useMapResource(
Options,
DBDriverResource,
@@ -293,6 +304,24 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
>
{translate('connections_connection_name')}
</InputField>
<Combobox
name='projectId'
value={state.projectId ?? PROJECT_GLOBAL_ID}
items={projects}
keySelector={project => project.id}
valueSelector={project => project.name}
titleSelector={project => project.description}
isDisabled={project => !project.canCreateConnections}
searchable={projects.length > 10}
readOnly={readonly || edit || projects.length < 2}
disabled={disabled}
loading={projectsLoader.isLoading()}
tiny
fill
onSelect={handleProjectSelect}
>
{translate('connections_connection_project')}
</Combobox>
{!config.template && (
<InputField
type="text"
@@ -301,6 +330,7 @@ export const Options: TabContainerPanelComponent<IConnectionFormProps> = observe
disabled={disabled}
autoComplete={`section-${config.driverId || 'driver'} section-folder`}
mod='surface'
autoHide
readOnly
tiny
fill
@@ -155,22 +155,30 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
}: INodeMoveData,
contexts: IExecutionContextProvider<INodeMoveData>
) {
if (![NAV_NODE_TYPE_PROJECT, NAV_NODE_TYPE_FOLDER].includes(targetNode.nodeType!)) {
return;
}
await this.projectsResource.load(CachedMapAllKey);
const move = contexts.getContext(navNodeMoveContext);
const nodes = getNodesFromContext(moveContexts);
const nodeIdList = nodes.map(node => node.id);
const children = this.navTreeResource.get(targetNode.id);
const children = this.navTreeResource.get(targetNode.id) ?? [];
const targetProject = this.projectsNavNodeService.getProject(targetNode.id);
const supported = (
[NAV_NODE_TYPE_PROJECT, NAV_NODE_TYPE_FOLDER].includes(targetNode.nodeType!)
&& nodes.every(node => (
node.nodeType === NAV_NODE_TYPE_CONNECTION
&& targetProject === this.projectsNavNodeService.getProject(node.id)
&& !children?.includes(node.id)
))
);
const supported = nodes.every(node => {
if (
![NAV_NODE_TYPE_CONNECTION, NAV_NODE_TYPE_FOLDER, NAV_NODE_TYPE_PROJECT].includes(node.nodeType!)
|| targetProject !== this.projectsNavNodeService.getProject(node.id)
|| children.includes(node.id)
|| targetNode.id === node.id
) {
return false;
}
return true;
});
if (!supported) {
return;
@@ -181,6 +189,31 @@ export class ConnectionFoldersBootstrap extends Bootstrap {
move.setCanMove(true);
}
} else {
const childrenNode = this.navNodeInfoResource.get(resourceKeyList(children));
const folderDuplicates = nodes.filter(node => (
node.nodeType === NAV_NODE_TYPE_FOLDER
&& (
childrenNode.some(child => child?.nodeType === NAV_NODE_TYPE_FOLDER && child.name === node.name)
|| nodes.some(child => (
child.nodeType === NAV_NODE_TYPE_FOLDER
&& child.name === node.name
&& child.id !== node.id
))
)
));
if (folderDuplicates.length > 0) {
this.notificationService.logError({
title: 'connections_public_connection_folder_move_failed',
message: this.localizationService.translate(
'connections_public_connection_folder_move_duplication',
undefined,
{ name: folderDuplicates.map(node => `"${node.name}"`).join(', ') }
),
});
return;
}
try {
await this.navTreeResource.moveTo(resourceKeyList(nodeIdList), targetNode.id);
const connections = nodeIdList
@@ -13,9 +13,9 @@ import { injectable } from '@cloudbeaver/core-di';
import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor';
import { SessionDataResource } from '@cloudbeaver/core-root';
import type { ConnectionConfig, ResourceKey } from '@cloudbeaver/core-sdk';
import { OptionsPanelService } from '@cloudbeaver/core-ui';
import { AuthenticationService } from '@cloudbeaver/plugin-authentication';
import { ConnectionAuthService } from '../ConnectionAuthService';
import { ConnectionFormService } from '../ConnectionForm/ConnectionFormService';
@@ -36,14 +36,29 @@ export class PublicConnectionFormService {
private readonly connectionFormService: ConnectionFormService,
private readonly connectionInfoResource: ConnectionInfoResource,
private readonly connectionAuthService: ConnectionAuthService,
private readonly sessionDataResource: SessionDataResource
private readonly authenticationService: AuthenticationService,
) {
this.formState = null;
this.optionsPanelService.closeTask.addHandler(this.closeHandler);
this.connectionInfoResource.onDataUpdate.addPostHandler(this.closeRemoved);
this.connectionInfoResource.onItemDelete.addPostHandler(this.closeDeleted);
this.sessionDataResource.onDataOutdated.addHandler(() => {
this.close(true);
this.authenticationService.onLogin.addHandler(async (event, context) => {
if (event === 'before') {
const confirmed = await this.showUnsavedChangesDialog();
if (!confirmed) {
ExecutorInterrupter.interrupt(context);
}
}
});
this.authenticationService.onLogout.addHandler(async (event, context) => {
if (event === 'before') {
const confirmed = await this.close(false);
if (!confirmed) {
ExecutorInterrupter.interrupt(context);
}
}
});
makeObservable(this, {
@@ -87,9 +102,9 @@ export class PublicConnectionFormService {
return state;
}
async close(saved?: boolean): Promise<void> {
async close(saved?: boolean): Promise<boolean> {
if (!this.formState) {
return;
return true;
}
if (saved) {
@@ -101,6 +116,8 @@ export class PublicConnectionFormService {
if (state) {
this.clearFormState();
}
return state;
}
async save(): Promise<void> {
@@ -147,6 +164,14 @@ export class PublicConnectionFormService {
};
private readonly closeHandler: IExecutorHandler<any> = async (data, contexts) => {
const confirmed = await this.showUnsavedChangesDialog();
if (!confirmed) {
ExecutorInterrupter.interrupt(contexts);
}
};
private async showUnsavedChangesDialog(): Promise<boolean> {
if (
!this.formState
|| !this.optionsPanelService.isOpen(formGetter)
@@ -159,13 +184,13 @@ export class PublicConnectionFormService {
))
)
) {
return;
return true;
}
const state = await this.formState.checkFormState();
if (!state?.edited) {
return;
return true;
}
const result = await this.commonDialogService.open(ConfirmationDialog, {
@@ -174,10 +199,8 @@ export class PublicConnectionFormService {
confirmActionText: 'ui_processing_ok',
});
if (result === DialogueStateResult.Rejected) {
ExecutorInterrupter.interrupt(contexts);
}
};
return result !== DialogueStateResult.Rejected;
}
private async tryReconnect(connectionKey: IConnectionInfoParams) {
const result = await this.commonDialogService.open(ConfirmationDialog, {
@@ -6,5 +6,9 @@ export default [
['connections_public_connection_edit_reconnect_message', 'Connection has been updated. Do you want to reconnect?'],
['connections_public_connection_edit_reconnect_failed', 'Failed to reconnect'],
['connections_public_connection_folder_move_failed', 'Failed to move to folder'],
['connections_public_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({args.name})'],
['plugin_connections_connection_form_project_invalid', 'You have no access to create connections in selected project'],
['plugin_connections_connection_form_host_configuration_invalid', 'Host configuration is not supported'],
['plugin_connections_connection_form_name_invalid', 'Field \'name\' can\'t be empty'],
['connections_public_connection_folder_delete_confirmation', 'You\'re going to delete "{args.name}". Connections won\'t be deleted. Are you sure?'],
];
@@ -6,5 +6,9 @@ export default [
['connections_public_connection_edit_reconnect_message', 'Connection has been updated. Do you want to reconnect?'],
['connections_public_connection_edit_reconnect_failed', 'Failed to reconnect'],
['connections_public_connection_folder_move_failed', 'Failed to move to folder'],
['connections_public_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({args.name})'],
['plugin_connections_connection_form_project_invalid', 'You have no access to create connections in selected project'],
['plugin_connections_connection_form_host_configuration_invalid', 'Host configuration is not supported'],
['plugin_connections_connection_form_name_invalid', 'Field \'name\' can\'t be empty'],
['connections_public_connection_folder_delete_confirmation', 'You\'re going to delete "{args.name}". Connections won\'t be deleted. Are you sure?'],
];
@@ -6,5 +6,9 @@ export default [
['connections_public_connection_edit_reconnect_message', 'Подключение было обновлено. Вы хотите переподключиться?'],
['connections_public_connection_edit_reconnect_failed', 'Не удалось переподключиться'],
['connections_public_connection_folder_move_failed', 'Ошибка перемещения в папку'],
['connections_public_connection_folder_move_duplication', 'Выбранные папки или папка назначения содержит папки с таким же названием ({args.name})'],
['plugin_connections_connection_form_project_invalid', 'У вас нет разрешения создавать коннекшены в выбранном проекте'],
['plugin_connections_connection_form_host_configuration_invalid', 'Конфигурация хоста не поддерживается'],
['plugin_connections_connection_form_name_invalid', 'Поле \'Название\' не может быть пустым'],
['connections_public_connection_folder_delete_confirmation', 'Вы удаляете "{args.name}". Подключения не будут удалены. Вы уверены?'],
];
@@ -6,5 +6,9 @@ export default [
['connections_public_connection_edit_reconnect_message', '连接已更新。您想重新连接吗?'],
['connections_public_connection_edit_reconnect_failed', '重新连接失败'],
['connections_public_connection_folder_move_failed', 'Failed to move to folder'],
['connections_public_connection_folder_move_duplication', 'Target folder or selected folders contains folder with the same name ({args.name})'],
['plugin_connections_connection_form_project_invalid', 'You have no access to create connections in selected project'],
['plugin_connections_connection_form_host_configuration_invalid', 'Host configuration is not supported'],
['plugin_connections_connection_form_name_invalid', 'Field \'name\' can\'t be empty'],
['connections_public_connection_folder_delete_confirmation', 'You\'re going to delete "{args.name}". Connections won\'t be deleted. Are you sure?'],
];
@@ -0,0 +1,50 @@
import styled, { css } from 'reshadow';
import { Combobox } from '@cloudbeaver/core-blocks';
import type { CrsKey } from './LeafletMap';
const styles = css`
root {
display: inline-flex;
align-items: center;
font-size: 12px;
}
label {
margin-right: 4px;
flex-grow: 0;
flex-shrink: 1;
}
Combobox {
width: 120px;
flex: 0 0 auto;
}
`;
interface Props {
value: CrsKey;
onChange: (value: CrsKey) => void;
}
const items: CrsKey[] = [
'Simple',
'EPSG3395',
'EPSG3857',
'EPSG4326',
'EPSG900913',
];
export function CrsInput(props: Props) {
return styled(styles)(
<root>
<label>CRS:</label>
<Combobox
items={items}
value={props.value}
onSelect={props.onChange}
/>
</root>
);
}
@@ -5,18 +5,51 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { observer } from 'mobx-react-lite';
import { useCallback, useMemo } from 'react';
import { useCallback, useMemo, useState } from 'react';
import styled, { css } from 'reshadow';
import wellknown from 'wellknown';
import { TextPlaceholder } from '@cloudbeaver/core-blocks';
import { useTranslate } from '@cloudbeaver/core-localization';
import { IDatabaseResultSet, ResultSetSelectAction, IResultSetElementKey, IDatabaseDataModel, ResultSetViewAction, ResultSetDataKeysUtils } from '@cloudbeaver/plugin-data-viewer';
import { IGeoJSONFeature, IAssociatedValue, LeafletMap } from './LeafletMap';
import { CrsInput } from './CrsInput';
import { IGeoJSONFeature, IAssociatedValue, LeafletMap, CrsKey } from './LeafletMap';
import { ResultSetGISAction } from './ResultSetGISAction';
function getCrsKey(feature?: IGeoJSONFeature): CrsKey {
switch (feature?.properties.srid) {
case 3857:
return 'EPSG3857';
case 4326:
return 'EPSG4326';
case 3395:
return 'EPSG3395';
case 900913:
return 'EPSG900913';
default:
return 'EPSG3857';
}
}
const styles = css`
root {
display: flex;
flex-direction: column;
width: 100%;
}
map {
flex: 1 1 auto;
}
toolbar {
margin-top: 8px;
flex: 0 0 auto;
}
`;
interface Props {
model: IDatabaseDataModel<any, IDatabaseResultSet>;
resultIndex: number;
@@ -87,11 +120,25 @@ export const GISValuePresentation = observer<Props>(function GISValuePresentatio
return values;
}, [view]);
const defaultCrsKey = getCrsKey(parsedGISData[0]);
const [crsKey, setCrsKey] = useState(defaultCrsKey);
if (!parsedGISData.length) {
return <TextPlaceholder>{translate('gis_presentation_placeholder')}</TextPlaceholder>;
}
return (
<LeafletMap geoJSON={parsedGISData} getAssociatedValues={getAssociatedValues} />
return styled(styles)(
<root>
<map>
<LeafletMap key={crsKey} geoJSON={parsedGISData} crsKey={crsKey} getAssociatedValues={getAssociatedValues} />
</map>
<toolbar>
<CrsInput
value={crsKey}
onChange={setCrsKey}
/>
</toolbar>
</root>
);
});
@@ -42,8 +42,11 @@ interface IBaseTile extends TileLayerProps {
checked?: boolean;
}
export type CrsKey = 'Simple' | 'EPSG3857' | 'EPSG4326' | 'EPSG3395' | 'EPSG900913';
interface Props {
geoJSON: IGeoJSONFeature[];
crsKey: CrsKey;
getAssociatedValues: (cell: IResultSetElementKey) => IAssociatedValue[];
}
@@ -87,17 +90,17 @@ function polyStyle() {
};
}
function getCRS(feature?: IGeoJSONFeature): leaflet.CRS {
switch (feature?.properties.srid) {
case 0:
function getCRS(crsKey: CrsKey): leaflet.CRS {
switch (crsKey) {
case 'Simple':
return leaflet.CRS.Simple;
case 3857:
case 'EPSG3857':
return leaflet.CRS.EPSG3857;
case 4326:
case 'EPSG4326':
return leaflet.CRS.EPSG4326;
case 3395:
case 'EPSG3395':
return leaflet.CRS.EPSG3395;
case 900913:
case 'EPSG900913':
return leaflet.CRS.EPSG900913;
default:
return leaflet.CRS.EPSG3857;
@@ -111,14 +114,14 @@ const styles = css`
}
`;
export const LeafletMap: React.FC<Props> = function LeafletMap({ geoJSON, getAssociatedValues }) {
export const LeafletMap: React.FC<Props> = function LeafletMap({ geoJSON, crsKey, getAssociatedValues }) {
const splitContext = useSplit();
const translate = useTranslate();
const [mapRef, setMapRef] = useState<leaflet.Map | null>(null);
const [geoJSONLayerRef, setGeoJSONLayerRef] = useState<leaflet.GeoJSON | null>(null);
const crs = getCRS(geoJSON[0]);
const crs = getCRS(crsKey);
const onEachFeature = useCallback((feature: IGeoJSONFeature, layer: leaflet.Layer) => {
const associatedValues = getAssociatedValues(feature.properties.associatedCell);
@@ -409,8 +409,11 @@ export function useElementsTree(options: IOptions): IElementsTree {
this.select(node, false, false);
this.actions.execute({ type: 'show', nodeId });
});
}
if (path.length > 0) {
await functionsRef.loadTree(path[0]);
}
}
},
async expand(node: NavNode, state: boolean) {
const treeNodeState = this.state.get(node.id);
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { computed, makeObservable } from 'mobx';
import { computed, makeObservable, untracked } from 'mobx';
import {
ConnectionExecutionContextResource,
@@ -28,7 +28,7 @@ import {
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { Executor, ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/core-executor';
import { NavNodeManagerService, objectNavNodeProvider, NodeManagerUtils } from '@cloudbeaver/core-navigation-tree';
import { objectNavNodeProvider, NodeManagerUtils, NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import { CachedMapAllKey, NavNodeInfoFragment, ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk';
import { NavigationTabsService, TabHandler, ITab, ITabOptions } from '@cloudbeaver/plugin-navigation-tabs';
import { SqlResultTabsService, ISqlEditorTabState, SqlEditorService, SqlDataSourceService } from '@cloudbeaver/plugin-sql-editor';
@@ -55,7 +55,7 @@ export class SqlEditorTabService extends Bootstrap {
private readonly connectionExecutionContextService: ConnectionExecutionContextService,
private readonly connectionExecutionContextResource: ConnectionExecutionContextResource,
private readonly connectionInfoResource: ConnectionInfoResource,
private readonly navNodeManagerService: NavNodeManagerService,
private readonly navNodeInfoResource: NavNodeInfoResource,
private readonly sqlDataSourceService: SqlDataSourceService,
private readonly connectionsManagerService: ConnectionsManagerService,
private readonly containerResource: ContainerResource
@@ -190,12 +190,9 @@ export class SqlEditorTabService extends Bootstrap {
return;
}
const parents = NodeManagerUtils.parentsFromPath(nodeId);
const parent = this.navNodeManagerService.getNode(parents[0]);
const parents = this.navNodeInfoResource.getParents(nodeId);
if (parent) {
parents.unshift(parent.parentId);
}
untracked(() => this.navNodeInfoResource.load(nodeId!));
return {
nodeId,
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@cloudbeaver/product-default",
"sideEffects": false,
"version": "22.1.5",
"version": "22.2.0",
"description": "",
"license": "Apache-2.0",
"main": "lib/index.js",