diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/VirtualProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/VirtualProjectImpl.java index 1143f8a800..9ea63c2d8b 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/VirtualProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/VirtualProjectImpl.java @@ -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); } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceRegistryProxy.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceRegistryProxy.java index 89d9cc9905..eca241b2b3 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceRegistryProxy.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceRegistryProxy.java @@ -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 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 getDataSources() { - return super.getDataSources() + public List 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 getAllFolders() { + return dataSourceRegistry.getAllFolders(); + } + + @NotNull + @Override + public List 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 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 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 getAllAuthProfiles() { + return dataSourceRegistry.getAllAuthProfiles(); + } + + @NotNull + @Override + public List 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 getTemporaryFolders() { + return dataSourceRegistry.getTemporaryFolders(); + } + + @Override + public void loadDataSources( + @NotNull List 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 filter + ) { + dataSourceRegistry.saveConfigurationToManager(monitor, configurationManager, filter); + } + } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/fa/AbstractSessionFederated.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/fa/AbstractSessionFederated.java index 4b9edeca46..daffd15ed4 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/fa/AbstractSessionFederated.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/fa/AbstractSessionFederated.java @@ -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; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java index 9d45432cac..80d89ceeb2 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/rm/local/LocalResourceController.java @@ -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); diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java index c0f0872e8f..d2ce091a70 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/session/WebSession.java @@ -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; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebConnectionFolderUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebConnectionFolderUtils.java index c95ebd05d3..0ddb8dda88 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebConnectionFolderUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebConnectionFolderUtils.java @@ -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"); + } + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebTestUtils.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebTestUtils.java index 24b59dbc0a..17456db347 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebTestUtils.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/utils/WebTestUtils.java @@ -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 doPost(String apiUrl, String input, HttpClient client) throws Exception { - HttpRequest request = HttpRequest.newBuilder() + return doPostWithHeaders(apiUrl, input, client, List.of()); + } + + public static Map doPostWithHeaders( + String apiUrl, + String input, + HttpClient client, + List 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 response = client.send(request, HttpResponse.BodyHandlers.ofString()); diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index 54d2bb2dd2..c097dd45e4 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -198,6 +198,7 @@ type DriverInfo { embedded: Boolean enabled: Boolean! requiresServerName: Boolean + requiresDatabaseName: Boolean licenseRequired: Boolean license: String diff --git a/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls index 5750ad0fdf..2566fffcd8 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.navigator.graphqls @@ -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! diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java index b3b5ef96e3..8af429a492 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java @@ -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 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()); + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverConfig.java index fb0f5350ef..bc3795be16 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverConfig.java @@ -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); + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java index 87f562c63f..8fd830c197 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java @@ -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; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java index 445dcb24db..ab6e649038 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java @@ -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(); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java index 5afb3bb4ea..bd490aade0 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/WebNavigatorNodeInfo.java @@ -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 tempFolders = ((DataSourceRegistry) folder.getDataSourceRegistry()).getTemporaryFolders(); + Set tempFolders = folder.getDataSourceRegistry().getTemporaryFolders(); isShared = !projectName.equals(session.getUserId()) || tempFolders.contains(folder); } if (isShared) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java index 38d127ed38..5b01f9b60d 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/navigator/impl/WebServiceNavigator.java @@ -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 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; diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java index d781168a26..2fd74b31bd 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java @@ -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 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); diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java index fcb178b64b..a4a87437b4 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/RPSessionHandler.java @@ -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 userRoles = roles == null ? Collections.emptyList() : List.of(roles.split("\\|")); - SMSession authSession; if (userName != null) { try { Map credentials = new HashMap<>(); @@ -79,39 +73,18 @@ public class RPSessionHandler implements DBWSessionHandler { sessionParameters.put(SMConstants.SESSION_PARAM_TRUSTED_USER_ROLES, userRoles); Map 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); } diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthSession.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthSession.java index 9c6b98478b..bc876fcbdf 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthSession.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/auth/provider/local/LocalAuthSession.java @@ -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; diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java index 22adf77024..9a58c32163 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/AuthenticationTest.java @@ -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 sessionInfo = openSession(client); + Assert.assertTrue(JSONUtils.getBoolean(sessionInfo, "valid")); + Map activeUser = getActiveUser(client); + Assert.assertEquals(REVERSE_PROXY_TEST_USER, JSONUtils.getString(activeUser, "userId")); + } + + private Map openSession(HttpClient client) throws Exception { + Map data = doPostQuery(client, GQL_TEMPLATE_OPEN_SESSION); + if (data != null) { + return JSONUtils.getObject(data, "session"); + } + return Collections.emptyMap(); + } + + private Map getActiveUser(HttpClient client) throws Exception { + Map data = doPostQuery(client, GQL_TEMPLATE_ACTIVE_USER); + if (data != null) { + return JSONUtils.getObject(data, "user"); + } + return Collections.emptyMap(); + } + + private Map doPostQuery(HttpClient client, String gqlScript) throws Exception { + String input = WebTestUtils.readScriptTemplate(gqlScript, CEServerTestSuite.getScriptsPath()); + List headers = List.of(RPAuthProvider.X_USER, REVERSE_PROXY_TEST_USER, RPAuthProvider.X_ROLE, "user"); + Map map = WebTestUtils.doPostWithHeaders(CEServerTestSuite.GQL_API_URL, input, client, headers); + return JSONUtils.getObjectOrNull(map, "data"); + } + + } diff --git a/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf b/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf index e6d24f8063..f56cacd670 100644 --- a/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf +++ b/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf @@ -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: [ diff --git a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/activeUser.json b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/activeUser.json new file mode 100644 index 0000000000..97b079675a --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/activeUser.json @@ -0,0 +1,4 @@ +{ + "query": "\n query activeUser {\n user: activeUser {\n userId\n }\n}\n ", + "operationName": "activeUser" +} \ No newline at end of file diff --git a/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/openSession.json b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/openSession.json new file mode 100644 index 0000000000..eff24dcf81 --- /dev/null +++ b/server/test/io.cloudbeaver.test.platform/workspace/gql_scripts/openSession.json @@ -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" +} \ No newline at end of file diff --git a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx index 9c7fdc5ba4..b82dbb201f 100644 --- a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx @@ -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 = Omit, 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; diff --git a/webapp/packages/core-blocks/src/FormControls/isControlPresented.ts b/webapp/packages/core-blocks/src/FormControls/isControlPresented.ts index 83cecc2e83..a85a940a6a 100644 --- a/webapp/packages/core-blocks/src/FormControls/isControlPresented.ts +++ b/webapp/packages/core-blocks/src/FormControls/isControlPresented.ts @@ -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; } diff --git a/webapp/packages/core-connections/src/locales/en.ts b/webapp/packages/core-connections/src/locales/en.ts index a0184a1ddb..345d2d9669 100644 --- a/webapp/packages/core-connections/src/locales/en.ts +++ b/webapp/packages/core-connections/src/locales/en.ts @@ -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'], diff --git a/webapp/packages/core-connections/src/locales/it.ts b/webapp/packages/core-connections/src/locales/it.ts index e5e162ae56..459eac0296 100644 --- a/webapp/packages/core-connections/src/locales/it.ts +++ b/webapp/packages/core-connections/src/locales/it.ts @@ -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'], diff --git a/webapp/packages/core-connections/src/locales/ru.ts b/webapp/packages/core-connections/src/locales/ru.ts index 56f153f42d..95aba0ed5b 100644 --- a/webapp/packages/core-connections/src/locales/ru.ts +++ b/webapp/packages/core-connections/src/locales/ru.ts @@ -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', 'Хост'], diff --git a/webapp/packages/core-connections/src/locales/zh.ts b/webapp/packages/core-connections/src/locales/zh.ts index 1be926f601..bbba3eb1b5 100644 --- a/webapp/packages/core-connections/src/locales/zh.ts +++ b/webapp/packages/core-connections/src/locales/zh.ts @@ -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', '主机'], diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeInfoResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeInfoResource.ts index 6a7799571e..6323be187c 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeInfoResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeInfoResource.ts @@ -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 { 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 { } private async loadNodeInfo(nodePath: string): Promise { + 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 { 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; + }); } } diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts index 9c21828ff6..61541420be 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavNodeManagerService.ts @@ -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; diff --git a/webapp/packages/core-sdk/src/queries/database/navGetStructContainers.gql b/webapp/packages/core-sdk/src/queries/database/navGetStructContainers.gql index 392bd20f9a..fcf79e8d4c 100644 --- a/webapp/packages/core-sdk/src/queries/database/navGetStructContainers.gql +++ b/webapp/packages/core-sdk/src/queries/database/navGetStructContainers.gql @@ -11,14 +11,14 @@ query navGetStructContainers( ) { catalogList { catalog { - ...NavNodeInfo + ...NavNodeInfo } schemaList { - ...NavNodeInfo + ...NavNodeInfo } } schemaList { - ...NavNodeInfo + ...NavNodeInfo } supportsCatalogChange supportsSchemaChange diff --git a/webapp/packages/core-sdk/src/queries/navigation-tree/getNodeParents.gql b/webapp/packages/core-sdk/src/queries/navigation-tree/getNodeParents.gql new file mode 100644 index 0000000000..274d91c2a5 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/navigation-tree/getNodeParents.gql @@ -0,0 +1,11 @@ +query getNodeParents( + $nodePath: ID! + $withDetails: Boolean! +) { + node: navNodeInfo(nodePath: $nodePath) { + ...NavNodeInfo + } + parents: navNodeParents(nodePath: $nodePath) { + ...NavNodeInfo + } +} diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 80d165b11e..d88f4e2e26 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -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, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }, parents: Array<{ id: string, name?: string, hasChildren?: boolean, nodeType?: string, icon?: string, folder?: boolean, inline?: boolean, navigable?: boolean, features?: Array, object?: { features?: Array }, nodeDetails?: Array<{ id?: string, category?: string, dataType?: string, description?: string, displayName?: string, length: ObjectPropertyLength, features: Array, value?: any, order: number }> }> }; + export type NavDeleteNodesMutationVariables = Exact<{ nodePaths: Array | 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 { return withWrapper((wrappedRequestHeaders) => client.request(GetNavNodeFullNameDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getNavNodeFullName', 'query'); }, + getNodeParents(variables: GetNodeParentsQueryVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { + return withWrapper((wrappedRequestHeaders) => client.request(GetNodeParentsDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'getNodeParents', 'query'); + }, navDeleteNodes(variables: NavDeleteNodesMutationVariables, requestHeaders?: Dom.RequestInit["headers"]): Promise { return withWrapper((wrappedRequestHeaders) => client.request(NavDeleteNodesDocument, variables, {...requestHeaders, ...wrappedRequestHeaders}), 'navDeleteNodes', 'mutation'); }, diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index f41dcce11c..ece75f25a5 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -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; + readonly onLogout: Executor; + readonly onLogin: Executor; 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; } diff --git a/webapp/packages/plugin-authentication/src/PluginBootstrap.ts b/webapp/packages/plugin-authentication/src/PluginBootstrap.ts index 85ab02e640..1798fe6101 100644 --- a/webapp/packages/plugin-authentication/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-authentication/src/PluginBootstrap.ts @@ -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() } ), ]; } diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActions.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActions.tsx index 16131a77c3..0df75b7601 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActions.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/ConnectionFormBaseActions.tsx @@ -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 {onCancel && ( @@ -45,7 +40,7 @@ export const ConnectionFormBaseActions: PlaceholderComponent(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(); diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormProps.ts b/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormProps.ts index 7d2d79df06..f96456c19c 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormProps.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/IConnectionFormProps.ts @@ -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; readonly test: () => Promise; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts index 622e14d02a..54528cbd67 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ConnectionOptionsTabService.ts @@ -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; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx index 3f0ae0b60e..ba10f04288 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx @@ -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 = 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 = observe > {translate('connections_connection_name')} + 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')} + {!config.template && ( = observe disabled={disabled} autoComplete={`section-${config.driverId || 'driver'} section-folder`} mod='surface' + autoHide readOnly tiny fill diff --git a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts index dca837972e..5f1b4513b3 100644 --- a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts @@ -155,22 +155,30 @@ export class ConnectionFoldersBootstrap extends Bootstrap { }: INodeMoveData, contexts: IExecutionContextProvider ) { + 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 diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts index 6c15a4cf04..c53dc58370 100644 --- a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormService.ts @@ -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 { + async close(saved?: boolean): Promise { if (!this.formState) { - return; + return true; } if (saved) { @@ -101,6 +116,8 @@ export class PublicConnectionFormService { if (state) { this.clearFormState(); } + + return state; } async save(): Promise { @@ -147,6 +164,14 @@ export class PublicConnectionFormService { }; private readonly closeHandler: IExecutorHandler = async (data, contexts) => { + const confirmed = await this.showUnsavedChangesDialog(); + + if (!confirmed) { + ExecutorInterrupter.interrupt(contexts); + } + }; + + private async showUnsavedChangesDialog(): Promise { 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, { diff --git a/webapp/packages/plugin-connections/src/locales/en.ts b/webapp/packages/plugin-connections/src/locales/en.ts index 7586edd8c0..8f5395b2dd 100644 --- a/webapp/packages/plugin-connections/src/locales/en.ts +++ b/webapp/packages/plugin-connections/src/locales/en.ts @@ -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?'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/it.ts b/webapp/packages/plugin-connections/src/locales/it.ts index fc4b70d326..677ddf1984 100644 --- a/webapp/packages/plugin-connections/src/locales/it.ts +++ b/webapp/packages/plugin-connections/src/locales/it.ts @@ -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?'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/ru.ts b/webapp/packages/plugin-connections/src/locales/ru.ts index 1976281521..cee3925c3d 100644 --- a/webapp/packages/plugin-connections/src/locales/ru.ts +++ b/webapp/packages/plugin-connections/src/locales/ru.ts @@ -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}". Подключения не будут удалены. Вы уверены?'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/zh.ts b/webapp/packages/plugin-connections/src/locales/zh.ts index 5576999d6b..51abab77bc 100644 --- a/webapp/packages/plugin-connections/src/locales/zh.ts +++ b/webapp/packages/plugin-connections/src/locales/zh.ts @@ -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?'], ]; diff --git a/webapp/packages/plugin-gis-viewer/src/CrsInput.tsx b/webapp/packages/plugin-gis-viewer/src/CrsInput.tsx new file mode 100644 index 0000000000..d3e4836764 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/CrsInput.tsx @@ -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)( + + + + + ); +} \ No newline at end of file diff --git a/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx b/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx index 43e5322b1b..1c97f0ff2d 100644 --- a/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx +++ b/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx @@ -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; resultIndex: number; @@ -87,11 +120,25 @@ export const GISValuePresentation = observer(function GISValuePresentatio return values; }, [view]); + + const defaultCrsKey = getCrsKey(parsedGISData[0]); + const [crsKey, setCrsKey] = useState(defaultCrsKey); + if (!parsedGISData.length) { return {translate('gis_presentation_placeholder')}; } - return ( - + return styled(styles)( + + + + + + + + ); }); diff --git a/webapp/packages/plugin-gis-viewer/src/LeafletMap.tsx b/webapp/packages/plugin-gis-viewer/src/LeafletMap.tsx index bff6d0153f..0a7673ae2b 100644 --- a/webapp/packages/plugin-gis-viewer/src/LeafletMap.tsx +++ b/webapp/packages/plugin-gis-viewer/src/LeafletMap.tsx @@ -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 = function LeafletMap({ geoJSON, getAssociatedValues }) { +export const LeafletMap: React.FC = function LeafletMap({ geoJSON, crsKey, getAssociatedValues }) { const splitContext = useSplit(); const translate = useTranslate(); const [mapRef, setMapRef] = useState(null); const [geoJSONLayerRef, setGeoJSONLayerRef] = useState(null); - const crs = getCRS(geoJSON[0]); + const crs = getCRS(crsKey); const onEachFeature = useCallback((feature: IGeoJSONFeature, layer: leaflet.Layer) => { const associatedValues = getAssociatedValues(feature.properties.associatedCell); diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts index 311458fc9e..a60a5c551c 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts @@ -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); 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 31e4908114..8514e78176 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts @@ -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, diff --git a/webapp/packages/product-default/package.json b/webapp/packages/product-default/package.json index a68c97f2e5..262cb65f3f 100644 --- a/webapp/packages/product-default/package.json +++ b/webapp/packages/product-default/package.json @@ -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",