diff --git a/README.md b/README.md index 55487d8b31..68410f5437 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,17 @@ You can see a live demo of CloudBeaver here: https://demo.cloudbeaver.io ## Changelog +### 24.3.3. 2025-01-20 +- Administration: + - Added an ability to match users from LDAP configuration with CloudBeaver users; + - New environment variables were introduced for theme styling, SQL editor, and log viewer settings. You can use them for quick setup during the initial Docker configuration stage. +- General: + - An ability to show metadata object comments was added in the Navigator tree. You can enable it in the database navigator settings panel; + - Added transaction log viewing for connections with manual commit mode. This allows users to see all modifying statements before committing; + - Added an ability to import data in tables without unique keys; + - Added an ability to insert data in tables without unique keys; + - Added Ctrl + Shift + S (Cmd + Shift + S on Mac) shortcut for "Save As Script" action. + ### 24.3.2. 2025-01-06 - Added an ability to specify a user login as an attribute parameter for LDAP providers; - The collapse of the grouping panel doesn't lead to the full panel cleaning anymore. diff --git a/config/core/cloudbeaver.conf b/config/core/cloudbeaver.conf index 9ebad0078f..09311d9f16 100644 --- a/config/core/cloudbeaver.conf +++ b/config/core/cloudbeaver.conf @@ -13,16 +13,16 @@ productSettings: { # Global properties - core.theming.theme: 'light', - core.localization.localization: 'en', - plugin.sql-editor.autoSave: true, - plugin.sql-editor.disabled: false, + core.theming.theme: "${CLOUDBEAVER_CORE_THEMING_THEME:light}", + core.localization.localization: "${CLOUDBEAVER_CORE_LOCALIZATION:en}", + plugin.sql-editor.autoSave: "${CLOUDBEAVER_SQL_EDITOR_AUTOSAVE:true}", + plugin.sql-editor.disabled: "${CLOUDBEAVER_SQL_EDITOR_DISABLED:false}", # max size of the file that can be uploaded to the editor (in kilobytes) - plugin.sql-editor.maxFileSize: 10240, - plugin.log-viewer.disabled: false, - plugin.log-viewer.logBatchSize: 1000, - plugin.log-viewer.maxLogRecords: 2000, - sql.proposals.insert.table.alias: PLAIN + plugin.sql-editor.maxFileSize: "${CLOUDBEAVER_SQL_EDITOR_MAX_FILE_SIZE:10240}", + plugin.log-viewer.disabled: "${CLOUDBEAVER_LOG_VIEWER_DISABLED:false}", + plugin.log-viewer.logBatchSize: "${CLOUDBEAVER_LOG_VIEWER_LOG_BATCH_SIZE:1000}", + plugin.log-viewer.maxLogRecords: "${CLOUDBEAVER_LOG_VIEWER_MAX_LOG_RECORDS:2000}", + sql.proposals.insert.table.alias: "${CLOUDBEAVER_SQL_PROPOSALS_INSERT_TABLE_ALIAS:PLAIN}" }, expireSessionAfterPeriod: "${CLOUDBEAVER_EXPIRE_SESSION_AFTER_PERIOD:1800000}", diff --git a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF index a48f43888a..7a9283d66e 100644 --- a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Model Bundle-SymbolicName: io.cloudbeaver.model;singleton:=true -Bundle-Version: 1.0.69.qualifier -Bundle-Release-Date: 20250120 +Bundle-Version: 1.0.70.qualifier +Bundle-Release-Date: 20250203 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.model/pom.xml b/server/bundles/io.cloudbeaver.model/pom.xml index f08c6b5e93..0dede3d654 100644 --- a/server/bundles/io.cloudbeaver.model/pom.xml +++ b/server/bundles/io.cloudbeaver.model/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.model - 1.0.69-SNAPSHOT + 1.0.70-SNAPSHOT eclipse-plugin 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 4593e06f1c..437ff61684 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceRegistryProxy.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebDataSourceRegistryProxy.java @@ -44,10 +44,12 @@ import java.util.stream.Collectors; public class WebDataSourceRegistryProxy implements DBPDataSourceRegistry, DataSourcePersistentRegistry, DBPDataSourceRegistryCache { + @NotNull private final DataSourceFilter dataSourceFilter; + @NotNull private final DataSourceRegistry dataSourceRegistry; - public WebDataSourceRegistryProxy(DataSourceRegistry dataSourceRegistry, DataSourceFilter filter) { + public WebDataSourceRegistryProxy(@NotNull DataSourceRegistry dataSourceRegistry, @NotNull DataSourceFilter filter) { this.dataSourceRegistry = dataSourceRegistry; this.dataSourceFilter = filter; } @@ -62,7 +64,7 @@ public DBPProject getProject() { @Override public DBPDataSourceContainer getDataSource(@NotNull String id) { DBPDataSourceContainer dataSource = dataSourceRegistry.getDataSource(id); - if (dataSource == null || dataSourceFilter != null && !dataSourceFilter.filter(dataSource)) { + if (dataSource == null || !dataSourceFilter.filter(dataSource)) { return null; } return dataSource; @@ -71,7 +73,7 @@ public DBPDataSourceContainer getDataSource(@NotNull String id) { @Nullable @Override public DBPDataSourceContainer getDataSource(@NotNull DBPDataSource dataSource) { - if (dataSourceFilter != null && !dataSourceFilter.filter(dataSource.getContainer())) { + if (!dataSourceFilter.filter(dataSource.getContainer())) { return null; } return dataSourceRegistry.getDataSource(dataSource); @@ -81,10 +83,8 @@ public DBPDataSourceContainer getDataSource(@NotNull DBPDataSource dataSource) { @Override public DBPDataSourceContainer findDataSourceByName(String name) { var dataSource = dataSourceRegistry.findDataSourceByName(name); - if (dataSource != null) { - if (dataSourceFilter == null || dataSourceFilter.filter(dataSource)) { - return dataSource; - } + if (dataSource != null && dataSourceFilter.filter(dataSource)) { + return dataSource; } return null; } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java index 6058052231..8a1691cefc 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/WebProjectImpl.java @@ -90,6 +90,7 @@ protected DBPDataSourceRegistry createDataSourceRegistry() { ); } + @NotNull protected DataSourceRegistry createRMRegistry() { return new DataSourceRegistryRM<>(this, getResourceController(), preferenceStore); } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java index d590d9c24f..b5105b6229 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebConnectionInfo.java @@ -25,6 +25,7 @@ import io.cloudbeaver.utils.ServletAppUtils; import io.cloudbeaver.utils.WebCommonUtils; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.*; @@ -76,6 +77,8 @@ public class WebConnectionInfo { private String connectTime; private String serverVersion; private String clientVersion; + @Nullable + private Boolean credentialsSavedInSession; private transient Map savedAuthProperties; private transient List savedNetworkCredentials; @@ -195,7 +198,8 @@ public boolean isSaveCredentials() { @Property public boolean isCredentialsSaved() throws DBException { - return dataSourceContainer.isCredentialsSaved(); + // isCredentialsSaved can be true if credentials were saved during connection init for global project + return dataSourceContainer.isCredentialsSaved() && !(credentialsSavedInSession != null && credentialsSavedInSession); } @Property @@ -514,4 +518,10 @@ public List getTools() { return tools; } + /** + * Updates param that checks whether credentials were saved only in session. + */ + public void setCredentialsSavedInSession(@Nullable Boolean credentialsSavedInSession) { + this.credentialsSavedInSession = credentialsSavedInSession; + } } diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebDriverRegistry.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebDriverRegistry.java index 132d8806ff..6b01362c4d 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebDriverRegistry.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/registry/WebDriverRegistry.java @@ -19,11 +19,20 @@ import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IExtensionRegistry; import org.eclipse.core.runtime.Platform; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.connection.DBPDataSourceProviderDescriptor; import org.jkiss.dbeaver.model.connection.DBPDriver; +import org.jkiss.dbeaver.model.connection.DBPDriverLibrary; +import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.stream.Collectors; public class WebDriverRegistry { @@ -42,6 +51,7 @@ public synchronized static WebDriverRegistry getInstance() { return instance; } + private final List applicableDrivers = new ArrayList<>(); private final Set webDrivers = new HashSet<>(); protected WebDriverRegistry() { @@ -59,8 +69,48 @@ private void loadExtensions(IExtensionRegistry registry) { } } - public boolean isDriverEnabled(DBPDriver driver) { - return webDrivers.contains(driver.getFullId()); + public List getApplicableDrivers() { + return applicableDrivers; } + /** + * Updates info about applicable drivers (f.e. some changes were made in driver config file). + */ + public void refreshApplicableDrivers() { + this.applicableDrivers.clear(); + this.applicableDrivers.addAll( + DataSourceProviderRegistry.getInstance().getEnabledDataSourceProviders().stream() + .map(DBPDataSourceProviderDescriptor::getEnabledDrivers) + .flatMap(List::stream) + .filter(this::isDriverApplicable) + .toList()); + log.info("Available drivers: " + applicableDrivers.stream().map(DBPDriver::getFullName).collect(Collectors.joining(","))); + } + + protected boolean isDriverApplicable(@NotNull DBPDriver driver) { + List libraries = driver.getDriverLibraries(); + if (!webDrivers.contains(driver.getFullId())) { + return false; + } + boolean hasAllFiles = true; + for (DBPDriverLibrary lib : libraries) { + if (!isDriverLibraryFilePresent(lib)) { + hasAllFiles = false; + log.error("\tDriver '" + driver.getId() + "' is missing library '" + lib.getDisplayName() + "'"); + } else { + if (lib.getType() == DBPDriverLibrary.FileType.jar) { + return true; + } + } + } + return hasAllFiles; + } + + private boolean isDriverLibraryFilePresent(@NotNull DBPDriverLibrary lib) { + if (lib.getType() == DBPDriverLibrary.FileType.license) { + return true; + } + Path localFile = lib.getLocalFile(); + return localFile != null && Files.exists(localFile); + } } diff --git a/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF index af50c0b201..4524652f80 100644 --- a/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.product.ce/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Community Product Bundle-SymbolicName: io.cloudbeaver.product.ce;singleton:=true -Bundle-Version: 24.3.3.qualifier -Bundle-Release-Date: 20250120 +Bundle-Version: 24.3.4.qualifier +Bundle-Release-Date: 20250203 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.product.ce/pom.xml b/server/bundles/io.cloudbeaver.product.ce/pom.xml index 36c1de540e..a90ef8a0ac 100644 --- a/server/bundles/io.cloudbeaver.product.ce/pom.xml +++ b/server/bundles/io.cloudbeaver.product.ce/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.product.ce - 24.3.3-SNAPSHOT + 24.3.4-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF index 7d4e42c811..893ca43153 100644 --- a/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF @@ -2,8 +2,8 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: Base JDBC drivers Bundle-SymbolicName: io.cloudbeaver.resources.drivers.base;singleton:=true -Bundle-Version: 1.0.114.qualifier -Bundle-Release-Date: 20250120 +Bundle-Version: 1.0.115.qualifier +Bundle-Release-Date: 20250203 Bundle-Vendor: DBeaver Corp Bundle-ActivationPolicy: lazy Automatic-Module-Name: io.cloudbeaver.resources.drivers.base diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml b/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml index 94f017c29d..25da3f3030 100644 --- a/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml @@ -9,6 +9,6 @@ ../ io.cloudbeaver.resources.drivers.base - 1.0.114-SNAPSHOT + 1.0.115-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.server.ce/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.server.ce/META-INF/MANIFEST.MF index e039e9039e..9cf47a7d23 100644 --- a/server/bundles/io.cloudbeaver.server.ce/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.server.ce/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver CE Server Bundle-SymbolicName: io.cloudbeaver.server.ce;singleton:=true -Bundle-Version: 24.3.3.qualifier -Bundle-Release-Date: 20250120 +Bundle-Version: 24.3.4.qualifier +Bundle-Release-Date: 20250203 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-Activator: io.cloudbeaver.server.CBPlatformActivator diff --git a/server/bundles/io.cloudbeaver.server.ce/pom.xml b/server/bundles/io.cloudbeaver.server.ce/pom.xml index 7e0fa0293d..9f569b6f0c 100644 --- a/server/bundles/io.cloudbeaver.server.ce/pom.xml +++ b/server/bundles/io.cloudbeaver.server.ce/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.server.ce - 24.3.3-SNAPSHOT + 24.3.4-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplication.java index ebe007cceb..0f6df490d6 100644 --- a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBApplication.java @@ -735,9 +735,10 @@ protected void sendConfigChangedEvent(SMCredentialsProvider credentialsProvider) public abstract CBServerConfigurationController getServerConfigurationController(); private void refreshDisabledDriversConfig() { + getDriverRegistry().refreshApplicableDrivers(); CBAppConfig config = getAppConfiguration(); Set disabledDrivers = new LinkedHashSet<>(Arrays.asList(config.getDisabledDrivers())); - for (DBPDriver driver : CBPlatform.getInstance().getApplicableDrivers()) { + for (DBPDriver driver : getDriverRegistry().getApplicableDrivers()) { if (!driver.isEmbedded() || config.isDriverForceEnabled(driver.getFullId())) { continue; } diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBPlatform.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBPlatform.java index 3adab6265c..8477321ff6 100644 --- a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBPlatform.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/CBPlatform.java @@ -17,7 +17,6 @@ package io.cloudbeaver.server; -import io.cloudbeaver.auth.NoAuthCredentialsProvider; import io.cloudbeaver.server.jobs.SessionStateJob; import io.cloudbeaver.server.jobs.WebDataSourceMonitorJob; import io.cloudbeaver.server.jobs.WebSessionMonitorJob; @@ -26,22 +25,13 @@ import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.Log; -import org.jkiss.dbeaver.model.DBFileController; -import org.jkiss.dbeaver.model.connection.DBPDataSourceProviderDescriptor; -import org.jkiss.dbeaver.model.connection.DBPDriver; -import org.jkiss.dbeaver.model.connection.DBPDriverLibrary; import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore; import org.jkiss.dbeaver.model.runtime.AbstractJob; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; -import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.utils.IOUtils; import java.io.IOException; -import java.nio.file.Files; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; /** * CBPlatform @@ -57,7 +47,6 @@ public class CBPlatform extends BaseWebPlatform { private static CBApplication application = null; private WebServerPreferenceStore preferenceStore; - protected final List applicableDrivers = new ArrayList<>(); public static CBPlatform getInstance() { return (CBPlatform) DBWorkbench.getPlatform(); @@ -76,8 +65,6 @@ protected synchronized void initialize() { log.info("Initialize web platform...: "); this.preferenceStore = new WebServerPreferenceStore(WebPlatformActivator.getInstance().getPreferences()); super.initialize(); - refreshApplicableDrivers(); - scheduleServerJobs(); log.info("Web platform initialized (" + (System.currentTimeMillis() - startTime) + "ms)"); } @@ -124,10 +111,6 @@ public CBApplication getApplication() { return application; } - public List getApplicableDrivers() { - return applicableDrivers; - } - @NotNull @Override @@ -140,41 +123,4 @@ public boolean isShuttingDown() { return false; } - public void refreshApplicableDrivers() { - this.applicableDrivers.clear(); - - for (DBPDataSourceProviderDescriptor dspd : DataSourceProviderRegistry.getInstance().getEnabledDataSourceProviders()) { - for (DBPDriver driver : dspd.getEnabledDrivers()) { - List libraries = driver.getDriverLibraries(); - { - if (!application.getDriverRegistry().isDriverEnabled(driver)) { - continue; - } - boolean hasAllFiles = true, hasJars = false; - for (DBPDriverLibrary lib : libraries) { - if (!DBWorkbench.isDistributed() && !lib.isOptional() && lib.getType() != DBPDriverLibrary.FileType.license && - (lib.getLocalFile() == null || !Files.exists(lib.getLocalFile()))) - { - hasAllFiles = false; - log.error("\tDriver '" + driver.getId() + "' is missing library '" + lib.getDisplayName() + "'"); - } else { - if (lib.getType() == DBPDriverLibrary.FileType.jar) { - hasJars = true; - } - } - } - if (hasAllFiles || hasJars) { - applicableDrivers.add(driver); - } - } - } - } - log.info("Available drivers: " + applicableDrivers.stream().map(DBPDriver::getFullName).collect(Collectors.joining(","))); - } - - @NotNull - @Override - public DBFileController createFileController() { - return getApplication().createFileController(new NoAuthCredentialsProvider()); - } } diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java index a46ca14f99..9b367adbb8 100644 --- a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/server/events/WSObjectPermissionUpdatedEventHandler.java @@ -20,6 +20,7 @@ import io.cloudbeaver.model.session.BaseWebSession; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.WebAppUtils; import io.cloudbeaver.service.security.SMUtils; import io.cloudbeaver.utils.ServletAppUtils; import org.jkiss.code.NotNull; @@ -95,8 +96,18 @@ private Consumer getUpdateUserDataSourcesInfoConsumer( return; } var user = activeUserSession.getUserContext().getUser(); - var userSubjects = new HashSet<>(Set.of(user.getTeams())); - userSubjects.add(user.getUserId()); + Set userSubjects = new HashSet<>(); + if (user == null) { + String anonymousUserTeam = WebAppUtils.getWebApplication().getAppConfiguration().getAnonymousUserTeam(); + if (anonymousUserTeam == null) { + // cannot apply event for anonymous user is there are no user subjects + return; + } + userSubjects.add(anonymousUserTeam); + } else { + userSubjects.addAll(Set.of(user.getTeams())); + userSubjects.add(user.getUserId()); + } boolean shouldBeAccessible = dataSourcePermissions.stream().anyMatch(userSubjects::contains); List dataSources = List.of(dataSourceId); WebSessionGlobalProjectImpl project = webSession.getGlobalProject(); @@ -174,9 +185,4 @@ private Consumer getUpdateUserProjectsInfoConsumer( } }; } - - @Override - protected boolean isAcceptableInSession(@NotNull BaseWebSession activeUserSession, @NotNull WSObjectPermissionEvent event) { - return activeUserSession.getUserContext().getUser() != null && super.isAcceptableInSession(activeUserSession, event); - } } diff --git a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/session/CBSessionManager.java b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/session/CBSessionManager.java index c58b49eaf7..5c8da8e32b 100644 --- a/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/session/CBSessionManager.java +++ b/server/bundles/io.cloudbeaver.server.ce/src/io/cloudbeaver/service/session/CBSessionManager.java @@ -61,18 +61,26 @@ public CBSessionManager(CBApplication application) { @Override public BaseWebSession closeSession(@NotNull HttpServletRequest request) { HttpSession session = request.getSession(); - if (session != null) { - BaseWebSession webSession; - synchronized (sessionMap) { - webSession = sessionMap.remove(session.getId()); - } - if (webSession != null) { - log.debug("> Close session '" + session.getId() + "'"); - webSession.close(); - return webSession; - } + if (session == null) { + return null; } - return null; + + return closeSession(session.getId()); + } + + @Override + public BaseWebSession closeSession(@NotNull String sessionId) { + BaseWebSession webSession; + synchronized (sessionMap) { + webSession = sessionMap.remove(sessionId); + } + if (webSession == null) { + return null; + } + + log.debug("> Close session '" + sessionId + "'"); + webSession.close(); + return webSession; } protected CBApplication getApplication() { @@ -114,14 +122,14 @@ public WebSession getWebSession( var baseWebSession = sessionMap.get(sessionId); if (baseWebSession == null && CBApplication.getInstance().isConfigurationMode()) { try { - webSession = createWebSessionImpl(new WebHttpRequestInfo(request)); + webSession = createWebSession(new WebHttpRequestInfo(request)); } catch (DBException e) { throw new DBWebException("Failed to create web session", e); } sessionMap.put(sessionId, webSession); } else if (baseWebSession == null) { try { - webSession = createWebSessionImpl(new WebHttpRequestInfo(request)); + webSession = createWebSession(new WebHttpRequestInfo(request)); } catch (DBException e) { throw new DBWebException("Failed to create web session", e); } @@ -182,7 +190,7 @@ public WebSession getOrRestoreWebSession(@NotNull WebHttpRequestInfo requestInfo return null; } - webSession = createWebSessionImpl(requestInfo); + webSession = createWebSession(requestInfo); restorePreviousUserSession(webSession, oldAuthInfo); sessionMap.put(sessionId, webSession); @@ -216,7 +224,7 @@ private void restorePreviousUserSession( } @NotNull - protected WebSession createWebSessionImpl(@NotNull WebHttpRequestInfo request) throws DBException { + public WebSession createWebSession(@NotNull WebHttpRequestInfo request) throws DBException { return new WebSession(request, application, getSessionHandlers()); } diff --git a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF index 5afb38c730..9378453958 100644 --- a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Server Bundle-SymbolicName: io.cloudbeaver.server;singleton:=true -Bundle-Version: 24.3.3.qualifier -Bundle-Release-Date: 20250120 +Bundle-Version: 24.3.4.qualifier +Bundle-Release-Date: 20250203 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.server/pom.xml b/server/bundles/io.cloudbeaver.server/pom.xml index 6457e90907..f515e90d4e 100644 --- a/server/bundles/io.cloudbeaver.server/pom.xml +++ b/server/bundles/io.cloudbeaver.server/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.server - 24.3.3-SNAPSHOT + 24.3.4-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index ccd536c6a4..9b4603d8ec 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -261,6 +261,7 @@ type DriverInfo { configurationTypes: [DriverConfigurationType]! + downloadable: Boolean! @since(version: "24.3.3") driverInstalled: Boolean! driverLibraries: [DriverLibraryInfo!]! } @@ -269,6 +270,13 @@ type DriverLibraryInfo { id: ID! name: String! icon: String + libraryFiles: [DriverFileInfo!] +} + +type DriverFileInfo @since(version: "24.3.2") { + id: ID! + fileName: String! + icon: String } enum ResultDataFormat { diff --git a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls index b82da85fdd..04ad4a20ab 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.events.graphqls @@ -35,6 +35,8 @@ enum CBServerEventId { cb_database_output_log_updated, + cb_transaction_count @since(version: "24.3.3") + cb_session_task_info_updated @since(version: "24.3.1") } @@ -62,7 +64,9 @@ enum CBEventTopic { cb_session_task, @since(version: "24.3.1") cb_datasource_connection, - cb_delete_temp_folder + cb_delete_temp_folder, + + cb_transaction @since(version: "24.3.3") } # Base server event interface @@ -212,6 +216,16 @@ type WSDataSourceConnectEvent implements CBServerEvent { timestamp: Int! } +# Datasource count event in transactional mode +type WSTransactionalCountEvent implements CBServerEvent { + id: CBServerEventId! + topicId: CBEventTopic + contextId: String! + projectId: String! + connectionId: String! + transactionalCount: Int! +} + extend type Query { emptyEvent: Boolean } diff --git a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls index a1d12c3dd5..c9f8acd073 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.sql.graphqls @@ -223,6 +223,24 @@ type DynamicTraceProperty { description: String } +#################################################### +# Transactional info +#################################################### +type TransactionLogInfoItem { + id: Int! + time: DateTime! + type: String! + queryString: String! + durationMs: Int! + rows: Int! + result: String! +} +type TransactionLogInfos { + count: Int! + transactionLogInfos: [TransactionLogInfoItem!]! +} + + #################################################### # Query and Mutation #################################################### @@ -331,6 +349,12 @@ extend type Mutation { dataFormat: ResultDataFormat ): AsyncTaskInfo! + getTransactionLogInfo( + projectId: ID!, + connectionId: ID!, + contextId: ID! + ): TransactionLogInfos! + # Close results (free resources) sqlResultClose(projectId: ID, connectionId: ID!, contextId: ID!, resultId: ID!): Boolean! 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 a74ff5bafd..a3edc537b6 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java @@ -365,7 +365,7 @@ public static List getEnabledAuthProviders() { */ @NotNull public static Set getApplicableDriversIds() { - return WebAppUtils.getWebPlatform().getApplicableDrivers().stream() + return WebAppUtils.getWebApplication().getDriverRegistry().getApplicableDrivers().stream() .map(DBPDriver::getId) .collect(Collectors.toSet()); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverInfo.java index 47aabec821..5a620ea9c2 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDatabaseDriverInfo.java @@ -25,10 +25,7 @@ import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBConstants; -import org.jkiss.dbeaver.model.connection.DBPAuthModelDescriptor; -import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; -import org.jkiss.dbeaver.model.connection.DBPDriver; -import org.jkiss.dbeaver.model.connection.DBPDriverConfigurationType; +import org.jkiss.dbeaver.model.connection.*; import org.jkiss.dbeaver.model.impl.auth.AuthModelDatabaseNative; import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; @@ -53,7 +50,7 @@ public class WebDatabaseDriverInfo { public static final String URL_DATABASE_FIELD = ".*(?:\\{(?:database|file|folder)}).*"; private final WebSession webSession; private final DBPDriver driver; - private String id; + private final String id; public WebDatabaseDriverInfo(WebSession webSession, DBPDriver driver) { this.webSession = webSession; @@ -296,13 +293,18 @@ public boolean getRequiresDatabaseName() { @Property public WebDriverLibraryInfo[] getDriverLibraries() { return driver.getDriverLibraries().stream() - .map(dbpDriverLibrary -> new WebDriverLibraryInfo(webSession, dbpDriverLibrary)) + .map(dbpDriverLibrary -> new WebDriverLibraryInfo(driver, dbpDriverLibrary)) .toArray(WebDriverLibraryInfo[]::new); } @Property public boolean isDriverInstalled() { - return !driver.needsExternalDependencies(webSession.getProgressMonitor()); + return driver.isDriverInstalled(); + } + + @Property + public boolean isDownloadable() { + return driver.getDriverLibraries().stream().anyMatch(DBPDriverLibrary::isDownloadable); } @Property diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryFileInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryFileInfo.java new file mode 100644 index 0000000000..b04729f6de --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryFileInfo.java @@ -0,0 +1,53 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.model; + +import io.cloudbeaver.WebServiceUtils; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.DBIcon; +import org.jkiss.dbeaver.model.connection.DBPDriverLibrary; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.registry.driver.DriverDescriptor; + +public class WebDriverLibraryFileInfo { + + @NotNull + private final DriverDescriptor.DriverFileInfo fileInfo; + + public WebDriverLibraryFileInfo(@NotNull DriverDescriptor.DriverFileInfo fileInfo) { + this.fileInfo = fileInfo; + } + + + @Property + public String getId() { + return fileInfo.getId(); + } + + @Property + public String getFileName() { + return fileInfo.toString(); + } + + @Property + public String getIcon() { + if (fileInfo.getType() == DBPDriverLibrary.FileType.license) { + return WebServiceUtils.makeIconId(DBIcon.TYPE_TEXT); + } + return WebServiceUtils.makeIconId(DBIcon.JAR); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryInfo.java index fdf9b75d29..d8e3ad9844 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebDriverLibraryInfo.java @@ -17,18 +17,24 @@ package io.cloudbeaver.model; import io.cloudbeaver.WebServiceUtils; -import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.connection.DBPDriver; import org.jkiss.dbeaver.model.connection.DBPDriverLibrary; import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.registry.driver.DriverDescriptor; + +import java.util.List; public class WebDriverLibraryInfo { - private final WebSession webSession; + @NotNull + private final DBPDriver driver; + @NotNull private final DBPDriverLibrary driverLibrary; - public WebDriverLibraryInfo(@NotNull WebSession webSession, @NotNull DBPDriverLibrary driverLibrary) { - this.webSession = webSession; + public WebDriverLibraryInfo(@NotNull DBPDriver driver, @NotNull DBPDriverLibrary driverLibrary) { + this.driver = driver; this.driverLibrary = driverLibrary; } @@ -43,6 +49,18 @@ public String getName() { return driverLibrary.getDisplayName(); } + @Property + @Nullable + public List getLibraryFiles() { + var libraryFiles = ((DriverDescriptor) driver).getLibraryFiles(driverLibrary); + if (libraryFiles == null) { + return null; + } + return libraryFiles.stream() + .map(WebDriverLibraryFileInfo::new) + .toList(); + } + @Property public String getIcon() { return WebServiceUtils.makeIconId(driverLibrary.getIcon()); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebTransactionLogInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebTransactionLogInfo.java new file mode 100644 index 0000000000..651fb4ba64 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebTransactionLogInfo.java @@ -0,0 +1,24 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.model; + +import org.jkiss.code.NotNull; + +import java.util.List; + +public record WebTransactionLogInfo(@NotNull List transactionLogInfos, int count) { +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebTransactionLogItemInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebTransactionLogItemInfo.java new file mode 100644 index 0000000000..926387a5ea --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebTransactionLogItemInfo.java @@ -0,0 +1,31 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2025 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.model; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; + +public record WebTransactionLogItemInfo( + @Nullable Integer id, + @NotNull String time, + @NotNull String type, + @NotNull String queryString, + long durationMs, + long rows, + String result +) { +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/BaseWebPlatform.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/BaseWebPlatform.java index d12d76f740..7ce34b1661 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/BaseWebPlatform.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/BaseWebPlatform.java @@ -24,7 +24,6 @@ import org.jkiss.dbeaver.model.DBConstants; import org.jkiss.dbeaver.model.app.DBACertificateStorage; import org.jkiss.dbeaver.model.app.DBPWorkspace; -import org.jkiss.dbeaver.model.connection.DBPDriver; import org.jkiss.dbeaver.model.impl.app.DefaultCertificateStorage; import org.jkiss.dbeaver.model.qm.QMRegistry; import org.jkiss.dbeaver.model.qm.QMUtils; @@ -41,7 +40,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; public abstract class BaseWebPlatform extends BasePlatformImpl { private static final Log log = Log.getLog(BaseWebPlatform.class); @@ -166,5 +164,4 @@ public QMRegistry getQueryManager() { return queryManager; } - public abstract List getApplicableDrivers(); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebAppSessionManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebAppSessionManager.java index 9e54be6766..75184af3de 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebAppSessionManager.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebAppSessionManager.java @@ -32,6 +32,8 @@ public interface WebAppSessionManager { BaseWebSession closeSession(@NotNull HttpServletRequest request); + BaseWebSession closeSession(@NotNull String sessionId); + @NotNull WebSession getWebSession( @NotNull HttpServletRequest request, diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java index c03064c1ac..2549fa64c5 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java @@ -121,8 +121,8 @@ WebConnectionInfo initConnection( @NotNull String connectionId, @NotNull Map authProperties, @Nullable List networkCredentials, - @Nullable Boolean saveCredentials, - @Nullable Boolean sharedCredentials, + boolean saveCredentials, + boolean sharedCredentials, @Nullable String selectedCredentials ) throws DBWebException; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java index 6f416ec19b..c310b95d33 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java @@ -24,7 +24,6 @@ import io.cloudbeaver.model.WebConnectionConfig; import io.cloudbeaver.model.WebNetworkHandlerConfigInput; import io.cloudbeaver.model.session.WebSession; -import io.cloudbeaver.server.BaseWebPlatform; import io.cloudbeaver.server.WebAppSessionManager; import io.cloudbeaver.server.WebAppUtils; import io.cloudbeaver.server.graphql.GraphQLEndpoint; @@ -33,6 +32,7 @@ import io.cloudbeaver.service.core.impl.WebServiceCore; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jkiss.utils.CommonUtils; import java.util.Collections; import java.util.List; @@ -134,8 +134,8 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { env.getArgument("id"), env.getArgument("credentials"), nhc, - env.getArgument("saveCredentials"), - env.getArgument("sharedCredentials"), + CommonUtils.toBoolean(env.getArgument("saveCredentials")), + CommonUtils.toBoolean(env.getArgument("sharedCredentials")), env.getArgument("selectedSecretId") ); } 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 4461424b01..f08c41b0e4 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 @@ -44,6 +44,7 @@ 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.exec.DBCConnectException; import org.jkiss.dbeaver.model.navigator.*; import org.jkiss.dbeaver.model.net.DBWHandlerConfiguration; import org.jkiss.dbeaver.model.net.DBWHandlerType; @@ -62,6 +63,7 @@ import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor; import org.jkiss.dbeaver.registry.network.NetworkHandlerRegistry; import org.jkiss.dbeaver.registry.settings.ProductSettingsRegistry; +import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.runtime.jobs.ConnectionTestJob; import org.jkiss.dbeaver.utils.RuntimeUtils; import org.jkiss.utils.CommonUtils; @@ -84,7 +86,7 @@ public WebServerConfig getServerConfig() { @Override public List getDriverList(@NotNull WebSession webSession, String driverId) { List result = new ArrayList<>(); - for (DBPDriver driver : WebAppUtils.getWebPlatform().getApplicableDrivers()) { + for (DBPDriver driver : WebAppUtils.getWebApplication().getDriverRegistry().getApplicableDrivers()) { if (driverId == null || driverId.equals(driver.getFullId())) { result.add(new WebDatabaseDriverInfo(webSession, driver)); } @@ -141,7 +143,7 @@ public List getTemplateDataSources() throws DBWebException for (DBPDataSourceContainer ds : dsRegistry.getDataSources()) { if (ds.isTemplate()) { - if (WebAppUtils.getWebPlatform().getApplicableDrivers().contains(ds.getDriver())) { + if (WebAppUtils.getWebApplication().getDriverRegistry().getApplicableDrivers().contains(ds.getDriver())) { result.add(new WebDataSourceConfig(ds)); } else { log.debug("Template datasource '" + ds.getName() + "' ignored - driver is not applicable"); @@ -180,7 +182,7 @@ private void getTemplateConnectionsFromProject( for (DBPDataSourceContainer ds : registry.getDataSources()) { if (ds.isTemplate() && project.getDataSourceFilter().filter(ds) && - WebAppUtils.getWebPlatform().getApplicableDrivers().contains(ds.getDriver())) { + WebAppUtils.getWebApplication().getDriverRegistry().getApplicableDrivers().contains(ds.getDriver())) { result.add(new WebConnectionInfo(webSession, ds)); } } @@ -337,8 +339,8 @@ public WebConnectionInfo initConnection( @NotNull String connectionId, @NotNull Map authProperties, @Nullable List networkCredentials, - @Nullable Boolean saveCredentials, - @Nullable Boolean sharedCredentials, + boolean saveCredentials, + boolean sharedCredentials, @Nullable String selectedSecretId ) throws DBWebException { WebConnectionInfo connectionInfo = WebDataSourceUtils.getWebConnectionInfo(webSession, projectId, connectionId); @@ -366,8 +368,10 @@ public WebConnectionInfo initConnection( } boolean oldSavePassword = dataSourceContainer.isSavePassword(); + DBRProgressMonitor monitor = webSession.getProgressMonitor(); + validateDriverLibrariesPresence(dataSourceContainer); try { - boolean connect = dataSourceContainer.connect(webSession.getProgressMonitor(), true, false); + boolean connect = dataSourceContainer.connect(monitor, true, false); if (connect) { webSession.addSessionEvent( new WSDataSourceConnectEvent( @@ -404,14 +408,14 @@ public WebConnectionInfo initConnection( } }); } - if (saveCredentials != null && saveCredentials) { + if (saveCredentials) { // Save all passed credentials in the datasource container WebServiceUtils.saveAuthProperties( dataSourceContainer, dataSourceContainer.getConnectionConfiguration(), authProperties, true, - sharedCredentials == null ? false : sharedCredentials + sharedCredentials ); var project = dataSourceContainer.getProject(); @@ -432,6 +436,9 @@ public WebConnectionInfo initConnection( } if (WebServiceUtils.isGlobalProject(dataSourceContainer.getProject())) { // Do not flush config for global project (only admin can do it - CB-2415) + if (saveCredentials) { + connectionInfo.setCredentialsSavedInSession(true); + } saveConfig[0] = false; } if (saveConfig[0]) { @@ -552,6 +559,7 @@ public WebConnectionInfo updateConnection( config.isSharedCredentials() ); } + connectionInfo.setCredentialsSavedInSession(null); WSDataSourceProperty property = getDatasourceEventProperty(oldDataSource, dataSource); @@ -755,6 +763,7 @@ public WebConnectionInfo testConnection( testDataSource = (DataSourceDescriptor) WebServiceUtils.createConnectionFromConfig(connectionConfig, sessionRegistry); } + validateDriverLibrariesPresence(testDataSource); webSession.provideAuthParameters(webSession.getProgressMonitor(), testDataSource, testDataSource.getConnectionConfiguration()); @@ -767,6 +776,12 @@ public WebConnectionInfo testConnection( }); ct.run(webSession.getProgressMonitor()); if (ct.getConnectError() != null) { + if (ct.getConnectError() instanceof DBCConnectException error) { + Throwable rootCause = CommonUtils.getRootCause(error); + if (rootCause instanceof ClassNotFoundException) { + throwDriverNotFoundException(testDataSource); + } + } throw new DBWebException("Connection failed", ct.getConnectError()); } WebConnectionInfo connectionInfo = new WebConnectionInfo(webSession, testDataSource); @@ -1016,4 +1031,16 @@ private WebSessionProjectImpl getProjectById(WebSession webSession, String proje } return project; } + + private void validateDriverLibrariesPresence(@NotNull DBPDataSourceContainer container) throws DBWebException { + if (!DBWorkbench.isDistributed() && container.getDriver().needsExternalDependencies()) { + throwDriverNotFoundException(container); + } + } + + @NotNull + private static String throwDriverNotFoundException(@NotNull DBPDataSourceContainer container) throws DBWebException { + throw new DBWebException("Driver files for %s are not found. Please ask the administrator to download it." + .formatted(container.getDriver().getName())); + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java index a7d5a95fc1..8685f096da 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/DBWServiceSQL.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others + * Copyright (C) 2010-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,7 @@ import io.cloudbeaver.WebAction; import io.cloudbeaver.model.WebAsyncTaskInfo; import io.cloudbeaver.model.WebConnectionInfo; +import io.cloudbeaver.model.WebTransactionLogInfo; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.DBWService; import org.jkiss.code.NotNull; @@ -202,4 +203,10 @@ WebAsyncTaskInfo asyncSqlRollbackTransaction( WebAsyncTaskInfo asyncSqlCommitTransaction( @NotNull WebSession webSession, @NotNull WebSQLContextInfo sqlContext); + + @WebAction + WebTransactionLogInfo getTransactionLogInfo( + @NotNull WebSession webSession, + @NotNull WebSQLContextInfo sqlContext + ); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java index f0115c6cef..4ad2905dba 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLContextInfo.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others + * Copyright (C) 2010-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,22 +26,36 @@ import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBConstants; import org.jkiss.dbeaver.model.DBUtils; import org.jkiss.dbeaver.model.data.DBDAttributeBinding; import org.jkiss.dbeaver.model.exec.*; import org.jkiss.dbeaver.model.exec.trace.DBCTrace; +import org.jkiss.dbeaver.model.messages.ModelMessages; import org.jkiss.dbeaver.model.meta.Property; +import io.cloudbeaver.model.WebTransactionLogInfo; +import io.cloudbeaver.model.WebTransactionLogItemInfo; import org.jkiss.dbeaver.model.qm.QMTransactionState; import org.jkiss.dbeaver.model.qm.QMUtils; +import org.jkiss.dbeaver.model.qm.meta.QMMConnectionInfo; +import org.jkiss.dbeaver.model.qm.meta.QMMStatementExecuteInfo; +import org.jkiss.dbeaver.model.qm.meta.QMMTransactionInfo; +import org.jkiss.dbeaver.model.qm.meta.QMMTransactionSavepointInfo; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.struct.DBSDataContainer; import org.jkiss.dbeaver.model.struct.rdb.DBSCatalog; import org.jkiss.dbeaver.model.struct.rdb.DBSSchema; +import org.jkiss.dbeaver.model.websocket.event.WSTransactionalCountEvent; import org.jkiss.dbeaver.utils.RuntimeUtils; import org.jkiss.utils.CommonUtils; import java.lang.reflect.InvocationTargetException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; @@ -59,6 +73,9 @@ public class WebSQLContextInfo implements WebSessionProvider { private final AtomicInteger resultId = new AtomicInteger(); + public static final DateTimeFormatter ISO_DATE_FORMAT = DateTimeFormatter.ofPattern(DBConstants.DEFAULT_ISO_TIMESTAMP_FORMAT) + .withZone(ZoneId.of("UTC")); + public WebSQLContextInfo( WebSQLProcessor processor, String id, String catalogName, String schemaName, String projectId ) throws DBCException { @@ -216,6 +233,70 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException, In } + public WebTransactionLogInfo getTransactionLogInfo() { + DBCExecutionContext context = processor.getExecutionContext(); + return getTransactionLogInfo(context); + } + + @NotNull + private WebTransactionLogInfo getTransactionLogInfo(DBCExecutionContext executionContext) { + int updateCount = 0; + List logItemInfos = new ArrayList<>(); + QMMConnectionInfo sessionInfo = QMUtils.getCurrentConnection(executionContext); + if (sessionInfo.isTransactional()) { + QMMTransactionInfo txnInfo = sessionInfo.getTransaction(); + if (txnInfo != null) { + QMMTransactionSavepointInfo sp = txnInfo.getCurrentSavepoint(); + QMMStatementExecuteInfo execInfo = sp.getLastExecute(); + for (QMMStatementExecuteInfo exec = execInfo; exec != null && exec.getSavepoint() == sp; exec = exec.getPrevious()) { + if (exec.getUpdateRowCount() > 0 ) { + DBCExecutionPurpose purpose = exec.getStatement().getPurpose(); + if (!exec.hasError() && purpose != DBCExecutionPurpose.META && purpose != DBCExecutionPurpose.UTIL) { + updateCount++; + } + generateLogInfo(logItemInfos, exec, purpose, updateCount); + } + } + } + } else { + QMMStatementExecuteInfo execInfo = sessionInfo.getExecutionStack(); + for (QMMStatementExecuteInfo exec = execInfo; exec != null; exec = exec.getPrevious()) { + if (exec.getUpdateRowCount() > 0) { + updateCount++; + DBCExecutionPurpose purpose = exec.getStatement().getPurpose(); + generateLogInfo(logItemInfos, exec, purpose, updateCount); + } + } + } + return new WebTransactionLogInfo(logItemInfos, updateCount); + } + + private void generateLogInfo( + @NotNull List logItemInfos, + @NotNull QMMStatementExecuteInfo exec, + @NotNull DBCExecutionPurpose purpose, + int id + ) { + String type = "SQL / " + purpose.getTitle(); + String dateTime = ISO_DATE_FORMAT.format(Instant.ofEpochMilli(exec.getCloseTime())); + String result = ModelMessages.controls_querylog_success; + if (exec.hasError()) { + if (exec.getErrorCode() == 0) { + result = exec.getErrorMessage(); + } else if (exec.getErrorMessage() == null) { + result = ModelMessages.controls_querylog_error + exec.getErrorCode() + "]"; //$NON-NLS-1$ + } else { + result = "[" + exec.getErrorCode() + "] " + exec.getErrorMessage(); //$NON-NLS-1$ //$NON-NLS-2$ + } + } + + logItemInfos.add( + new WebTransactionLogItemInfo(id, dateTime, type, exec.getQueryString(), + exec.getDuration(), exec.getUpdateRowCount(), result) + ); + } + + public WebAsyncTaskInfo commitTransaction() { DBCExecutionContext context = processor.getExecutionContext(); DBCTransactionManager txnManager = DBUtils.getTransactionManager(context); @@ -238,6 +319,17 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException, In RuntimeUtils.formatExecutionTime(System.currentTimeMillis() - txnInfo.getTransactionStartTime()) ); } + processor.getWebSession().addSessionEvent( + new WSTransactionalCountEvent( + processor.getWebSession().getSessionId(), + processor.getWebSession().getUserId(), + getProjectId(), + getId(), + getConnectionId(), + 0 + ) + ); + } }; return getWebSession().createAndRunAsyncTask("Commit transaction", runnable); @@ -265,6 +357,16 @@ public void run(DBRProgressMonitor monitor) throws InvocationTargetException, In txnInfo.getUpdateCount(), RuntimeUtils.formatExecutionTime(System.currentTimeMillis() - txnInfo.getTransactionStartTime()) ); + processor.getWebSession().addSessionEvent( + new WSTransactionalCountEvent( + processor.getWebSession().getSessionId(), + processor.getWebSession().getUserId(), + getProjectId(), + getId(), + getConnectionId(), + 0 + ) + ); } } }; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java index d0f55ca283..e7e3ba42c4 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLProcessor.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others + * Copyright (C) 2010-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,12 +41,14 @@ import org.jkiss.dbeaver.model.impl.DefaultServerOutputReader; import org.jkiss.dbeaver.model.navigator.DBNDatabaseItem; import org.jkiss.dbeaver.model.navigator.DBNNode; +import org.jkiss.dbeaver.model.qm.QMUtils; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.sql.*; import org.jkiss.dbeaver.model.sql.parser.SQLParserContext; import org.jkiss.dbeaver.model.sql.parser.SQLRuleManager; import org.jkiss.dbeaver.model.sql.parser.SQLScriptParser; import org.jkiss.dbeaver.model.struct.*; +import org.jkiss.dbeaver.model.websocket.event.WSTransactionalCountEvent; import org.jkiss.dbeaver.utils.GeneralUtils; import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; @@ -164,7 +166,7 @@ public WebSQLExecuteInfo processQuery( @Nullable WebSQLDataFilter filter, @Nullable WebDataFormat dataFormat, @NotNull WebSession webSession, - boolean readLogs) throws DBWebException { + boolean readLogs) throws DBWebException, DBCException { if (filter == null) { // Use default filter filter = new WebSQLDataFilter(); @@ -270,6 +272,11 @@ public WebSQLExecuteInfo processQuery( } catch (DBException e) { throw new DBWebException("Error executing query", e); } + DBCTransactionManager txnManager = DBUtils.getTransactionManager(context); + if (txnManager != null && !txnManager.isAutoCommit()) { + sendTransactionalEvent(contextInfo); + } + executeInfo.setDuration(System.currentTimeMillis() - startTime); if (executeInfo.getResults().length == 0) { executeInfo.setStatusMessage("No Data"); @@ -354,17 +361,17 @@ public WebSQLExecuteInfo updateResultsDataBatch( WebSQLExecuteInfo result = new WebSQLExecuteInfo(); List queryResults = new ArrayList<>(); + boolean isAutoCommitEnabled = true; + for (var rowIdentifier : rowIdentifierList) { Map resultBatches = new LinkedHashMap<>(); DBSDataManipulator dataManipulator = generateUpdateResultsDataBatch( monitor, resultsInfo, rowIdentifier, updatedRows, deletedRows, addedRows, dataFormat, resultBatches, keyReceiver); - DBCExecutionContext executionContext = getExecutionContext(dataManipulator); try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.USER, "Update data in container")) { DBCTransactionManager txnManager = DBUtils.getTransactionManager(executionContext); boolean revertToAutoCommit = false; - boolean isAutoCommitEnabled = true; DBCSavepoint savepoint = null; if (txnManager != null) { isAutoCommitEnabled = txnManager.isAutoCommit(); @@ -421,6 +428,10 @@ public WebSQLExecuteInfo updateResultsDataBatch( } getUpdatedRowsInfo(resultsInfo, newResultSetRows, dataFormat, monitor); + if (!isAutoCommitEnabled) { + sendTransactionalEvent(contextInfo); + } + WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet(); updatedResultSet.setResultsInfo(resultsInfo); updatedResultSet.setColumns(resultsInfo.getAttributes()); @@ -437,6 +448,20 @@ public WebSQLExecuteInfo updateResultsDataBatch( return result; } + private void sendTransactionalEvent(WebSQLContextInfo contextInfo) { + int count = QMUtils.getTransactionState(getExecutionContext()).getUpdateCount(); + webSession.addSessionEvent( + new WSTransactionalCountEvent( + contextInfo.getWebSession().getSessionId(), + contextInfo.getWebSession().getUserId(), + contextInfo.getProjectId(), + contextInfo.getId(), + contextInfo.getConnectionId(), + count + ) + ); + } + private void getUpdatedRowsInfo( @NotNull WebSQLResultsInfo resultsInfo, @NotNull Set newResultSetRows, diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java index c183c7d350..108eedd137 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebServiceBindingSQL.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others + * Copyright (C) 2010-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -226,6 +226,11 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { getWebSession(env), getSQLContext(env) )) + .dataFetcher("getTransactionLogInfo", env -> + getService(env).getTransactionLogInfo( + getWebSession(env), + getSQLContext(env) + )) .dataFetcher("asyncSqlRollbackTransaction", env -> getService(env).asyncSqlRollbackTransaction( getWebSession(env), diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java index 0308509dec..353040434f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/impl/WebServiceSQL.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others + * Copyright (C) 2010-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,6 +42,7 @@ import org.jkiss.dbeaver.model.impl.sql.BasicSQLDialect; import org.jkiss.dbeaver.model.navigator.DBNModel; import org.jkiss.dbeaver.model.navigator.DBNNode; +import io.cloudbeaver.model.WebTransactionLogInfo; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.sql.*; import org.jkiss.dbeaver.model.sql.completion.SQLCompletionAnalyzer; @@ -615,4 +616,8 @@ public WebAsyncTaskInfo asyncSqlCommitTransaction(@NotNull WebSession webSession return contextInfo.commitTransaction(); } + @Override + public WebTransactionLogInfo getTransactionLogInfo(@NotNull WebSession webSession, @NotNull WebSQLContextInfo sqlContext) { + return sqlContext.getTransactionLogInfo(); + } } diff --git a/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF index d6070834ef..4aa3d40fd1 100644 --- a/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.admin/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Administration Bundle-SymbolicName: io.cloudbeaver.service.admin;singleton:=true -Bundle-Version: 1.0.113.qualifier -Bundle-Release-Date: 20250120 +Bundle-Version: 1.0.114.qualifier +Bundle-Release-Date: 20250203 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.admin/pom.xml b/server/bundles/io.cloudbeaver.service.admin/pom.xml index 3592331641..ad68ec2bed 100644 --- a/server/bundles/io.cloudbeaver.service.admin/pom.xml +++ b/server/bundles/io.cloudbeaver.service.admin/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.admin - 1.0.113-SNAPSHOT + 1.0.114-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/ConnectionSearcher.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/ConnectionSearcher.java index 8292df8c2c..0f2df31b78 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/ConnectionSearcher.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/ConnectionSearcher.java @@ -21,6 +21,7 @@ import io.cloudbeaver.model.utils.ConfigurationUtils; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.server.WebAppUtils; import io.cloudbeaver.service.admin.AdminConnectionSearchInfo; import org.jkiss.dbeaver.model.connection.DBPDriver; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; @@ -44,14 +45,15 @@ public class ConnectionSearcher implements DBRRunnableWithProgress { private final WebSession webSession; private final String[] hostNames; private final List foundConnections = new ArrayList<>(); - private List availableDrivers = new ArrayList<>(); public ConnectionSearcher(WebSession webSession, String[] hostNames) { this.webSession = webSession; this.hostNames = hostNames; - this.availableDrivers.addAll(CBPlatform.getInstance().getApplicableDrivers()); } + /** + * Returns all found connections in a current machine. + */ public List getFoundConnections() { synchronized (foundConnections) { return new ArrayList<>(foundConnections); @@ -107,7 +109,7 @@ private void searchConnections(DBRProgressMonitor monitor, String hostName, Stri int checkTimeout = 150; Map portCache = new HashMap<>(); - for (DBPDriver driver : availableDrivers) { + for (DBPDriver driver : WebAppUtils.getWebApplication().getDriverRegistry().getApplicableDrivers()) { monitor.subTask("Check '" + driver.getName() + "' on '" + hostName + "'"); if (!CommonUtils.isEmpty(driver.getDefaultPort()) && !isPortInBlockList(CommonUtils.toInt(driver.getDefaultPort()))) { updatePortInfo(portCache, hostName, displayName, driver, checkTimeout); 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 96e1fab90a..f499ff68a2 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 @@ -30,7 +30,7 @@ import io.cloudbeaver.registry.*; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBConstants; -import io.cloudbeaver.server.CBPlatform; +import io.cloudbeaver.server.WebAppUtils; import io.cloudbeaver.service.DBWServiceServerConfigurator; import io.cloudbeaver.service.admin.*; import io.cloudbeaver.service.security.SMUtils; @@ -633,7 +633,7 @@ public boolean configureServer(WebSession webSession, Map params // Just reload session state webSession.refreshUserData(); } - CBPlatform.getInstance().refreshApplicableDrivers(); + WebAppUtils.getWebApplication().getDriverRegistry().refreshApplicableDrivers(); } catch (Throwable e) { throw new DBWebException("Error configuring server", e); } diff --git a/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF index cb555cbe70..fc3a19ebfe 100644 --- a/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.auth/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Authentication Bundle-SymbolicName: io.cloudbeaver.service.auth;singleton:=true -Bundle-Version: 1.0.113.qualifier -Bundle-Release-Date: 20250120 +Bundle-Version: 1.0.114.qualifier +Bundle-Release-Date: 20250203 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.auth/pom.xml b/server/bundles/io.cloudbeaver.service.auth/pom.xml index b715d8fdbe..e138fbb720 100644 --- a/server/bundles/io.cloudbeaver.service.auth/pom.xml +++ b/server/bundles/io.cloudbeaver.service.auth/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.auth - 1.0.113-SNAPSHOT + 1.0.114-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF index cdf1419202..2c067f0253 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.data.transfer/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Data Transfer Bundle-SymbolicName: io.cloudbeaver.service.data.transfer;singleton:=true -Bundle-Version: 1.0.114.qualifier -Bundle-Release-Date: 20250120 +Bundle-Version: 1.0.115.qualifier +Bundle-Release-Date: 20250203 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml b/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml index 95a8175f25..7d9407994c 100644 --- a/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml +++ b/server/bundles/io.cloudbeaver.service.data.transfer/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.data.transfer - 1.0.114-SNAPSHOT + 1.0.115-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF index 204ab1bf4a..717f201a70 100644 --- a/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.fs/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - File System Bundle-SymbolicName: io.cloudbeaver.service.fs;singleton:=true -Bundle-Version: 1.0.31.qualifier -Bundle-Release-Date: 20250120 +Bundle-Version: 1.0.32.qualifier +Bundle-Release-Date: 20250203 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.fs/pom.xml b/server/bundles/io.cloudbeaver.service.fs/pom.xml index e5a90cddec..6af654897c 100644 --- a/server/bundles/io.cloudbeaver.service.fs/pom.xml +++ b/server/bundles/io.cloudbeaver.service.fs/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.fs - 1.0.31-SNAPSHOT + 1.0.32-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml b/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml index ed76a9973b..bf59248e18 100644 --- a/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml @@ -38,6 +38,13 @@ user="true" encryption="plain"/> + + + + + + diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java index a10626b6c4..f50a517ae2 100644 --- a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others + * Copyright (C) 2010-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,9 @@ package io.cloudbeaver.service.ldap.auth; import io.cloudbeaver.DBWUserIdentity; +import io.cloudbeaver.auth.SMAuthProviderAssigner; import io.cloudbeaver.auth.SMAuthProviderExternal; +import io.cloudbeaver.auth.SMAutoAssign; import io.cloudbeaver.auth.SMBruteForceProtected; import io.cloudbeaver.auth.provider.local.LocalAuthProviderConstants; import io.cloudbeaver.model.session.WebSession; @@ -43,7 +45,7 @@ import java.util.Map; import java.util.UUID; -public class LdapAuthProvider implements SMAuthProviderExternal, SMBruteForceProtected { +public class LdapAuthProvider implements SMAuthProviderExternal, SMBruteForceProtected, SMAuthProviderAssigner { private static final Log log = Log.getLog(LdapAuthProvider.class); public LdapAuthProvider() { @@ -77,9 +79,8 @@ public Map authExternalUser( } if (userData == null) { - String fullUserDN = buildFullUserDN(userName, ldapSettings); - validateUserAccess(fullUserDN, ldapSettings); - userData = authenticateLdap(fullUserDN, password, ldapSettings, null, environment); + validateUserAccess(userName, ldapSettings); + userData = authenticateLdap(userName, password, ldapSettings, null, environment); } return userData; } @@ -331,6 +332,7 @@ private Map authenticateLdap( userContext = new InitialDirContext(environment); Map userData = new HashMap<>(); userData.put(LdapConstants.CRED_USERNAME, findUserNameFromDN(userDN, ldapSettings)); + userData.put(LdapConstants.CRED_FULL_DN, userDN); userData.put(LdapConstants.CRED_SESSION_ID, UUID.randomUUID()); if (login != null) { userData.put(LdapConstants.CRED_DISPLAY_NAME, login); @@ -349,4 +351,72 @@ private Map authenticateLdap( } } + @NotNull + @Override + public SMAutoAssign detectAutoAssignments( + @NotNull DBRProgressMonitor monitor, + @NotNull SMAuthProviderCustomConfiguration providerConfig, + @NotNull Map authParameters + ) throws DBException { + String userName = JSONUtils.getString(authParameters, LdapConstants.CRED_USERNAME); + if (CommonUtils.isEmpty(userName)) { + throw new DBException("LDAP user name is empty"); + } + + LdapSettings ldapSettings = new LdapSettings(providerConfig); + String fullDN = JSONUtils.getString(authParameters, LdapConstants.CRED_FULL_DN); + String userDN; + if (!CommonUtils.isEmpty(fullDN)) { + userDN = fullDN; + } else { + userDN = getUserDN(ldapSettings, JSONUtils.getString(authParameters, LdapConstants.CRED_DISPLAY_NAME)); + } + if (userDN == null) { + return new SMAutoAssign(); + } + + SMAutoAssign smAutoAssign = new SMAutoAssign(); + smAutoAssign.addExternalTeamId(userDN); + + String groupDN = getGroupForMember(userDN, ldapSettings); + if (groupDN != null) { + smAutoAssign.addExternalTeamId(groupDN); + } + + return smAutoAssign; + } + + private String getUserDN(LdapSettings ldapSettings, String displayName) { + DirContext context; + try { + context = new InitialDirContext(creteAuthEnvironment(ldapSettings)); + return findUserDN(context, ldapSettings, displayName); + } catch (Exception e) { + log.error("User not found", e); + return null; + } + } + + private String getGroupForMember(String fullDN, LdapSettings ldapSettings) { + DirContext context; + try { + context = new InitialDirContext(creteAuthEnvironment(ldapSettings)); + String searchFilter = "(member=" + fullDN + ")"; + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + + NamingEnumeration results = context.search(ldapSettings.getBaseDN(), searchFilter, searchControls); + if (results.hasMore()) { + return results.next().getName(); + } + } catch (Exception e) { + log.error("Group not found", e); + } + return null; + } + + @Override + public String getExternalTeamIdMetadataFieldName() { + return LdapConstants.LDAP_META_GROUP_NAME; + } } diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java index 8b18c1fb6e..b6148bbb1a 100644 --- a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java @@ -1,6 +1,6 @@ /* * DBeaver - Universal Database Manager - * Copyright (C) 2010-2024 DBeaver Corp and others + * Copyright (C) 2010-2025 DBeaver Corp and others * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,4 +32,6 @@ public interface LdapConstants { String CRED_USER_DN = "user-dn"; String CRED_PASSWORD = "password"; String CRED_SESSION_ID = "session-id"; + String CRED_FULL_DN = "full-dn"; + String LDAP_META_GROUP_NAME = "ldap.group-name"; } diff --git a/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF index d4a281e6e7..7795b02a85 100644 --- a/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Metadata Bundle-SymbolicName: io.cloudbeaver.service.metadata;singleton:=true -Bundle-Version: 1.0.117.qualifier -Bundle-Release-Date: 20250120 +Bundle-Version: 1.0.118.qualifier +Bundle-Release-Date: 20250203 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.metadata/pom.xml b/server/bundles/io.cloudbeaver.service.metadata/pom.xml index 42ed2f11e3..5d1671a452 100644 --- a/server/bundles/io.cloudbeaver.service.metadata/pom.xml +++ b/server/bundles/io.cloudbeaver.service.metadata/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.metadata - 1.0.117-SNAPSHOT + 1.0.118-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF index 80c0773806..4f1f911fa8 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.rm.nio/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Resource manager NIO implementation Bundle-SymbolicName: io.cloudbeaver.service.rm.nio;singleton:=true -Bundle-Version: 1.0.31.qualifier -Bundle-Release-Date: 20250120 +Bundle-Version: 1.0.32.qualifier +Bundle-Release-Date: 20250203 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml b/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml index f5caf1b271..debce9ffc0 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml +++ b/server/bundles/io.cloudbeaver.service.rm.nio/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.rm.nio - 1.0.31-SNAPSHOT + 1.0.32-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF index 48b72daa43..4b3ef16fd5 100644 --- a/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.rm/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: Cloudbeaver Web Service - Resource manager Bundle-SymbolicName: io.cloudbeaver.service.rm;singleton:=true -Bundle-Version: 1.0.66.qualifier -Bundle-Release-Date: 20250120 +Bundle-Version: 1.0.67.qualifier +Bundle-Release-Date: 20250203 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.rm/pom.xml b/server/bundles/io.cloudbeaver.service.rm/pom.xml index 5c87c7c002..74bdde2b87 100644 --- a/server/bundles/io.cloudbeaver.service.rm/pom.xml +++ b/server/bundles/io.cloudbeaver.service.rm/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.rm - 1.0.66-SNAPSHOT + 1.0.67-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF index 4f4760b29f..b93f9ab968 100644 --- a/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.service.security/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Name: Cloudbeaver Web Service - Security Bundle-Vendor: DBeaver Corp Bundle-SymbolicName: io.cloudbeaver.service.security;singleton:=true -Bundle-Version: 1.0.69.qualifier -Bundle-Release-Date: 20250120 +Bundle-Version: 1.0.70.qualifier +Bundle-Release-Date: 20250203 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.service.security/pom.xml b/server/bundles/io.cloudbeaver.service.security/pom.xml index 5dca0e5c2a..7064ed6eba 100644 --- a/server/bundles/io.cloudbeaver.service.security/pom.xml +++ b/server/bundles/io.cloudbeaver.service.security/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.service.security - 1.0.69-SNAPSHOT + 1.0.70-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF index 488aa9728e..18fc6d5659 100644 --- a/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.slf4j/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: DBeaver Corp Bundle-Name: CloudBeaver SLF4j Binding Bundle-SymbolicName: io.cloudbeaver.slf4j;singleton:=true -Bundle-Version: 1.0.29.qualifier -Bundle-Release-Date: 20250120 +Bundle-Version: 1.0.30.qualifier +Bundle-Release-Date: 20250203 Bundle-RequiredExecutionEnvironment: JavaSE-17 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.slf4j/pom.xml b/server/bundles/io.cloudbeaver.slf4j/pom.xml index 8ee54bce31..f669ff657d 100644 --- a/server/bundles/io.cloudbeaver.slf4j/pom.xml +++ b/server/bundles/io.cloudbeaver.slf4j/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.slf4j - 1.0.29-SNAPSHOT + 1.0.30-SNAPSHOT eclipse-plugin diff --git a/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml b/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml index b1b1774f47..e37daa5e56 100644 --- a/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml +++ b/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml b/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml index a41df532b9..61c1cda7cf 100644 --- a/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml +++ b/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml @@ -9,6 +9,6 @@ ../ io.cloudbeaver.ce.drivers.feature - 1.0.137-SNAPSHOT + 1.0.138-SNAPSHOT eclipse-feature diff --git a/server/features/io.cloudbeaver.product.ce.feature/feature.xml b/server/features/io.cloudbeaver.product.ce.feature/feature.xml index 2f8c9161a3..aad57ac61b 100644 --- a/server/features/io.cloudbeaver.product.ce.feature/feature.xml +++ b/server/features/io.cloudbeaver.product.ce.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/server/features/io.cloudbeaver.product.ce.feature/pom.xml b/server/features/io.cloudbeaver.product.ce.feature/pom.xml index aa0879a77f..c3ec06c6e7 100644 --- a/server/features/io.cloudbeaver.product.ce.feature/pom.xml +++ b/server/features/io.cloudbeaver.product.ce.feature/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.product.ce.feature - 24.3.3-SNAPSHOT + 24.3.4-SNAPSHOT eclipse-feature diff --git a/server/features/io.cloudbeaver.server.feature/feature.xml b/server/features/io.cloudbeaver.server.feature/feature.xml index 0a65dbfa31..c6c014d000 100644 --- a/server/features/io.cloudbeaver.server.feature/feature.xml +++ b/server/features/io.cloudbeaver.server.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/server/features/io.cloudbeaver.server.feature/pom.xml b/server/features/io.cloudbeaver.server.feature/pom.xml index 11963d5cd6..2087c84041 100644 --- a/server/features/io.cloudbeaver.server.feature/pom.xml +++ b/server/features/io.cloudbeaver.server.feature/pom.xml @@ -10,6 +10,6 @@ ../ io.cloudbeaver.server.feature - 24.3.3-SNAPSHOT + 24.3.4-SNAPSHOT eclipse-feature diff --git a/server/features/io.cloudbeaver.ws.feature/feature.xml b/server/features/io.cloudbeaver.ws.feature/feature.xml index 15e889f55c..24581f307e 100644 --- a/server/features/io.cloudbeaver.ws.feature/feature.xml +++ b/server/features/io.cloudbeaver.ws.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/server/features/io.cloudbeaver.ws.feature/pom.xml b/server/features/io.cloudbeaver.ws.feature/pom.xml index 559c30d502..2af00c2a8e 100644 --- a/server/features/io.cloudbeaver.ws.feature/pom.xml +++ b/server/features/io.cloudbeaver.ws.feature/pom.xml @@ -10,6 +10,6 @@ ../ io.cloudbeaver.ws.feature - 1.0.67-SNAPSHOT + 1.0.68-SNAPSHOT eclipse-feature diff --git a/server/pom.xml b/server/pom.xml index 415ea0736b..fc298e9da5 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -19,7 +19,7 @@ CloudBeaver CE - 24.3.3 + 24.3.4 diff --git a/server/product/web-server/CloudbeaverServer.product b/server/product/web-server/CloudbeaverServer.product index 86487d9256..22313bf096 100644 --- a/server/product/web-server/CloudbeaverServer.product +++ b/server/product/web-server/CloudbeaverServer.product @@ -2,7 +2,7 @@ diff --git a/server/product/web-server/pom.xml b/server/product/web-server/pom.xml index f61c68f920..0eb23ece32 100644 --- a/server/product/web-server/pom.xml +++ b/server/product/web-server/pom.xml @@ -9,7 +9,7 @@ 1.0.0-SNAPSHOT ../../ - 24.3.3-SNAPSHOT + 24.3.4-SNAPSHOT web-server eclipse-repository Cloudbeaver Server Product diff --git a/webapp/packages/core-blocks/src/FormControls/InputFiles.tsx b/webapp/packages/core-blocks/src/FormControls/InputFiles.tsx index 6f2acefc24..cc4a7b8b4d 100644 --- a/webapp/packages/core-blocks/src/FormControls/InputFiles.tsx +++ b/webapp/packages/core-blocks/src/FormControls/InputFiles.tsx @@ -32,6 +32,7 @@ type BaseProps = Omit, 'onChange' | error?: boolean; loading?: boolean; description?: string; + buttonText?: string; labelTooltip?: string; hideTags?: boolean; ref?: React.Ref; @@ -72,6 +73,7 @@ export const InputFiles: InputFilesType = observer( error, loading, description, + buttonText, labelTooltip, hideTags, autoHide, @@ -167,6 +169,12 @@ export const InputFiles: InputFilesType = observer( const files = Array.from(value ?? []); + let text = buttonText; + + if (!text) { + text = translate(rest.multiple ? 'ui_upload_files' : 'ui_upload_file'); + } + return ( @@ -175,7 +183,7 @@ export const InputFiles: InputFilesType = observer(
{!hideTags && ( diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeDescription.module.css b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeDescription.module.css new file mode 100644 index 0000000000..1d4fb726e4 --- /dev/null +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeDescription.module.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.description { + composes: theme-typography--caption from global; + color: var(--theme-text-hint-on-light); +} diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeDescription.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeDescription.tsx new file mode 100644 index 0000000000..b33969895f --- /dev/null +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeDescription.tsx @@ -0,0 +1,26 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { s } from '../../s.js'; +import { useS } from '../../useS.js'; +import classes from './TreeNodeDescription.module.css'; + +interface Props extends React.HTMLAttributes { + className?: string; +} + +export const TreeNodeDescription: React.FC = observer(function TreeNodeDescription({ className, children, ...rest }) { + const styles = useS(classes); + + return ( + + {children} + + ); +}); diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 002ef6cadb..ef56612b57 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -131,6 +131,7 @@ export * from './Tree/TreeNode/TreeNodeControl.js'; export * from './Tree/TreeNode/TreeNodeExpand.js'; export * from './Tree/TreeNode/TreeNodeIcon.js'; export * from './Tree/TreeNode/TreeNodeName.js'; +export * from './Tree/TreeNode/TreeNodeDescription.js'; export * from './Tree/TreeNode/TreeNodeNested.js'; export * from './Tree/TreeNode/TreeNodeNestedMessage.js'; export * from './Tree/TreeNode/TreeNodeSelect.js'; diff --git a/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContext.ts b/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContext.ts index 2416dd02ee..47dcd65b09 100644 --- a/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContext.ts +++ b/webapp/packages/core-connections/src/ConnectionExecutionContext/ConnectionExecutionContext.ts @@ -162,6 +162,20 @@ export class ConnectionExecutionContext implements IConnectionExecutionContext { return mapAsyncTaskInfo(result); } + async getLog() { + const result = await this.withContext(async context => { + const { log } = await this.graphQLService.sdk.getTransactionLog({ + projectId: context.projectId, + connectionId: context.connectionId, + contextId: context.id, + }); + + return log; + }); + + return result?.transactionLogInfos; + } + private withContext(callback: (context: IConnectionExecutionContextInfo) => Promise): Promise { if (!this.context) { throw new Error('Execution Context not found'); diff --git a/webapp/packages/core-sdk/src/queries/fragments/DatabaseDriver.gql b/webapp/packages/core-sdk/src/queries/fragments/DatabaseDriver.gql index 7a84c0a15a..aad8588889 100644 --- a/webapp/packages/core-sdk/src/queries/fragments/DatabaseDriver.gql +++ b/webapp/packages/core-sdk/src/queries/fragments/DatabaseDriver.gql @@ -22,6 +22,9 @@ fragment DatabaseDriver on DriverInfo { applicableAuthModels applicableNetworkHandlers configurationTypes + driverId + driverInstalled + downloadable mainProperties @include(if: $includeMainProperties) { ...DriverPropertyInfo diff --git a/webapp/packages/core-sdk/src/queries/fragments/NavNodeInfo.gql b/webapp/packages/core-sdk/src/queries/fragments/NavNodeInfo.gql index 0c87b6b2bc..1d1ccfc04a 100644 --- a/webapp/packages/core-sdk/src/queries/fragments/NavNodeInfo.gql +++ b/webapp/packages/core-sdk/src/queries/fragments/NavNodeInfo.gql @@ -2,6 +2,7 @@ fragment NavNodeInfo on NavigatorNodeInfo { id name plainName + description hasChildren nodeType icon diff --git a/webapp/packages/core-sdk/src/queries/transactions/getTransactionCount.gql b/webapp/packages/core-sdk/src/queries/transactions/getTransactionCount.gql new file mode 100644 index 0000000000..0289c29a88 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/transactions/getTransactionCount.gql @@ -0,0 +1,5 @@ +mutation getTransactionCount($projectId: ID!, $connectionId: ID!, $contextId: ID!) { + info: getTransactionLogInfo(projectId: $projectId, connectionId: $connectionId, contextId: $contextId) { + count + } +} diff --git a/webapp/packages/core-sdk/src/queries/transactions/getTransactionLog.gql b/webapp/packages/core-sdk/src/queries/transactions/getTransactionLog.gql new file mode 100644 index 0000000000..116e622bd1 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/transactions/getTransactionLog.gql @@ -0,0 +1,13 @@ +mutation getTransactionLog($projectId: ID!, $connectionId: ID!, $contextId: ID!) { + log: getTransactionLogInfo(projectId: $projectId, connectionId: $connectionId, contextId: $contextId) { + transactionLogInfos { + id + time + type + queryString + durationMs + rows + result + } + } +} diff --git a/webapp/packages/plugin-connection-custom/src/DriverSelector/Driver.module.css b/webapp/packages/plugin-connection-custom/src/DriverSelector/Driver.module.css index 1563215dc9..2467b6cf2c 100644 --- a/webapp/packages/plugin-connection-custom/src/DriverSelector/Driver.module.css +++ b/webapp/packages/plugin-connection-custom/src/DriverSelector/Driver.module.css @@ -5,8 +5,24 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +.icon { + position: relative; +} + .staticImage { box-sizing: border-box; width: 24px; max-height: 24px; -}; +} + +.indicator { + position: absolute; + bottom: 2px; + right: 0; + width: 14px; + height: 14px; + display: flex; + border-radius: 50%; + background-color: var(--theme-surface); + border: 1px solid var(--theme-surface); +} diff --git a/webapp/packages/plugin-connection-custom/src/DriverSelector/Driver.tsx b/webapp/packages/plugin-connection-custom/src/DriverSelector/Driver.tsx index fdb52fe310..86ab72b4e4 100644 --- a/webapp/packages/plugin-connection-custom/src/DriverSelector/Driver.tsx +++ b/webapp/packages/plugin-connection-custom/src/DriverSelector/Driver.tsx @@ -8,7 +8,7 @@ import { observer } from 'mobx-react-lite'; import { useCallback } from 'react'; -import { ListItem, ListItemDescription, ListItemIcon, ListItemName, s, StaticImage, useS } from '@cloudbeaver/core-blocks'; +import { IconOrImage, ListItem, ListItemDescription, ListItemIcon, ListItemName, s, StaticImage, useS, useTranslate } from '@cloudbeaver/core-blocks'; import style from './Driver.module.css'; @@ -17,6 +17,7 @@ export interface IDriver { icon?: string; name?: string; description?: string; + driverInstalled?: boolean; } interface Props { @@ -25,13 +26,19 @@ interface Props { } export const Driver = observer(function Driver({ driver, onSelect }) { + const translate = useTranslate(); const select = useCallback(() => onSelect(driver.id), [driver]); const styles = useS(style); return ( - + + {!driver.driverInstalled && ( +
+ +
+ )}
{driver.name} {driver.description} diff --git a/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.tsx b/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.tsx index 90cffabfd2..f3c2db1d46 100644 --- a/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.tsx +++ b/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.tsx @@ -35,7 +35,7 @@ export const DriverSelectorDialog: DialogComponent = observer(function - + ); diff --git a/webapp/packages/plugin-connection-custom/src/DriverSelector/useDriverSelectorDialog.ts b/webapp/packages/plugin-connection-custom/src/DriverSelector/useDriverSelectorDialog.ts index 518cb459f0..1bd9451ad3 100644 --- a/webapp/packages/plugin-connection-custom/src/DriverSelector/useDriverSelectorDialog.ts +++ b/webapp/packages/plugin-connection-custom/src/DriverSelector/useDriverSelectorDialog.ts @@ -5,20 +5,21 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, observable } from 'mobx'; +import { action, computed } from 'mobx'; -import { useObservableRef, useResource } from '@cloudbeaver/core-blocks'; +import { useObservableRef, usePermission, useResource } from '@cloudbeaver/core-blocks'; import { ConnectionsManagerService, DBDriverResource } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; +import { EAdminPermission } from '@cloudbeaver/core-root'; import { PublicConnectionFormService } from '@cloudbeaver/plugin-connections'; import type { IDriver } from './Driver.js'; interface State { + readonly drivers: IDriver[]; select(driverId: string): Promise; - enabledDrivers: IDriver[]; } interface DriverSelectorDialogArgs { @@ -28,17 +29,26 @@ interface DriverSelectorDialogArgs { } export function useDriverSelectorDialog({ onSelect, projectId, folderPath }: DriverSelectorDialogArgs) { + const isAdmin = usePermission(EAdminPermission.admin); const notificationService = useService(NotificationService); const connectionsManagerService = useService(ConnectionsManagerService); const publicConnectionFormService = useService(PublicConnectionFormService); const dbDriverResource = useResource(useDriverSelectorDialog, DBDriverResource, CachedMapAllKey); - const enabledDrivers = dbDriverResource.resource.enabledDrivers; const state: State = useObservableRef( () => ({ + get drivers() { + return dbDriverResource.resource.enabledDrivers.filter(driver => { + if (this.isAdmin) { + return true; + } + + return driver.driverInstalled; + }); + }, async select(driverId: string) { const projects = this.connectionsManagerService.createConnectionProjects; - const drivers = this.enabledDrivers.map(driver => driver.id); + const drivers = this.drivers.map(driver => driver.id); if (projects.length === 0) { this.notificationService.logError({ title: 'core_projects_no_default_project' }); @@ -53,8 +63,8 @@ export function useDriverSelectorDialog({ onSelect, projectId, folderPath }: Dri } }, }), - { select: action.bound, enabledDrivers: observable.ref }, - { notificationService, connectionsManagerService, publicConnectionFormService, enabledDrivers }, + { select: action.bound, drivers: computed }, + { notificationService, connectionsManagerService, publicConnectionFormService, dbDriverResource, isAdmin }, ); return state; diff --git a/webapp/packages/plugin-connection-custom/src/locales/en.ts b/webapp/packages/plugin-connection-custom/src/locales/en.ts index 3c03aca9cf..290e841494 100644 --- a/webapp/packages/plugin-connection-custom/src/locales/en.ts +++ b/webapp/packages/plugin-connection-custom/src/locales/en.ts @@ -5,4 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -export default [['plugin_connection_custom_action_custom_label', 'New Connection']]; +export default [ + ['plugin_connection_custom_action_custom_label', 'New Connection'], + ['plugin_connection_custom_drivers_driver_not_installed', 'Driver is not installed'], +]; diff --git a/webapp/packages/plugin-connection-custom/src/locales/fr.ts b/webapp/packages/plugin-connection-custom/src/locales/fr.ts index 4c0f72d04d..63aeb3523b 100644 --- a/webapp/packages/plugin-connection-custom/src/locales/fr.ts +++ b/webapp/packages/plugin-connection-custom/src/locales/fr.ts @@ -5,4 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -export default [['plugin_connection_custom_action_custom_label', 'Nouvelle connexion']]; +export default [ + ['plugin_connection_custom_action_custom_label', 'Nouvelle connexion'], + ['plugin_connection_custom_drivers_driver_not_installed', 'Driver is not installed'], +]; diff --git a/webapp/packages/plugin-connection-custom/src/locales/it.ts b/webapp/packages/plugin-connection-custom/src/locales/it.ts index 3c03aca9cf..290e841494 100644 --- a/webapp/packages/plugin-connection-custom/src/locales/it.ts +++ b/webapp/packages/plugin-connection-custom/src/locales/it.ts @@ -5,4 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -export default [['plugin_connection_custom_action_custom_label', 'New Connection']]; +export default [ + ['plugin_connection_custom_action_custom_label', 'New Connection'], + ['plugin_connection_custom_drivers_driver_not_installed', 'Driver is not installed'], +]; diff --git a/webapp/packages/plugin-connection-custom/src/locales/ru.ts b/webapp/packages/plugin-connection-custom/src/locales/ru.ts index 4d4f153ef8..76eb1d7456 100644 --- a/webapp/packages/plugin-connection-custom/src/locales/ru.ts +++ b/webapp/packages/plugin-connection-custom/src/locales/ru.ts @@ -5,4 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -export default [['plugin_connection_custom_action_custom_label', 'Новое подключение']]; +export default [ + ['plugin_connection_custom_action_custom_label', 'Новое подключение'], + ['plugin_connection_custom_drivers_driver_not_installed', 'Драйвер не установлен'], +]; diff --git a/webapp/packages/plugin-connection-custom/src/locales/zh.ts b/webapp/packages/plugin-connection-custom/src/locales/zh.ts index 0de6a0635e..b2c7c30da4 100644 --- a/webapp/packages/plugin-connection-custom/src/locales/zh.ts +++ b/webapp/packages/plugin-connection-custom/src/locales/zh.ts @@ -5,4 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -export default [['plugin_connection_custom_action_custom_label', '新建连接']]; +export default [ + ['plugin_connection_custom_action_custom_label', '新建连接'], + ['plugin_connection_custom_drivers_driver_not_installed', 'Driver is not installed'], +]; diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx index d37e416c24..14d4ba582d 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/Options.tsx @@ -14,11 +14,13 @@ import { Combobox, Container, FieldCheckbox, + Flex, Form, FormFieldDescription, getComputed, Group, GroupTitle, + IconOrImage, InputField, Link, ObjectPropertyInfoForm, @@ -28,6 +30,7 @@ import { Textarea, useAdministrationSettings, useFormValidator, + usePermission, useResource, useS, useTranslate, @@ -35,7 +38,7 @@ import { import { DatabaseAuthModelsResource, type DBDriver, DBDriverResource, isLocalConnection } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { ProjectInfoResource } from '@cloudbeaver/core-projects'; -import { ServerConfigResource } from '@cloudbeaver/core-root'; +import { EAdminPermission, ServerConfigResource } from '@cloudbeaver/core-root'; import { DriverConfigurationType } from '@cloudbeaver/core-sdk'; import { type TabContainerPanelComponent, TabsContext, useAuthenticationAction } from '@cloudbeaver/core-ui'; import { EMPTY_ARRAY } from '@cloudbeaver/core-utils'; @@ -76,6 +79,7 @@ const driverConfiguration: IDriverConfiguration[] = [ ]; export const Options: TabContainerPanelComponent = observer(function Options({ state }) { + const isAdmin = usePermission(EAdminPermission.admin); const serverConfigResource = useResource(Options, ServerConfigResource, undefined); const projectInfoResource = useService(ProjectInfoResource); const service = useService(ConnectionFormService); @@ -151,7 +155,13 @@ export const Options: TabContainerPanelComponent = observe const edit = state.mode === 'edit'; const originLocal = !info || (originInfo?.origin && isLocalConnection(originInfo.origin)); - const drivers = driverMap.resource.enabledDrivers.filter(({ id }) => availableDrivers.includes(id)); + const drivers = driverMap.resource.enabledDrivers.filter(({ id, driverInstalled }) => { + if (!edit && !isAdmin && !driverInstalled) { + return false; + } + + return availableDrivers.includes(id); + }); let properties = authModel?.properties; @@ -187,7 +197,12 @@ export const Options: TabContainerPanelComponent = observe tiny fill > - {translate('connections_connection_driver')} + + {isAdmin && !driver?.driverInstalled && ( + + )} + {translate('connections_connection_driver')} + {configurationTypes.length > 1 && ( diff --git a/webapp/packages/plugin-connections/src/locales/en.ts b/webapp/packages/plugin-connections/src/locales/en.ts index 61d5f9f09f..77505452bb 100644 --- a/webapp/packages/plugin-connections/src/locales/en.ts +++ b/webapp/packages/plugin-connections/src/locales/en.ts @@ -45,4 +45,5 @@ export default [ 'There are multiple credentials available for authentication.\nPlease choose credentials you want to use.', ], ['plugin_connections_connection_create_menu_title', 'Connection'], + ['plugin_connections_connection_driver_not_installed', 'Driver is not installed. You can install it in the "Administration" part.'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/fr.ts b/webapp/packages/plugin-connections/src/locales/fr.ts index 5fb33cc349..3dc8842cb2 100644 --- a/webapp/packages/plugin-connections/src/locales/fr.ts +++ b/webapp/packages/plugin-connections/src/locales/fr.ts @@ -54,4 +54,5 @@ export default [ ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'Onglet Identifiants'], ['plugin_connections_connection_auth_secret_description', 'Veuillez sélectionner les identifiants fournis par une de vos équipes'], ['plugin_connections_connection_create_menu_title', 'Connection'], + ['plugin_connections_connection_driver_not_installed', 'Driver is not installed. You can install it in the "Administration" part.'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/it.ts b/webapp/packages/plugin-connections/src/locales/it.ts index 1789dfe180..52c6997b77 100644 --- a/webapp/packages/plugin-connections/src/locales/it.ts +++ b/webapp/packages/plugin-connections/src/locales/it.ts @@ -47,4 +47,5 @@ export default [ 'There are multiple credentials available for authentication.\nPlease choose credentials you want to use.', ], ['plugin_connections_connection_create_menu_title', 'Connection'], + ['plugin_connections_connection_driver_not_installed', 'Driver is not installed. You can install it in the "Administration" part.'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/ru.ts b/webapp/packages/plugin-connections/src/locales/ru.ts index e85d1f46cc..c1f4974c6f 100644 --- a/webapp/packages/plugin-connections/src/locales/ru.ts +++ b/webapp/packages/plugin-connections/src/locales/ru.ts @@ -45,4 +45,5 @@ export default [ 'У вас есть несколько учетных записей для авторизации.\nВыберите учетную запись из списка.', ], ['plugin_connections_connection_create_menu_title', 'Подключение'], + ['plugin_connections_connection_driver_not_installed', 'Драйвер не установлен. Вы можете установить его в "Администрированой" части.'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/zh.ts b/webapp/packages/plugin-connections/src/locales/zh.ts index c22e3872d0..da7d9284ac 100644 --- a/webapp/packages/plugin-connections/src/locales/zh.ts +++ b/webapp/packages/plugin-connections/src/locales/zh.ts @@ -41,4 +41,5 @@ export default [ ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', '凭证页签'], ['plugin_connections_connection_auth_secret_description', '有多个凭证可用于身份验证.\n请选择您要使用的凭证。'], ['plugin_connections_connection_create_menu_title', 'Connection'], + ['plugin_connections_connection_driver_not_installed', 'Driver is not installed. You can install it in the "Administration" part.'], ]; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx index eb5537ec68..f81238bc49 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/CellRenderer/CellRenderer.tsx @@ -103,7 +103,10 @@ export const CellRenderer = observer): boolean { - if (!cellContext.cell) { + if ( + !cellContext.cell || + (!dataGridContext.model.hasElementIdentifier(tableDataContext.view.resultIndex) && cellContext.editionState !== DatabaseEditChangeType.add) + ) { return false; } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts index a4d7a03f5b..a056c6c35d 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContextMenu/DataGridContextMenuCellEditingService.ts @@ -86,9 +86,11 @@ export class DataGridContextMenuCellEditingService { const isComplex = format.isBinary(key) || format.isGeometry(key); const isTruncated = content.isTextTruncated(key); const selectedElements = select?.getSelectedElements() || []; + // we can't edit table cells if table doesn't have row identifier, but we can edit new created rows before insert (CB-6063) + const canEdit = model.hasElementIdentifier(resultIndex) || editor.getElementState(key) === DatabaseEditChangeType.add; if (action === ACTION_EDIT) { - if (!column || cellValue === undefined || format.isReadOnly(key) || isComplex || isTruncated) { + if (!column || cellValue === undefined || format.isReadOnly(key) || isComplex || isTruncated || !canEdit) { return false; } @@ -96,7 +98,7 @@ export class DataGridContextMenuCellEditingService { } if (action === ACTION_DATA_GRID_EDITING_SET_TO_NULL) { - return cellValue !== undefined && !format.isReadOnly(key) && !view.getColumn(key.column)?.required && !format.isNull(key); + return cellValue !== undefined && !format.isReadOnly(key) && !view.getColumn(key.column)?.required && !format.isNull(key) && canEdit; } if (action === ACTION_DATA_GRID_EDITING_ADD_ROW || action === ACTION_DATA_GRID_EDITING_DUPLICATE_ROW) { @@ -104,11 +106,11 @@ export class DataGridContextMenuCellEditingService { } if (action === ACTION_DATA_GRID_EDITING_DELETE_ROW) { - return !format.isReadOnly(key) && editor.getElementState(key) !== DatabaseEditChangeType.delete; + return !format.isReadOnly(key) && canEdit && editor.getElementState(key) !== DatabaseEditChangeType.delete; } if (action === ACTION_DATA_GRID_EDITING_DELETE_SELECTED_ROW) { - if (model.isReadonly(resultIndex) || !editor.hasFeature('delete')) { + if (model.isReadonly(resultIndex) || !canEdit || !editor.hasFeature('delete')) { return false; } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx index da7b068bdb..919a256102 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridTable.tsx @@ -222,12 +222,14 @@ export const DataGridTable = observer(function DataGridT function handleKeyDown(event: React.KeyboardEvent) { gridSelectedCellCopy.onKeydownHandler(event); + const cell = selectionAction.getFocusedElement(); + // we can't edit table cells if table doesn't have row identifier, but we can edit new created rows before insert (CB-6063) + const canEdit = model.hasElementIdentifier(resultIndex) || !!(cell && tableData.editor.getElementState(cell) === DatabaseEditChangeType.add); - if (EventContext.has(event, EventStopPropagationFlag) || tableData.isReadOnly() || model.isReadonly(resultIndex)) { + if (EventContext.has(event, EventStopPropagationFlag) || !canEdit || tableData.isReadOnly() || model.isReadonly(resultIndex)) { return; } - const cell = selectionAction.getFocusedElement(); const activeElements = selectionAction.getActiveElements(); const activeRows = selectionAction.getActiveRows(); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx index d6ef14007c..0d016feb71 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableIndexColumnHeader.tsx @@ -27,7 +27,12 @@ export const TableIndexColumnHeader = observer>(funct throw new Error('Contexts required'); } - const readonly = getComputed(() => tableDataContext.isReadOnly() || dataGridContext.model.isReadonly(dataGridContext.resultIndex)); + const readonly = getComputed( + () => + tableDataContext.isReadOnly() || + dataGridContext.model.isReadonly(dataGridContext.resultIndex) || + !dataGridContext.model.hasElementIdentifier(dataGridContext.resultIndex), + ); function handleClick(event: React.MouseEvent) { selectionContext.selectTable(); diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts index eadfee8830..1b101f2cf8 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataModel.ts @@ -59,6 +59,10 @@ export class DatabaseDataModel = I return this.source.isReadonly(resultIndex); } + hasElementIdentifier(resultIndex: number): boolean { + return this.source.hasElementIdentifier(resultIndex); + } + isDataAvailable(offset: number, count: number): boolean { return this.source.isDataAvailable(offset, count); } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts index aed18b399f..5490880853 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/DatabaseDataSource.ts @@ -206,6 +206,10 @@ export abstract class DatabaseDataSource 1 || this.disabled; } diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts index 5557d1b31a..1ecb50bb1e 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataModel.ts @@ -31,6 +31,7 @@ export interface IDatabaseDataModel this; isReadonly: (resultIndex: number) => boolean; + hasElementIdentifier: (resultIndex: number) => boolean; isDisabled: (resultIndex?: number) => boolean; isLoading: () => boolean; isDataAvailable: (offset: number, count: number) => boolean; diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts index a8705f3605..cbd4fc739c 100644 --- a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/IDatabaseDataSource.ts @@ -71,6 +71,7 @@ export interface IDatabaseDataSource boolean; isLoadable: () => boolean; isReadonly: (resultIndex: number) => boolean; + hasElementIdentifier: (resultIndex: number) => boolean; isDataAvailable: (offset: number, count: number) => boolean; isLoading: () => boolean; isDisabled: (resultIndex?: number) => boolean; diff --git a/webapp/packages/plugin-data-viewer/src/ResultSet/ResultSetDataSource.ts b/webapp/packages/plugin-data-viewer/src/ResultSet/ResultSetDataSource.ts index 3aa856dbf2..4a70da9867 100644 --- a/webapp/packages/plugin-data-viewer/src/ResultSet/ResultSetDataSource.ts +++ b/webapp/packages/plugin-data-viewer/src/ResultSet/ResultSetDataSource.ts @@ -39,7 +39,7 @@ export abstract class ResultSetDataSource exten } override isReadonly(resultIndex: number): boolean { - return super.isReadonly(resultIndex) || !this.executionContext?.context || this.getResult(resultIndex)?.data?.hasRowIdentifier === false; + return super.isReadonly(resultIndex) || !this.executionContext?.context; } override async cancel(): Promise { diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts index d2820b630b..1a4fb798dd 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService.ts @@ -101,13 +101,16 @@ export class TableFooterMenuService { } case ACTION_DELETE: { const editor = model.source.getActionImplementation(resultIndex, DatabaseEditAction); + const selectedElements = getActiveElements(model, resultIndex); - if (!editor) { + // we can't edit table cells if table doesn't have row identifier, but we can edit new created rows before insert (CB-6063) + const canEdit = + model.hasElementIdentifier(resultIndex) || selectedElements.every(key => editor?.getElementState(key) === DatabaseEditChangeType.add); + + if (!editor || !canEdit) { return true; } - const selectedElements = getActiveElements(model, resultIndex); - return selectedElements.length === 0 || !selectedElements.some(key => editor.getElementState(key) !== DatabaseEditChangeType.delete); } case ACTION_REVERT: { diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx index eeba7b5a7b..17f70f82d6 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/BooleanValue/BooleanValuePresentation.tsx @@ -51,7 +51,11 @@ export const BooleanValuePresentation: TabContainerPanelComponent diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValueReadonly.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValueReadonly.ts index a927689d4a..b1ad216ad1 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValueReadonly.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/isTextValueReadonly.ts @@ -30,6 +30,7 @@ export function isTextValueReadonly({ contentAction, formatAction, model, result formatAction.isGeometry(cell) || contentAction.isTextTruncated(cell) || model.isReadonly(resultIndex) || - model.isDisabled(resultIndex) + model.isDisabled(resultIndex) || + !model.hasElementIdentifier(resultIndex) ); } diff --git a/webapp/packages/plugin-datasource-transaction-manager/package.json b/webapp/packages/plugin-datasource-transaction-manager/package.json index 50655af823..f63cb4e288 100644 --- a/webapp/packages/plugin-datasource-transaction-manager/package.json +++ b/webapp/packages/plugin-datasource-transaction-manager/package.json @@ -25,15 +25,26 @@ "@cloudbeaver/core-events": "^0", "@cloudbeaver/core-executor": "^0", "@cloudbeaver/core-localization": "^0", + "@cloudbeaver/core-resource": "^0", + "@cloudbeaver/core-root": "^0", + "@cloudbeaver/core-sdk": "^0", "@cloudbeaver/core-settings": "^0", "@cloudbeaver/core-ui": "^0", "@cloudbeaver/core-utils": "^0", "@cloudbeaver/core-view": "^0", + "@cloudbeaver/plugin-codemirror6": "^0", + "@cloudbeaver/plugin-data-grid": "^0", "@cloudbeaver/plugin-datasource-context-switch": "^0", - "@cloudbeaver/plugin-top-app-bar": "^0" + "@cloudbeaver/plugin-sql-editor-new": "^0", + "@cloudbeaver/plugin-top-app-bar": "^0", + "mobx": "^6", + "mobx-react-lite": "^4", + "react": "^18" }, "peerDependencies": {}, "devDependencies": { - "typescript": "^5" + "@types/react": "^18", + "typescript": "^5", + "typescript-plugin-css-modules": "^5" } } diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TRANSACTION_INFO_PARAM_SCHEMA.ts b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TRANSACTION_INFO_PARAM_SCHEMA.ts new file mode 100644 index 0000000000..b4e0b04890 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TRANSACTION_INFO_PARAM_SCHEMA.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { schema } from '@cloudbeaver/core-utils'; + +export const TRANSACTION_INFO_PARAM_SCHEMA = schema + .object({ + connectionId: schema.string(), + projectId: schema.string(), + contextId: schema.string(), + }) + .required() + .strict(); + +export type ITransactionInfoParam = schema.infer; + +export function createTransactionInfoParam(connectionId: string, projectId: string, contextId: string): ITransactionInfoParam { + return { + connectionId, + projectId, + contextId, + }; +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionInfoAction.module.css b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionInfoAction.module.css new file mode 100644 index 0000000000..3408475ccf --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionInfoAction.module.css @@ -0,0 +1,38 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.container { + cursor: pointer; + padding: 0 12px; + position: relative; + + &:after { + position: absolute; + background: #236ea0; + height: 32px; + width: 1px; + top: 8px; + right: -1px; + opacity: 1; + content: ''; + } + + &:hover { + background: #338ecc; + } +} + +.count { + border: 1px solid var(--theme-on-primary); + border-radius: var(--theme-form-element-radius); + height: 26px; + width: 50px; + display: flex; + align-items: center; + justify-content: center; +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionInfoAction.tsx b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionInfoAction.tsx new file mode 100644 index 0000000000..e225f12d0a --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionInfoAction.tsx @@ -0,0 +1,41 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { Container, s, useResource, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import type { ICustomMenuItemComponent } from '@cloudbeaver/core-view'; + +import { TransactionManagerService } from '../TransactionManagerService.js'; +import { createTransactionInfoParam } from './TRANSACTION_INFO_PARAM_SCHEMA.js'; +import classes from './TransactionInfoAction.module.css'; +import { TransactionLogCountResource } from './TransactionLogCountResource.js'; + +export const TransactionInfoAction: ICustomMenuItemComponent = observer(function TransactionInfoAction(props) { + const styles = useS(classes); + const translate = useTranslate(); + const transactionManagerService = useService(TransactionManagerService); + const transaction = transactionManagerService.getActiveContextTransaction(); + const context = transaction?.context; + const key = context ? createTransactionInfoParam(context.connectionId, context.projectId, context.id) : null; + + const transactionLogCountResource = useResource(TransactionInfoAction, TransactionLogCountResource, key); + + return ( + props.item.events?.onSelect?.()} + > + {transactionLogCountResource.data} + + ); +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogCountEventHandler.ts b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogCountEventHandler.ts new file mode 100644 index 0000000000..e2d81ffbb3 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogCountEventHandler.ts @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { type ISessionEvent, type SessionEventId, SessionEventSource, SessionEventTopic, TopicEventHandler } from '@cloudbeaver/core-root'; +import type { WsTransactionalCountEvent } from '@cloudbeaver/core-sdk'; + +export type IWsTransactionCountEvent = WsTransactionalCountEvent; + +type TransactionCountEvent = IWsTransactionCountEvent; + +@injectable() +export class TransactionLogCountEventHandler extends TopicEventHandler { + constructor(sessionEventSource: SessionEventSource) { + super(SessionEventTopic.CbTransaction, sessionEventSource); + } + + map(event: any): TransactionCountEvent { + return event; + } +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogCountResource.ts b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogCountResource.ts new file mode 100644 index 0000000000..d72f03a851 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogCountResource.ts @@ -0,0 +1,63 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { toJS } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import { CachedMapResource } from '@cloudbeaver/core-resource'; +import { ServerEventId } from '@cloudbeaver/core-root'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; +import { schemaValidationError } from '@cloudbeaver/core-utils'; + +import { createTransactionInfoParam, type ITransactionInfoParam, TRANSACTION_INFO_PARAM_SCHEMA } from './TRANSACTION_INFO_PARAM_SCHEMA.js'; +import { type IWsTransactionCountEvent, TransactionLogCountEventHandler } from './TransactionLogCountEventHandler.js'; + +@injectable() +export class TransactionLogCountResource extends CachedMapResource { + constructor( + private readonly graphQLService: GraphQLService, + transactionLogCountEventHandler: TransactionLogCountEventHandler, + ) { + super(); + + transactionLogCountEventHandler.onEvent( + ServerEventId.CbTransactionCount, + async data => { + const key = createTransactionInfoParam(data.connectionId, data.projectId, data.contextId); + this.set(key, data.transactionalCount); + }, + undefined, + this, + ); + } + + protected async loader(key: ITransactionInfoParam) { + const { info } = await this.graphQLService.sdk.getTransactionCount({ + connectionId: key.connectionId, + projectId: key.projectId, + contextId: key.contextId, + }); + + this.set(key, info.count); + + return this.data; + } + + override isKeyEqual(key: ITransactionInfoParam, secondKey: ITransactionInfoParam): boolean { + return key.connectionId === secondKey.connectionId && key.projectId === secondKey.projectId; + } + + protected override validateKey(key: ITransactionInfoParam): boolean { + const parse = TRANSACTION_INFO_PARAM_SCHEMA.safeParse(toJS(key)); + + if (!parse.success) { + this.logger.warn(`Invalid resource key ${(schemaValidationError(parse.error).toString(), { prefix: null })}`); + } + + return parse.success; + } +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogDialog.tsx b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogDialog.tsx new file mode 100644 index 0000000000..469772405e --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogDialog.tsx @@ -0,0 +1,82 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { + Button, + CommonDialogBody, + CommonDialogFooter, + CommonDialogHeader, + CommonDialogWrapper, + Container, + Flex, + useAutoLoad, + useResource, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { type ConnectionExecutionContext, ConnectionInfoResource, createConnectionParam } from '@cloudbeaver/core-connections'; +import type { DialogComponent } from '@cloudbeaver/core-dialogs'; + +import { TransactionLogTable } from './TransactionLogTable/TransactionLogTable.js'; +import { useTransactionLog } from './useTransactionLog.js'; + +interface IPayload { + transaction: ConnectionExecutionContext; + onCommit: () => void; + onRollback: () => void; +} + +export const TransactionLogDialog: DialogComponent = observer(function TransactionLogDialog(props) { + const translate = useTranslate(); + const context = props.payload.transaction.context; + const connectionParam = context ? createConnectionParam(context.projectId, context.connectionId) : null; + + const state = useTransactionLog(props.payload); + const connectionInfoResource = useResource(useTransactionLog, ConnectionInfoResource, connectionParam); + let title: string = translate('plugin_datasource_transaction_manager_logs'); + + if (connectionInfoResource.data?.name) { + title = `${title} (${connectionInfoResource.data.name})`; + } + + useAutoLoad(TransactionLogDialog, state); + + function handleRollback() { + props.payload.onRollback(); + props.resolveDialog(); + } + + function handleCommit() { + props.payload.onCommit(); + props.resolveDialog(); + } + + return ( + + + + + + + + + + + + + + + + ); +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/HeaderCell.tsx b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/HeaderCell.tsx new file mode 100644 index 0000000000..66579a0f70 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/HeaderCell.tsx @@ -0,0 +1,17 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { useTranslate } from '@cloudbeaver/core-blocks'; +import { type TransactionLogInfoItem } from '@cloudbeaver/core-sdk'; +import type { RenderHeaderCellProps } from '@cloudbeaver/plugin-data-grid'; + +export const HeaderCell = observer>(function HeaderCell(props) { + const translate = useTranslate(); + return
{typeof props.column.name === 'string' ? translate(props.column.name) : props.column.name}
; +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryCell.module.css b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryCell.module.css new file mode 100644 index 0000000000..52479f1294 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryCell.module.css @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.cell { + cursor: pointer; +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryCell.tsx b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryCell.tsx new file mode 100644 index 0000000000..1800556829 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryCell.tsx @@ -0,0 +1,33 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { useService } from '@cloudbeaver/core-di'; +import { CommonDialogService } from '@cloudbeaver/core-dialogs'; +import type { TransactionLogInfoItem } from '@cloudbeaver/core-sdk'; +import { type RenderCellProps } from '@cloudbeaver/plugin-data-grid'; + +import classes from './QueryCell.module.css'; +import { QueryDetailsDialog } from './QueryDetailsDialog.js'; + +export const QueryCell = observer>(function QueryCell(props) { + const commonDialogService = useService(CommonDialogService); + const value = props.row.queryString; + + async function openDetails() { + await commonDialogService.open(QueryDetailsDialog, { + text: value, + }); + } + + return ( +
+ {value} +
+ ); +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryDetailsDialog.tsx b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryDetailsDialog.tsx new file mode 100644 index 0000000000..19848d8bc7 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/QueryDetailsDialog.tsx @@ -0,0 +1,40 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { Button, CommonDialogBody, CommonDialogFooter, CommonDialogHeader, CommonDialogWrapper, Group, useTranslate } from '@cloudbeaver/core-blocks'; +import type { DialogComponent } from '@cloudbeaver/core-dialogs'; +import { useCodemirrorExtensions } from '@cloudbeaver/plugin-codemirror6'; +import { SQLCodeEditorLoader, useSqlDialectExtension } from '@cloudbeaver/plugin-sql-editor-new'; + +interface IPayload { + text: string; +} + +export const QueryDetailsDialog: DialogComponent = observer(function QueryDetailsDialog(props) { + const translate = useTranslate(); + const sqlDialect = useSqlDialectExtension(undefined); + const extensions = useCodemirrorExtensions(); + extensions.set(...sqlDialect); + + return ( + + + + + + + + + + + + ); +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TimeCell.tsx b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TimeCell.tsx new file mode 100644 index 0000000000..201a7f3375 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TimeCell.tsx @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import type { TransactionLogInfoItem } from '@cloudbeaver/core-sdk'; +import { isSameDay } from '@cloudbeaver/core-utils'; +import { type RenderCellProps } from '@cloudbeaver/plugin-data-grid'; + +export const TimeCell = observer>(function TimeCell(props) { + const date = new Date(props.row.time); + const fullTime = date.toLocaleString(); + const displayTime = isSameDay(date, new Date()) ? date.toLocaleTimeString() : fullTime; + + return
{displayTime}
; +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TransactionLogTable.module.css b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TransactionLogTable.module.css new file mode 100644 index 0000000000..ee44e57732 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TransactionLogTable.module.css @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +.container { + composes: theme-typography--caption theme-border-color-background from global; + border: 1px solid; + height: 100%; +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TransactionLogTable.tsx b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TransactionLogTable.tsx new file mode 100644 index 0000000000..6d86bb911d --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/TransactionLogTable/TransactionLogTable.tsx @@ -0,0 +1,76 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { s, useS } from '@cloudbeaver/core-blocks'; +import type { TransactionLogInfoItem } from '@cloudbeaver/core-sdk'; +import { type Column, DataGrid } from '@cloudbeaver/plugin-data-grid'; + +import { HeaderCell } from './HeaderCell.js'; +import { QueryCell } from './QueryCell.js'; +import { TimeCell } from './TimeCell.js'; +import classes from './TransactionLogTable.module.css'; + +interface Props { + log: TransactionLogInfoItem[]; +} + +const COLUMNS: Column[] = [ + { + key: 'time', + name: 'plugin_datasource_transaction_manager_logs_table_column_time', + resizable: true, + renderCell: props => , + renderHeaderCell: props => , + }, + { + key: 'type', + name: 'plugin_datasource_transaction_manager_logs_table_column_type', + resizable: true, + renderCell: props =>
{props.row.type}
, + renderHeaderCell: props => , + }, + { + key: 'text', + name: 'plugin_datasource_transaction_manager_logs_table_column_text', + resizable: true, + renderCell: props => , + renderHeaderCell: props => , + }, + { + key: 'duration', + name: 'plugin_datasource_transaction_manager_logs_table_column_duration', + resizable: true, + renderCell: props =>
{props.row.durationMs}
, + renderHeaderCell: props => , + }, + { + key: 'rows', + name: 'plugin_datasource_transaction_manager_logs_table_column_rows', + resizable: true, + renderCell: props =>
{props.row.rows}
, + renderHeaderCell: props => , + }, + { + key: 'result', + name: 'plugin_datasource_transaction_manager_logs_table_column_result', + resizable: true, + renderCell: props =>
{props.row.result}
, + renderHeaderCell: props => , + }, +]; + +export const TransactionLogTable = observer(function TransactionLogTable(props) { + const styles = useS(classes); + + return ( +
+ row.id} columns={COLUMNS} rowHeight={30} /> +
+ ); +}); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/useTransactionLog.tsx b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/useTransactionLog.tsx new file mode 100644 index 0000000000..1d75c1c0c8 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionLog/useTransactionLog.tsx @@ -0,0 +1,60 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observable } from 'mobx'; + +import { useObservableRef } from '@cloudbeaver/core-blocks'; +import type { ConnectionExecutionContext } from '@cloudbeaver/core-connections'; +import type { TransactionLogInfoItem } from '@cloudbeaver/core-sdk'; +import type { ILoadableState } from '@cloudbeaver/core-utils'; + +interface Payload { + transaction: ConnectionExecutionContext; +} + +interface State extends ILoadableState { + log: TransactionLogInfoItem[] | null; + exception: Error | null; + promise: Promise | null; + payload: Payload; +} + +export function useTransactionLog(payload: Payload) { + const state = useObservableRef( + () => ({ + log: null, + exception: null, + promise: null, + isLoaded() { + return this.log !== null; + }, + isError() { + return this.exception !== null; + }, + isLoading() { + return this.promise !== null; + }, + async load() { + try { + this.exception = null; + + this.promise = payload.transaction.getLog(); + const log = await this.promise; + this.log = log; + } catch (exception: any) { + this.exception = exception; + } finally { + this.promise = null; + } + }, + }), + { log: observable.ref, promise: observable.ref, exception: observable.ref, payload: observable.ref }, + { payload }, + ); + + return state; +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerBootstrap.ts b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerBootstrap.ts index b3a8c343de..fd485cdf9d 100644 --- a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerBootstrap.ts +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerBootstrap.ts @@ -5,7 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { ConfirmationDialog } from '@cloudbeaver/core-blocks'; +import { ConfirmationDialog, importLazyComponent } from '@cloudbeaver/core-blocks'; import { ConnectionExecutionContext, ConnectionExecutionContextResource, @@ -24,15 +24,25 @@ import { ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeave import { LocalizationService } from '@cloudbeaver/core-localization'; import { OptionsPanelService } from '@cloudbeaver/core-ui'; import { isNotNullDefined } from '@cloudbeaver/core-utils'; -import { ActionService, MenuService } from '@cloudbeaver/core-view'; +import { ActionService, MenuCustomItem, MenuService } from '@cloudbeaver/core-view'; import { ConnectionSchemaManagerService } from '@cloudbeaver/plugin-datasource-context-switch'; import { MENU_APP_ACTIONS } from '@cloudbeaver/plugin-top-app-bar'; import { ACTION_DATASOURCE_TRANSACTION_COMMIT } from './actions/ACTION_DATASOURCE_TRANSACTION_COMMIT.js'; import { ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE } from './actions/ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE.js'; import { ACTION_DATASOURCE_TRANSACTION_ROLLBACK } from './actions/ACTION_DATASOURCE_TRANSACTION_ROLLBACK.js'; +import { createTransactionInfoParam } from './TransactionLog/TRANSACTION_INFO_PARAM_SCHEMA.js'; +import { TransactionLogCountResource } from './TransactionLog/TransactionLogCountResource.js'; import { TransactionManagerSettingsService } from './TransactionManagerSettingsService.js'; +const TransactionInfoAction = importLazyComponent(() => + import('./TransactionLog/TransactionInfoAction.js').then(module => module.TransactionInfoAction), +); + +const TransactionLogDialog = importLazyComponent(() => + import('./TransactionLog/TransactionLogDialog.js').then(module => module.TransactionLogDialog), +); + @injectable() export class TransactionManagerBootstrap extends Bootstrap { constructor( @@ -48,6 +58,7 @@ export class TransactionManagerBootstrap extends Bootstrap { private readonly commonDialogService: CommonDialogService, private readonly localizationService: LocalizationService, private readonly transactionManagerSettingsService: TransactionManagerSettingsService, + private readonly transactionLogCountResource: TransactionLogCountResource, ) { super(); } @@ -68,12 +79,38 @@ export class TransactionManagerBootstrap extends Bootstrap { isNotNullDefined(transaction.autoCommit) ); }, - getItems: (_, items) => [ - ...items, - ACTION_DATASOURCE_TRANSACTION_COMMIT, - ACTION_DATASOURCE_TRANSACTION_ROLLBACK, - ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE, - ], + getItems: (_, items) => { + const transaction = this.getContextTransaction(); + + const result = [ + ...items, + ACTION_DATASOURCE_TRANSACTION_COMMIT, + ACTION_DATASOURCE_TRANSACTION_ROLLBACK, + ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE, + ]; + + if (transaction && transaction.autoCommit === false) { + result.push( + new MenuCustomItem( + { + id: 'transaction-info', + getComponent: () => TransactionInfoAction, + }, + { + onSelect: async () => { + await this.commonDialogService.open(TransactionLogDialog, { + transaction, + onCommit: () => this.commit(transaction), + onRollback: () => this.rollback(transaction), + }); + }, + }, + ), + ); + } + + return result; + }, }); this.actionService.addHandler({ @@ -127,19 +164,19 @@ export class TransactionManagerBootstrap extends Bootstrap { break; } case ACTION_DATASOURCE_TRANSACTION_ROLLBACK: { - try { - const result = await transaction.rollback(); - this.showTransactionResult(transaction, result); - } catch (exception: any) { - this.notificationService.logException(exception, 'plugin_datasource_transaction_manager_rollback_fail'); - } - + await this.rollback(transaction); break; } case ACTION_DATASOURCE_TRANSACTION_COMMIT_MODE_TOGGLE: try { await transaction.setAutoCommit(!transaction.autoCommit); await this.connectionExecutionContextResource.refresh(); + + const context = transaction.context; + + if (transaction.autoCommit === true && context) { + this.transactionLogCountResource.markOutdated(createTransactionInfoParam(context.connectionId, context.projectId, context.id)); + } } catch (exception: any) { this.notificationService.logException(exception, 'plugin_datasource_transaction_manager_commit_mode_fail'); } @@ -200,6 +237,15 @@ export class TransactionManagerBootstrap extends Bootstrap { } } + private async rollback(transaction: ConnectionExecutionContext) { + try { + const result = await transaction.rollback(); + this.showTransactionResult(transaction, result); + } catch (exception: any) { + this.notificationService.logException(exception, 'plugin_datasource_transaction_manager_rollback_fail'); + } + } + private async commit(transaction: ConnectionExecutionContext, onError?: (exception: any) => void) { try { const result = await transaction.commit(); diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerService.ts b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerService.ts new file mode 100644 index 0000000000..e3b9ae4699 --- /dev/null +++ b/webapp/packages/plugin-datasource-transaction-manager/src/TransactionManagerService.ts @@ -0,0 +1,28 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { ConnectionExecutionContextService } from '@cloudbeaver/core-connections'; +import { injectable } from '@cloudbeaver/core-di'; +import { ConnectionSchemaManagerService } from '@cloudbeaver/plugin-datasource-context-switch'; + +@injectable() +export class TransactionManagerService { + constructor( + private readonly connectionSchemaManagerService: ConnectionSchemaManagerService, + private readonly connectionExecutionContextService: ConnectionExecutionContextService, + ) {} + + getActiveContextTransaction() { + const context = this.connectionSchemaManagerService.activeExecutionContext; + + if (!context) { + return; + } + + return this.connectionExecutionContextService.get(context.id); + } +} diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/locales/en.ts b/webapp/packages/plugin-datasource-transaction-manager/src/locales/en.ts index f113567706..b9ec6e443f 100644 --- a/webapp/packages/plugin-datasource-transaction-manager/src/locales/en.ts +++ b/webapp/packages/plugin-datasource-transaction-manager/src/locales/en.ts @@ -1,3 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ export default [ ['plugin_datasource_transaction_manager_commit', 'Commit'], ['plugin_datasource_transaction_manager_rollback', 'Rollback'], @@ -7,4 +14,13 @@ export default [ ['plugin_datasource_transaction_manager_rollback_fail', 'Failed to rollback transaction'], ['plugin_datasource_transaction_manager_commit_mode_fail', 'Failed to change commit mode'], ['plugin_datasource_transaction_manager_commit_confirmation_message', 'Do you want to commit changes?'], + + ['plugin_datasource_transaction_manager_logs', 'Transaction log'], + ['plugin_datasource_transaction_manager_logs_tooltip', 'Open transaction log'], + ['plugin_datasource_transaction_manager_logs_table_column_time', 'Time'], + ['plugin_datasource_transaction_manager_logs_table_column_type', 'Type'], + ['plugin_datasource_transaction_manager_logs_table_column_text', 'Query'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Duration (ms)'], + ['plugin_datasource_transaction_manager_logs_table_column_rows', 'Rows'], + ['plugin_datasource_transaction_manager_logs_table_column_result', 'Result'], ]; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/locales/fr.ts b/webapp/packages/plugin-datasource-transaction-manager/src/locales/fr.ts index 76278e2a5c..662db9253f 100644 --- a/webapp/packages/plugin-datasource-transaction-manager/src/locales/fr.ts +++ b/webapp/packages/plugin-datasource-transaction-manager/src/locales/fr.ts @@ -14,4 +14,13 @@ export default [ ['plugin_datasource_transaction_manager_rollback_fail', "Échec de l'annulation de la transaction"], ['plugin_datasource_transaction_manager_commit_mode_fail', 'Échec du changement de mode de commit'], ['plugin_datasource_transaction_manager_commit_confirmation_message', 'Voulez-vous commiter les modifications ?'], + + ['plugin_datasource_transaction_manager_logs', 'Transaction log'], + ['plugin_datasource_transaction_manager_logs_tooltip', 'Open transaction log'], + ['plugin_datasource_transaction_manager_logs_table_column_time', 'Time'], + ['plugin_datasource_transaction_manager_logs_table_column_type', 'Type'], + ['plugin_datasource_transaction_manager_logs_table_column_text', 'Query'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Duration (ms)'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Rows'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Result'], ]; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/locales/it.ts b/webapp/packages/plugin-datasource-transaction-manager/src/locales/it.ts index f113567706..c398f786d2 100644 --- a/webapp/packages/plugin-datasource-transaction-manager/src/locales/it.ts +++ b/webapp/packages/plugin-datasource-transaction-manager/src/locales/it.ts @@ -1,3 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ export default [ ['plugin_datasource_transaction_manager_commit', 'Commit'], ['plugin_datasource_transaction_manager_rollback', 'Rollback'], @@ -7,4 +14,13 @@ export default [ ['plugin_datasource_transaction_manager_rollback_fail', 'Failed to rollback transaction'], ['plugin_datasource_transaction_manager_commit_mode_fail', 'Failed to change commit mode'], ['plugin_datasource_transaction_manager_commit_confirmation_message', 'Do you want to commit changes?'], + + ['plugin_datasource_transaction_manager_logs', 'Transaction log'], + ['plugin_datasource_transaction_manager_logs_tooltip', 'Open transaction log'], + ['plugin_datasource_transaction_manager_logs_table_column_time', 'Time'], + ['plugin_datasource_transaction_manager_logs_table_column_type', 'Type'], + ['plugin_datasource_transaction_manager_logs_table_column_text', 'Query'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Duration (ms)'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Rows'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Result'], ]; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/locales/ru.ts b/webapp/packages/plugin-datasource-transaction-manager/src/locales/ru.ts index 921b0e3d58..cc5098fecf 100644 --- a/webapp/packages/plugin-datasource-transaction-manager/src/locales/ru.ts +++ b/webapp/packages/plugin-datasource-transaction-manager/src/locales/ru.ts @@ -1,3 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ export default [ ['plugin_datasource_transaction_manager_commit', 'Commit'], ['plugin_datasource_transaction_manager_rollback', 'Rollback'], @@ -7,4 +14,13 @@ export default [ ['plugin_datasource_transaction_manager_rollback_fail', 'Не удалось выполнить откат'], ['plugin_datasource_transaction_manager_commit_mode_fail', 'Не удалось переключить режим коммита'], ['plugin_datasource_transaction_manager_commit_confirmation_message', 'Вы хотите зафиксировать изменения?'], + + ['plugin_datasource_transaction_manager_logs', 'Журнал транзакции'], + ['plugin_datasource_transaction_manager_logs_tooltip', 'Открыть журнал транзакции'], + ['plugin_datasource_transaction_manager_logs_table_column_time', 'Время'], + ['plugin_datasource_transaction_manager_logs_table_column_type', 'Тип'], + ['plugin_datasource_transaction_manager_logs_table_column_text', 'Запрос'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Продолжительность (мс)'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Строки'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Результат'], ]; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/locales/zh.ts b/webapp/packages/plugin-datasource-transaction-manager/src/locales/zh.ts index ad0fc52178..b86156f6a5 100644 --- a/webapp/packages/plugin-datasource-transaction-manager/src/locales/zh.ts +++ b/webapp/packages/plugin-datasource-transaction-manager/src/locales/zh.ts @@ -14,4 +14,13 @@ export default [ ['plugin_datasource_transaction_manager_rollback_fail', '回滚事务失败'], ['plugin_datasource_transaction_manager_commit_mode_fail', '切换提交方式失败'], ['plugin_datasource_transaction_manager_commit_confirmation_message', '是否提交更改?'], + + ['plugin_datasource_transaction_manager_logs', 'Transaction log'], + ['plugin_datasource_transaction_manager_logs_tooltip', 'Open transaction log'], + ['plugin_datasource_transaction_manager_logs_table_column_time', 'Time'], + ['plugin_datasource_transaction_manager_logs_table_column_type', 'Type'], + ['plugin_datasource_transaction_manager_logs_table_column_text', 'Query'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Duration (ms)'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Rows'], + ['plugin_datasource_transaction_manager_logs_table_column_duration', 'Result'], ]; diff --git a/webapp/packages/plugin-datasource-transaction-manager/src/manifest.ts b/webapp/packages/plugin-datasource-transaction-manager/src/manifest.ts index bf9094704d..7e8fdf8576 100644 --- a/webapp/packages/plugin-datasource-transaction-manager/src/manifest.ts +++ b/webapp/packages/plugin-datasource-transaction-manager/src/manifest.ts @@ -16,5 +16,8 @@ export const datasourceTransactionManagerPlugin: PluginManifest = { () => import('./TransactionManagerBootstrap.js').then(m => m.TransactionManagerBootstrap), () => import('./TransactionManagerSettingsService.js').then(m => m.TransactionManagerSettingsService), () => import('./LocaleService.js').then(m => m.LocaleService), + () => import('./TransactionManagerService.js').then(m => m.TransactionManagerService), + () => import('./TransactionLog/TransactionLogCountResource.js').then(m => m.TransactionLogCountResource), + () => import('./TransactionLog/TransactionLogCountEventHandler.js').then(m => m.TransactionLogCountEventHandler), ], }; diff --git a/webapp/packages/plugin-datasource-transaction-manager/tsconfig.json b/webapp/packages/plugin-datasource-transaction-manager/tsconfig.json index 31735c5e23..ac5cc4afcd 100644 --- a/webapp/packages/plugin-datasource-transaction-manager/tsconfig.json +++ b/webapp/packages/plugin-datasource-transaction-manager/tsconfig.json @@ -27,6 +27,9 @@ { "path": "../core-localization/tsconfig.json" }, + { + "path": "../core-sdk/tsconfig.json" + }, { "path": "../core-settings/tsconfig.json" }, @@ -39,9 +42,18 @@ { "path": "../core-view/tsconfig.json" }, + { + "path": "../plugin-codemirror6/tsconfig.json" + }, + { + "path": "../plugin-data-grid/tsconfig.json" + }, { "path": "../plugin-datasource-context-switch/tsconfig.json" }, + { + "path": "../plugin-sql-editor-new/tsconfig.json" + }, { "path": "../plugin-top-app-bar/tsconfig.json" } diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/ObjectsDescriptionSettingsForm.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/ObjectsDescriptionSettingsForm.tsx new file mode 100644 index 0000000000..cdeb7df02e --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/ObjectsDescriptionSettingsForm.tsx @@ -0,0 +1,42 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { type PlaceholderComponent, type PlaceholderElement, Switch, useTranslate } from '@cloudbeaver/core-blocks'; + +import type { IElementsTreeSettingsProps } from './ElementsTreeSettingsService.js'; + +export const ObjectsDescriptionSettingsForm: PlaceholderComponent = observer(function ObjectsDescriptionSettingsForm({ + tree: { root, settings }, +}) { + const translate = useTranslate(); + + if (!settings) { + return null; + } + + return ( + + {translate('app_navigationTree_settings_filter_objects_description')} + + ); +}); + +export const ObjectsDescriptionSettingsPlaceholderElement: PlaceholderElement = { + id: 'settings-objects-description', + component: ObjectsDescriptionSettingsForm, + order: 2, +}; diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/createElementsTreeSettings.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/createElementsTreeSettings.ts index ede0403de8..3652c1ef53 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/createElementsTreeSettings.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSettings/createElementsTreeSettings.ts @@ -17,6 +17,7 @@ export function createElementsTreeSettings(defaults?: Partial !!navNodeInfoResource.getException(node.id) || !!navTreeResource.getException(node.id)); @@ -110,6 +113,7 @@ export const NavigationNodeControl: NavTreeControlComponent = observer( let icon = nodeInfo.icon; const name = nodeInfo.name; + const description = nodeInfo.description; const title = nodeInfo.tooltip; if (error) { @@ -145,7 +149,12 @@ export const NavigationNodeControl: NavTreeControlComponent = observer( {editing ? ( ) : ( -
{name}
+
+ {name} + {elementsTreeContext?.tree.settings?.objectsDescription && description && ( + {` - ${description}`} + )} +
)} diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/transformDescriptionNodeInfo.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/transformDescriptionNodeInfo.ts new file mode 100644 index 0000000000..207033a594 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/transformDescriptionNodeInfo.ts @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; + +import type { IElementsTreeCustomNodeInfo } from './useElementsTree.js'; + +export function transformDescriptionNodeInfo(navNodeInfoResource: NavNodeInfoResource): IElementsTreeCustomNodeInfo { + return function transformDescriptionNodeInfo(nodeId, info) { + const node = navNodeInfoResource.get(nodeId); + + if (node?.description && !node.folder) { + return { + ...info, + description: node.description, + }; + } + + return info; + }; +} 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 1b397c3e98..927ff50229 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts @@ -77,6 +77,7 @@ export interface IElementsTreeSettings { showFolderExplorerPath: boolean; configurable: boolean; projects: boolean; + objectsDescription: boolean; } export interface IElementsTreeOptions { diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx index 5f94ed089c..b0d39f5ce2 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx @@ -24,6 +24,8 @@ import { createElementsTreeSettings, validateElementsTreeSettings, } from './ElementsTree/ElementsTreeTools/NavigationTreeSettings/createElementsTreeSettings.js'; +import { ObjectsDescriptionSettingsPlaceholderElement } from './ElementsTree/ElementsTreeTools/NavigationTreeSettings/ObjectsDescriptionSettingsForm.js'; +import { transformDescriptionNodeInfo } from './ElementsTree/transformDescriptionNodeInfo.js'; import { transformFilteredNodeInfo } from './ElementsTree/transformFilteredNodeInfo.js'; import type { IElementsTreeSettings } from './ElementsTree/useElementsTree.js'; import elementsTreeToolsStyles from './ElementsTreeTools.module.css'; @@ -77,12 +79,13 @@ export const NavigationTree = observer(function NavigationTree() { [navNodeInfoResource, projectsService, projectsNavNodeService], ); const transformFilteredNode = useMemo(() => transformFilteredNodeInfo(navNodeInfoResource), [navNodeInfoResource]); + const transformDescriptionNode = useMemo(() => transformDescriptionNodeInfo(navNodeInfoResource), [navNodeInfoResource]); const projectFilter = useMemo( () => navigationTreeProjectFilter(projectsNavNodeService, projectsService, navNodeInfoResource, navTreeResource), [projectsNavNodeService, projectsService, navNodeInfoResource, navTreeResource], ); - const settingsElements = useMemo(() => [ProjectsSettingsPlaceholderElement], []); + const settingsElements = useMemo(() => [ProjectsSettingsPlaceholderElement, ObjectsDescriptionSettingsPlaceholderElement], []); return ( @@ -93,7 +96,7 @@ export const NavigationTree = observer(function NavigationTree() { filters={[duplicateFilter, connectionGroupFilter, projectFilter]} renderers={[projectsRendererRenderer, navigationTreeConnectionGroupRenderer, connectionRenderer]} navNodeFilterCompare={navigationTreeProjectSearchCompare} - nodeInfoTransformers={[transformFilteredNode]} + nodeInfoTransformers={[transformFilteredNode, transformDescriptionNode]} expandStateGetters={[projectsExpandStateGetter]} settingsElements={settingsElements} className={s(styles, { elementsTree: true })} diff --git a/webapp/packages/plugin-navigation-tree/src/locales/en.ts b/webapp/packages/plugin-navigation-tree/src/locales/en.ts index c29ccb8baa..3ce0050ba3 100644 --- a/webapp/packages/plugin-navigation-tree/src/locales/en.ts +++ b/webapp/packages/plugin-navigation-tree/src/locales/en.ts @@ -12,6 +12,7 @@ export default [ ['app_navigationTree_limited', 'Elements are limited to {arg:limit} items'], ['app_navigationTree_action_link_with_editor', 'Link with editor'], ['app_navigationTree_action_collapse_all', 'Collapse all'], + ['app_navigationTree_settings_filter_objects_description', 'Show objects description'], ['app_navigationTree_settings_filter_title', 'Filter'], ['app_navigationTree_settings_filter_description', 'Show filtering field'], ['app_navigationTree_settings_filter_all_title', 'Show collapsed'], diff --git a/webapp/packages/plugin-navigation-tree/src/locales/fr.ts b/webapp/packages/plugin-navigation-tree/src/locales/fr.ts index f8f14c4060..0c655f5333 100644 --- a/webapp/packages/plugin-navigation-tree/src/locales/fr.ts +++ b/webapp/packages/plugin-navigation-tree/src/locales/fr.ts @@ -12,6 +12,7 @@ export default [ ['app_navigationTree_limited', 'Les éléments sont limités à {arg:limit} éléments'], ['app_navigationTree_link_with_editor', "Lier avec l'éditeur"], ['app_navigationTree_action_collapse_all', 'Tout réduire'], + ['app_navigationTree_settings_filter_objects_description', 'Show objects description'], ['app_navigationTree_settings_filter_title', 'Filtrer'], ['app_navigationTree_settings_filter_description', 'Affiche les éléments filtrés dans les dossiers réduits'], ['app_navigationTree_settings_filter_all_title', 'Tout filtrer'], diff --git a/webapp/packages/plugin-navigation-tree/src/locales/it.ts b/webapp/packages/plugin-navigation-tree/src/locales/it.ts index f5ee281364..43f9ba7a4d 100644 --- a/webapp/packages/plugin-navigation-tree/src/locales/it.ts +++ b/webapp/packages/plugin-navigation-tree/src/locales/it.ts @@ -12,6 +12,7 @@ export default [ ['app_navigationTree_limited', 'Elements are limited to {arg:limit} items'], ['app_navigationTree_action_link_with_editor', 'Link with editor'], ['app_navigationTree_action_collapse_all', 'Collapse all'], + ['app_navigationTree_settings_filter_objects_description', 'Show objects description'], ['app_navigationTree_settings_filter_title', 'Filter'], ['app_navigationTree_settings_filter_description', 'Show filtering field'], ['app_navigationTree_settings_filter_all_title', 'Show collapsed'], diff --git a/webapp/packages/plugin-navigation-tree/src/locales/ru.ts b/webapp/packages/plugin-navigation-tree/src/locales/ru.ts index 565cc04503..15f97b9e16 100644 --- a/webapp/packages/plugin-navigation-tree/src/locales/ru.ts +++ b/webapp/packages/plugin-navigation-tree/src/locales/ru.ts @@ -13,6 +13,7 @@ export default [ ['app_navigationTree_action_link_with_editor', 'Синхронизовать с редактором'], ['app_navigationTree_action_collapse_all', 'Свернуть все элементы'], + ['app_navigationTree_settings_filter_objects_description', 'Показывать описание объектов'], ['app_navigationTree_settings_filter_title', 'Фильтр'], ['app_navigationTree_settings_filter_description', 'Показывать фильтр'], diff --git a/webapp/packages/plugin-navigation-tree/src/locales/zh.ts b/webapp/packages/plugin-navigation-tree/src/locales/zh.ts index bf573f317d..a4ff413d00 100644 --- a/webapp/packages/plugin-navigation-tree/src/locales/zh.ts +++ b/webapp/packages/plugin-navigation-tree/src/locales/zh.ts @@ -11,6 +11,7 @@ export default [ ['app_navigationTree_limited', '元素数量限制为 {arg:limit} '], ['app_navigationTree_action_link_with_editor', '连接至编辑器'], ['app_navigationTree_action_collapse_all', '收起全部'], + ['app_navigationTree_settings_filter_objects_description', 'Show objects description'], ['app_navigationTree_settings_filter_title', '过滤器'], ['app_navigationTree_settings_filter_description', '查看过滤字段'], ['app_navigationTree_settings_filter_all_title', '查看收缩项'], diff --git a/webapp/packages/plugin-sql-generator/src/GeneratorMenuBootstrap.ts b/webapp/packages/plugin-sql-generator/src/GeneratorMenuBootstrap.ts index d2f1031429..d2d1e3482f 100644 --- a/webapp/packages/plugin-sql-generator/src/GeneratorMenuBootstrap.ts +++ b/webapp/packages/plugin-sql-generator/src/GeneratorMenuBootstrap.ts @@ -43,6 +43,7 @@ export class GeneratorMenuBootstrap extends Bootstrap { const presentation = context.get(DATA_CONTEXT_DV_PRESENTATION); return ( !model.isReadonly(resultIndex) && + model.hasElementIdentifier(resultIndex) && model.source.getResult(resultIndex)?.dataFormat === ResultDataFormat.Resultset && !presentation?.readonly && (!presentation || presentation.type === DataViewerPresentationType.Data) diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/AuthenticationPanel.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/AuthenticationPanel.tsx deleted file mode 100644 index 6a04a56dc9..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/AuthenticationPanel.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { observer } from 'mobx-react-lite'; - -import { ColoredContainer, Container, useAutoLoad } from '@cloudbeaver/core-blocks'; -import { type TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; - -import type { UserProfileFormProps } from '../UserProfileFormService.js'; -import { ChangePassword } from './ChangePassword.js'; -import type { UserProfileFormAuthenticationPart } from './UserProfileFormAuthenticationPart.js'; - -export const AuthenticationPanel: TabContainerPanelComponent = observer(function AuthenticationPanel({ tabId }) { - const tab = useTab(tabId); - const tabState = useTabState(); - - useAutoLoad(AuthenticationPanel, tabState, tab.selected); - - const disabled = tabState.isLoading(); - return ( - - - - - - ); -}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/ChangePassword.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/ChangePassword.tsx index 450929203f..6c6156ef13 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/ChangePassword.tsx +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/ChangePassword.tsx @@ -5,20 +5,55 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { Form, Group, GroupTitle, InputField, useCustomInputValidation, usePasswordValidation, useTranslate } from '@cloudbeaver/core-blocks'; +import { UserInfoResource } from '@cloudbeaver/core-authentication'; +import { + ColoredContainer, + ConfirmationDialog, + Container, + Form, + Group, + GroupTitle, + InputField, + ToolsAction, + ToolsPanel, + useCustomInputValidation, + useExecutor, + useForm, + useObservableRef, + usePasswordValidation, + useTranslate, +} from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { ExecutorInterrupter } from '@cloudbeaver/core-executor'; import { isValuesEqual } from '@cloudbeaver/core-utils'; -import type { IUserProfileFormAuthenticationState } from './IUserProfileFormAuthenticationState.js'; +import { userProfileContext } from '../../userProfileContext.js'; +import { UserProfileOptionsPanelService } from '../../UserProfileOptionsPanelService.js'; -interface Props { - state: IUserProfileFormAuthenticationState; - disabled?: boolean; -} - -export const ChangePassword = observer(function ChangePassword({ state, disabled }) { +export const ChangePassword = observer(function ChangePassword() { const translate = useTranslate(); + const state = useObservableRef( + { + oldPassword: '', + password: '', + repeatedPassword: '', + }, + { + oldPassword: observable.ref, + password: observable.ref, + repeatedPassword: observable.ref, + }, + ); + const notificationService = useService(NotificationService); + const userProfileOptionsPanelService = useService(UserProfileOptionsPanelService); + const userInfoResource = useService(UserInfoResource); + const commonDialogService = useService(CommonDialogService); + const disabled = userInfoResource.isLoading(); const passwordValidationRef = usePasswordValidation(); const passwordRepeatRef = useCustomInputValidation(value => { if (!isValuesEqual(value, state.password, null)) { @@ -27,47 +62,106 @@ export const ChangePassword = observer(function ChangePassword({ state, d return null; }); + const form = useForm({ + async onSubmit() { + try { + await userInfoResource.updateLocalPassword(state.oldPassword, state.password); + resetForm(); + notificationService.logSuccess({ title: 'plugin_user_profile_authentication_change_password_success' }); + } catch (exception) { + if (exception instanceof Error) { + notificationService.logException(exception); + } + } + }, + }); + + function resetForm() { + state.oldPassword = ''; + state.password = ''; + state.repeatedPassword = ''; + form.ref?.reset(); + } + + useExecutor({ + executor: userProfileOptionsPanelService.onClose, + handlers: [ + async function closeHandler(_, contexts) { + const context = contexts.getContext(userProfileContext); + + if ((state.oldPassword || state.password || state.repeatedPassword) && !context.force) { + const result = await commonDialogService.open(ConfirmationDialog, { + title: 'plugin_user_profile_authentication_change_password_cancel_title', + message: 'plugin_user_profile_authentication_change_password_cancel_message', + confirmActionText: 'ui_processing_ok', + }); + + if (result === DialogueStateResult.Rejected) { + ExecutorInterrupter.interrupt(contexts); + } + } + }, + ], + }); + return ( -
- - {translate('plugin_user_profile_authentication_change_password')} - value?.trim() ?? ''} - small - required - > - {translate('plugin_user_profile_authentication_change_password_current_password')} - - value?.trim() ?? ''} - small - required - > - {translate('plugin_user_profile_authentication_change_password_new_password')} - - value?.trim() ?? ''} - small - required - > - {translate('plugin_user_profile_authentication_change_password_repeat_password')} - - -
+ + +
+ + + + form.submit()}> + {translate('plugin_user_profile_authentication_change_password_submit_label')} + + + {translate('ui_clear')} + + + + + + {translate('plugin_user_profile_authentication_change_password')} + value?.trim() ?? ''} + small + required + > + {translate('plugin_user_profile_authentication_change_password_current_password')} + + value?.trim() ?? ''} + small + required + > + {translate('plugin_user_profile_authentication_change_password_new_password')} + + value?.trim() ?? ''} + small + required + > + {translate('plugin_user_profile_authentication_change_password_repeat_password')} + + + +
+
+
); }); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPart.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPart.ts deleted file mode 100644 index 7f4e6eddf4..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPart.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import type { PasswordPolicyService, UserInfoMetaParametersResource, UserInfoResource } from '@cloudbeaver/core-authentication'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { FormPart, formValidationContext, type IFormState } from '@cloudbeaver/core-ui'; -import { isValuesEqual, schemaValidationError } from '@cloudbeaver/core-utils'; - -import type { IUserProfileFormState } from '../UserProfileFormService.js'; -import { - type IUserProfileFormAuthenticationState, - USER_PROFILE_FORM_AUTHENTICATION_PART_STATE_SCHEMA, -} from './IUserProfileFormAuthenticationState.js'; - -export class UserProfileFormAuthenticationPart extends FormPart { - constructor( - formState: IFormState, - private readonly userInfoResource: UserInfoResource, - private readonly passwordPolicyService: PasswordPolicyService, - private readonly userInfoMetaParametersResource: UserInfoMetaParametersResource, - ) { - super(formState, { - oldPassword: '', - password: '', - repeatedPassword: '', - }); - } - - protected override format(data: IFormState, contexts: IExecutionContextProvider>): void { - const parsed = USER_PROFILE_FORM_AUTHENTICATION_PART_STATE_SCHEMA.safeParse(this.state); - - this.state = parsed.success ? parsed.data : this.initialState; - } - - override isOutdated(): boolean { - return this.userInfoResource.isOutdated(undefined) || this.userInfoMetaParametersResource.isOutdated(undefined); - } - - override isLoaded(): boolean { - return this.loaded && this.userInfoResource.isLoaded(undefined) && this.userInfoMetaParametersResource.isLoaded(undefined); - } - - override get isChanged(): boolean { - if (!this.loaded) { - return false; - } - - return ( - !isValuesEqual(this.state.oldPassword, this.initialState.oldPassword, null) || - !isValuesEqual(this.state.password, this.initialState.password, null) || - !isValuesEqual(this.state.repeatedPassword, this.initialState.repeatedPassword, null) - ); - } - - protected override validate( - data: IFormState, - contexts: IExecutionContextProvider>, - ): void | Promise { - const state = USER_PROFILE_FORM_AUTHENTICATION_PART_STATE_SCHEMA.safeParse(this.state); - const validation = contexts.getContext(formValidationContext); - - if (!state.success) { - validation.error(schemaValidationError(state.error, { prefix: null }).toString()); - return; - } - - const passwordValidation = this.passwordPolicyService.validatePassword(this.state.password); - - if (!passwordValidation.isValid) { - validation.error(passwordValidation.errorMessage); - return; - } - } - - protected async saveChanges(): Promise { - await this.userInfoResource.updateLocalPassword(this.state.oldPassword, this.state.password); - } - - protected override async loader() { - this.setInitialState({ - oldPassword: '', - password: '', - repeatedPassword: '', - }); - } -} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartBootstrap.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartBootstrap.ts index a2d86626a9..bbc8b64d06 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartBootstrap.ts +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartBootstrap.ts @@ -5,27 +5,30 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { UserProfileFormService } from '../UserProfileFormService.js'; -import { getUserProfileFormAuthenticationPart } from './getUserProfileFormAuthenticationPart.js'; +import { UserProfileTabsService } from '../../UserProfileTabsService.js'; -const AuthenticationPanel = importLazyComponent(() => import('./AuthenticationPanel.js').then(m => m.AuthenticationPanel)); +const ChangePassword = importLazyComponent(() => import('./ChangePassword.js').then(m => m.ChangePassword)); @injectable() export class UserProfileFormAuthenticationPartBootstrap extends Bootstrap { - constructor(private readonly userProfileFormService: UserProfileFormService) { + constructor( + private readonly userProfileTabsService: UserProfileTabsService, + private readonly userInfoResource: UserInfoResource, + ) { super(); } override register(): void { - this.userProfileFormService.parts.add({ + this.userProfileTabsService.tabContainer.add({ key: 'authentication', name: 'ui_authentication', - order: 2, - panel: () => AuthenticationPanel, - stateGetter: props => () => getUserProfileFormAuthenticationPart(props.formState), + order: 4, + isHidden: () => this.userInfoResource.isAnonymous(), + panel: () => ChangePassword, }); } } diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartService.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartService.ts deleted file mode 100644 index 217752eea8..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartService.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; -import { injectable } from '@cloudbeaver/core-di'; - -import type { UserProfileFormProps } from '../UserProfileFormService.js'; - -@injectable() -export class UserProfileFormAuthenticationPartService { - placeholderContainer: PlaceholderContainer; - constructor() { - this.placeholderContainer = new PlaceholderContainer(); - } -} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/getUserProfileFormAuthenticationPart.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/getUserProfileFormAuthenticationPart.ts deleted file mode 100644 index 6859988100..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/getUserProfileFormAuthenticationPart.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { PasswordPolicyService, UserInfoMetaParametersResource, UserInfoResource } from '@cloudbeaver/core-authentication'; -import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; -import type { IFormState } from '@cloudbeaver/core-ui'; - -import type { IUserProfileFormState } from '../UserProfileFormService.js'; -import { UserProfileFormAuthenticationPart } from './UserProfileFormAuthenticationPart.js'; - -const DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART = createDataContext('User Profile Form Info Part'); - -export function getUserProfileFormAuthenticationPart(formState: IFormState): UserProfileFormAuthenticationPart { - return formState.getPart(DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART, context => { - const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; - const userInfoResource = di.getService(UserInfoResource); - const passwordPolicyService = di.getService(PasswordPolicyService); - const userInfoMetaParametersResource = di.getService(UserInfoMetaParametersResource); - - return new UserProfileFormAuthenticationPart(formState, userInfoResource, passwordPolicyService, userInfoMetaParametersResource); - }); -} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfo.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfo.tsx index 1c2f1b3bef..77baed0b0b 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfo.tsx +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfo.tsx @@ -5,43 +5,48 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { toJS } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { ColoredContainer, Container, Group, GroupTitle, InputField, Loader, useAutoLoad, useTranslate } from '@cloudbeaver/core-blocks'; -import { type TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; +import { UserInfoMetaParametersResource, UserInfoResource } from '@cloudbeaver/core-authentication'; +import { ColoredContainer, Container, Group, GroupTitle, InputField, Loader, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { type TabContainerPanelComponent, useTab } from '@cloudbeaver/core-ui'; -import type { UserProfileFormProps } from '../UserProfileFormService.js'; +import type { IUserProfileFormInfoState } from './IUserProfileFormInfoState.js'; import { UserActiveAuthMethods } from './UserActiveAuthMethods/UserActiveAuthMethods.js'; import { UserProfileFormInfoMetaParameters } from './UserProfileFormInfoMetaParameters.js'; -import type { UserProfileFormInfoPart } from './UserProfileFormInfoPart.js'; -export const UserProfileFormInfo: TabContainerPanelComponent = observer(function UserProfileFormInfo({ tabId }) { +export const UserProfileFormInfo: TabContainerPanelComponent = observer(function UserProfileFormInfo({ tabId }) { const translate = useTranslate(); + const userInfoResource = useResource(UserProfileFormInfo, UserInfoResource, undefined); + const userInfoMetaParametersResource = useResource(UserProfileFormInfo, UserInfoMetaParametersResource, undefined); + const disabled = userInfoResource.isLoading() || userInfoMetaParametersResource.isLoading(); const tab = useTab(tabId); - const tabState = useTabState(); - useAutoLoad(UserProfileFormInfo, tabState, tab.selected); - - const disabled = tabState.isLoading(); + const state: IUserProfileFormInfoState = { + userId: userInfoResource.data?.userId || '', + displayName: userInfoResource.data?.displayName || '', + authRole: userInfoResource.data?.authRole || '', + metaParameters: toJS(userInfoMetaParametersResource.data || {}), + }; return ( - + - {translate('plugin_user_profile_info')} - + {translate('plugin_user_profile_info_id')} - + {translate('plugin_user_profile_info_displayName')} - + {translate('authentication_user_role')} - + diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoMetaParameters.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoMetaParameters.tsx index adfd9879f0..0e59ee2cbf 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoMetaParameters.tsx +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoMetaParameters.tsx @@ -10,15 +10,15 @@ import { observer } from 'mobx-react-lite'; import { UserMetaParametersResource } from '@cloudbeaver/core-authentication'; import { Container, ObjectPropertyInfoForm, useResource } from '@cloudbeaver/core-blocks'; -import type { UserProfileFormInfoPart } from './UserProfileFormInfoPart.js'; +import type { IUserProfileFormInfoState } from './IUserProfileFormInfoState.js'; interface Props { - tabState: UserProfileFormInfoPart; + state: IUserProfileFormInfoState; tabSelected: boolean; disabled: boolean; } -export const UserProfileFormInfoMetaParameters = observer(function UserProfileFormInfoMetaParameters({ tabState, tabSelected, disabled }) { +export const UserProfileFormInfoMetaParameters = observer(function UserProfileFormInfoMetaParameters({ state, tabSelected, disabled }) { const userMetaParameters = useResource(UserProfileFormInfoMetaParameters, UserMetaParametersResource, undefined, { active: tabSelected }); if (userMetaParameters.data.length === 0) { @@ -27,7 +27,7 @@ export const UserProfileFormInfoMetaParameters = observer(function UserPr return ( - + ); }); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPart.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPart.ts deleted file mode 100644 index 89a5cc7f73..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPart.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { toJS } from 'mobx'; - -import type { UserInfoMetaParametersResource, UserInfoResource } from '@cloudbeaver/core-authentication'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; -import { FormPart, type IFormState } from '@cloudbeaver/core-ui'; -import { isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils'; - -import type { IUserProfileFormState } from '../UserProfileFormService.js'; -import { type IUserProfileFormInfoState, USER_PROFILE_FORM_INFO_PART_STATE_SCHEMA } from './IUserProfileFormInfoState.js'; - -export class UserProfileFormInfoPart extends FormPart { - constructor( - formState: IFormState, - private readonly userInfoResource: UserInfoResource, - private readonly userInfoMetaParametersResource: UserInfoMetaParametersResource, - ) { - super(formState, { - userId: userInfoResource.data?.userId || '', - displayName: userInfoResource.data?.displayName || '', - authRole: userInfoResource.data?.authRole || '', - metaParameters: toJS(userInfoMetaParametersResource.data || {}), - }); - } - - protected override format(data: IFormState, contexts: IExecutionContextProvider>): void { - this.state = USER_PROFILE_FORM_INFO_PART_STATE_SCHEMA.parse(this.state); - } - - override isOutdated(): boolean { - return this.userInfoResource.isOutdated(undefined) || this.userInfoMetaParametersResource.isOutdated(undefined); - } - - override isLoaded(): boolean { - return this.loaded && this.userInfoResource.isLoaded(undefined) && this.userInfoMetaParametersResource.isLoaded(undefined); - } - - override get isChanged(): boolean { - if (!this.loaded) { - return false; - } - - return ( - !isValuesEqual(this.state.userId, this.initialState.userId, null) || - !isValuesEqual(this.state.displayName, this.initialState.displayName, null) || - !isObjectsEqual(this.state.metaParameters, this.initialState.metaParameters) || - !isValuesEqual(this.state.authRole, this.initialState.authRole, '') - ); - } - - protected saveChanges(): Promise { - return Promise.resolve(); - } - - protected override async loader() { - const [user, metaParameters] = await Promise.all([this.userInfoResource.load(undefined), this.userInfoMetaParametersResource.load(undefined)]); - - this.setInitialState({ - userId: user?.userId || '', - displayName: user?.displayName || '', - authRole: user?.authRole ?? '', - metaParameters: toJS(metaParameters || {}), - }); - } -} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.ts index 78b4f743ee..23f0094821 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.ts +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.ts @@ -5,27 +5,30 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { UserProfileFormService } from '../UserProfileFormService.js'; -import { getUserProfileFormInfoPart } from './getUserProfileFormInfoPart.js'; +import { UserProfileTabsService } from '../../UserProfileTabsService.js'; const UserProfileFormInfo = importLazyComponent(() => import('./UserProfileFormInfo.js').then(m => m.UserProfileFormInfo)); @injectable() export class UserProfileFormInfoPartBootstrap extends Bootstrap { - constructor(private readonly userProfileFormService: UserProfileFormService) { + constructor( + private readonly userProfileTabsService: UserProfileTabsService, + private readonly userInfoResource: UserInfoResource, + ) { super(); } override register(): void { - this.userProfileFormService.parts.add({ - key: 'info', + this.userProfileTabsService.tabContainer.add({ + key: 'user_info', name: 'plugin_user_profile_info', order: 1, + isHidden: () => this.userInfoResource.isAnonymous(), panel: () => UserProfileFormInfo, - stateGetter: props => () => getUserProfileFormInfoPart(props.formState), }); } } diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartService.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartService.ts deleted file mode 100644 index a53930294b..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartService.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; -import { injectable } from '@cloudbeaver/core-di'; - -import type { UserProfileFormProps } from '../UserProfileFormService.js'; - -@injectable() -export class UserProfileFormInfoPartService { - placeholderContainer: PlaceholderContainer; - constructor() { - this.placeholderContainer = new PlaceholderContainer(); - } -} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/getUserProfileFormInfoPart.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/getUserProfileFormInfoPart.ts deleted file mode 100644 index 10580b8e3d..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/getUserProfileFormInfoPart.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { UserInfoMetaParametersResource, UserInfoResource } from '@cloudbeaver/core-authentication'; -import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; -import type { IFormState } from '@cloudbeaver/core-ui'; - -import type { IUserProfileFormState } from '../UserProfileFormService.js'; -import { UserProfileFormInfoPart } from './UserProfileFormInfoPart.js'; - -const DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART = createDataContext('User Profile Form Info Part'); - -export function getUserProfileFormInfoPart(formState: IFormState): UserProfileFormInfoPart { - return formState.getPart(DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART, context => { - const di = context.get(DATA_CONTEXT_DI_PROVIDER)!; - const userInfoResource = di.getService(UserInfoResource); - const userInfoMetaParametersResource = di.getService(UserInfoMetaParametersResource); - - return new UserProfileFormInfoPart(formState, userInfoResource, userInfoMetaParametersResource); - }); -} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileForm.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileForm.tsx deleted file mode 100644 index 81a1358326..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileForm.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { observer } from 'mobx-react-lite'; - -import { useService } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { BaseForm, type IBaseFormSubmitInfo, type IFormState } from '@cloudbeaver/core-ui'; - -import { type IUserProfileFormState, UserProfileFormService } from './UserProfileFormService.js'; - -interface Props { - state: IFormState; -} - -export const UserProfileForm = observer(function UserProfileForm({ state }) { - const notificationService = useService(NotificationService); - const userProfileFormService = useService(UserProfileFormService); - - function onSubmit({ success }: IBaseFormSubmitInfo) { - if (success) { - notificationService.logSuccess({ title: 'authentication_administration_user_updated' }); - } else { - notificationService.logError({ title: 'authentication_administration_user_update_failed' }); - } - } - - return ; -}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormBootstrap.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormBootstrap.ts deleted file mode 100644 index 260ebe8831..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormBootstrap.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { UserInfoResource } from '@cloudbeaver/core-authentication'; -import { importLazyComponent } from '@cloudbeaver/core-blocks'; -import { Bootstrap, injectable } from '@cloudbeaver/core-di'; - -import { UserProfileTabsService } from '../UserProfileTabsService.js'; - -const UserProfileFormPanel = importLazyComponent(() => import('./UserProfileFormPanel.js').then(module => module.UserProfileFormPanel)); - -@injectable() -export class UserProfileFormBootstrap extends Bootstrap { - constructor( - private readonly userProfileTabsService: UserProfileTabsService, - private readonly userInfoResource: UserInfoResource, - ) { - super(); - } - - override register(): void { - this.userProfileTabsService.tabContainer.add({ - key: 'account', - name: 'plugin_user_profile_account_title', - order: 1, - isHidden: () => this.userInfoResource.isAnonymous(), - panel: () => UserProfileFormPanel, - }); - } -} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormPanel.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormPanel.tsx deleted file mode 100644 index 6831c578d3..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormPanel.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { observer } from 'mobx-react-lite'; -import { useState } from 'react'; - -import { ConfirmationDialog, useExecutor } from '@cloudbeaver/core-blocks'; -import { IServiceProvider, useService } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { ExecutorInterrupter } from '@cloudbeaver/core-executor'; -import { FormMode, type TabContainerPanelComponent } from '@cloudbeaver/core-ui'; - -import { userProfileContext } from '../userProfileContext.js'; -import { UserProfileOptionsPanelService } from '../UserProfileOptionsPanelService.js'; -import { UserProfileForm } from './UserProfileForm.js'; -import { UserProfileFormService } from './UserProfileFormService.js'; -import { UserProfileFormState } from './UserProfileFormState.js'; - -export const UserProfileFormPanel: TabContainerPanelComponent = observer(function UserProfileFormPanel({ tabId }) { - const serviceProvider = useService(IServiceProvider); - const userProfileFormService = useService(UserProfileFormService); - const userProfileOptionsPanelService = useService(UserProfileOptionsPanelService); - const commonDialogService = useService(CommonDialogService); - - const [state] = useState(() => { - const state = new UserProfileFormState(serviceProvider, userProfileFormService, {}); - state.setMode(FormMode.Edit); - - return state; - }); - - useExecutor({ - executor: userProfileOptionsPanelService.onClose, - handlers: [ - async function closeHandler(_, contexts) { - const context = contexts.getContext(userProfileContext); - - if (state.isChanged && !context.force) { - const result = await commonDialogService.open(ConfirmationDialog, { - title: 'plugin_connections_connection_edit_cancel_title', - message: 'plugin_connections_connection_edit_cancel_message', - confirmActionText: 'ui_processing_ok', - }); - - if (result === DialogueStateResult.Rejected) { - ExecutorInterrupter.interrupt(contexts); - } - } - }, - ], - }); - - return ; -}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormService.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormService.ts deleted file mode 100644 index 445ecb1dca..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormService.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { injectable } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; -import { LocalizationService } from '@cloudbeaver/core-localization'; -import { FormBaseService, type IFormProps } from '@cloudbeaver/core-ui'; - -export interface IUserProfileFormState {} - -export type UserProfileFormProps = IFormProps; - -@injectable() -export class UserProfileFormService extends FormBaseService { - constructor(localizationService: LocalizationService, notificationService: NotificationService) { - super(localizationService, notificationService, 'User profile form'); - } -} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormState.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormState.ts deleted file mode 100644 index 79221de577..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormState.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import type { IServiceProvider } from '@cloudbeaver/core-di'; -import { FormState } from '@cloudbeaver/core-ui'; - -import type { IUserProfileFormState, UserProfileFormService } from './UserProfileFormService.js'; - -export class UserProfileFormState extends FormState { - constructor(serviceProvider: IServiceProvider, service: UserProfileFormService, config: IUserProfileFormState) { - super(serviceProvider, service, config); - } -} diff --git a/webapp/packages/plugin-user-profile/src/locales/en.ts b/webapp/packages/plugin-user-profile/src/locales/en.ts index 2baa5952cf..d9f724d4b8 100644 --- a/webapp/packages/plugin-user-profile/src/locales/en.ts +++ b/webapp/packages/plugin-user-profile/src/locales/en.ts @@ -24,4 +24,9 @@ export default [ ['plugin_user_profile_authentication_change_password_submit_label', 'Change'], ['plugin_user_profile_authentication_change_password_passwords_not_match', "Passwords don't match"], ['plugin_user_profile_authentication_change_password_password_validation_error', 'Password validation failed'], + ['plugin_user_profile_authentication_change_password_cancel_title', 'Cancel password change'], + [ + 'plugin_user_profile_authentication_change_password_cancel_message', + "You're going to cancel password changes. Unsaved changes will be lost. Are you sure?", + ], ]; diff --git a/webapp/packages/plugin-user-profile/src/locales/fr.ts b/webapp/packages/plugin-user-profile/src/locales/fr.ts index 068d334836..3a2a54f68c 100644 --- a/webapp/packages/plugin-user-profile/src/locales/fr.ts +++ b/webapp/packages/plugin-user-profile/src/locales/fr.ts @@ -10,12 +10,12 @@ export default [ ['plugin_user_profile_menu', 'Profil'], ['plugin_user_profile_info', 'Infos'], ['plugin_user_profile_info_id', 'Identifiant'], - ['plugin_user_profile_info_displayName', 'Nom d\'affichage'], - ['plugin_user_profile_auth_providers', 'Fournisseurs d\'authentification'], + ['plugin_user_profile_info_displayName', "Nom d'affichage"], + ['plugin_user_profile_auth_providers', "Fournisseurs d'authentification"], ['plugin_user_profile_info', 'Informations utilisateur'], - ['plugin_user_profile_auth_tokens', 'Jetons d\'authentification'], - ['plugin_user_profile_auth_providers_active', 'L\'authentification est active'], - + ['plugin_user_profile_auth_tokens', "Jetons d'authentification"], + ['plugin_user_profile_auth_providers_active', "L'authentification est active"], + ['plugin_user_profile_authentication_change_password', 'Changer le mot de passe'], ['plugin_user_profile_authentication_change_password_current_password', 'Mot de passe actuel'], ['plugin_user_profile_authentication_change_password_new_password', 'Nouveau mot de passe'], @@ -24,4 +24,9 @@ export default [ ['plugin_user_profile_authentication_change_password_submit_label', 'Changer'], ['plugin_user_profile_authentication_change_password_passwords_not_match', 'Les mots de passe ne correspondent pas'], ['plugin_user_profile_authentication_change_password_validation_error', 'Échec de la validation du mot de passe'], + ['plugin_user_profile_authentication_change_password_cancel_title', 'Cancel password change'], + [ + 'plugin_user_profile_authentication_change_password_cancel_message', + "You're going to cancel password changes. Unsaved changes will be lost. Are you sure?", + ], ]; diff --git a/webapp/packages/plugin-user-profile/src/locales/it.ts b/webapp/packages/plugin-user-profile/src/locales/it.ts index 7a51e831df..99cd22746b 100644 --- a/webapp/packages/plugin-user-profile/src/locales/it.ts +++ b/webapp/packages/plugin-user-profile/src/locales/it.ts @@ -24,4 +24,9 @@ export default [ ['plugin_user_profile_authentication_change_password_submit_label', 'Modifica'], ['plugin_user_profile_authentication_change_password_passwords_not_match', 'Le Passwords non coincidono'], ['plugin_user_profile_authentication_change_password_password_validation_error', 'Password validation failed'], + ['plugin_user_profile_authentication_change_password_cancel_title', 'Cancel password change'], + [ + 'plugin_user_profile_authentication_change_password_cancel_message', + "You're going to cancel password changes. Unsaved changes will be lost. Are you sure?", + ], ]; diff --git a/webapp/packages/plugin-user-profile/src/locales/ru.ts b/webapp/packages/plugin-user-profile/src/locales/ru.ts index a4af6286c1..ca9500685e 100644 --- a/webapp/packages/plugin-user-profile/src/locales/ru.ts +++ b/webapp/packages/plugin-user-profile/src/locales/ru.ts @@ -24,4 +24,9 @@ export default [ ['plugin_user_profile_authentication_change_password_submit_label', 'Сменить'], ['plugin_user_profile_authentication_change_password_passwords_not_match', 'Пароли не совпадают'], ['plugin_user_profile_authentication_change_password_password_validation_error', 'Валидация пароля не удалась'], + ['plugin_user_profile_authentication_change_password_cancel_title', 'Отменить смену пароля'], + [ + 'plugin_user_profile_authentication_change_password_cancel_message', + 'Вы собираетесь отменить смену пароля. Несохраненные данные будут утеряны. Вы уверены?', + ], ]; diff --git a/webapp/packages/plugin-user-profile/src/locales/zh.ts b/webapp/packages/plugin-user-profile/src/locales/zh.ts index 0d61d88faf..75f9ab1aa2 100644 --- a/webapp/packages/plugin-user-profile/src/locales/zh.ts +++ b/webapp/packages/plugin-user-profile/src/locales/zh.ts @@ -24,4 +24,9 @@ export default [ ['plugin_user_profile_authentication_change_password_submit_label', '更改'], ['plugin_user_profile_authentication_change_password_passwords_not_match', '密码不匹配'], ['plugin_user_profile_authentication_change_password_password_validation_error', '密码校验失败'], + ['plugin_user_profile_authentication_change_password_cancel_title', 'Cancel password change'], + [ + 'plugin_user_profile_authentication_change_password_cancel_message', + "You're going to cancel password changes. Unsaved changes will be lost. Are you sure?", + ], ]; diff --git a/webapp/packages/plugin-user-profile/src/manifest.ts b/webapp/packages/plugin-user-profile/src/manifest.ts index 410dee4c9b..522faed898 100644 --- a/webapp/packages/plugin-user-profile/src/manifest.ts +++ b/webapp/packages/plugin-user-profile/src/manifest.ts @@ -17,17 +17,10 @@ export const userProfilePlugin: PluginManifest = { () => import('./LocaleService.js').then(m => m.LocaleService), () => import('./UserProfileTabsService.js').then(m => m.UserProfileTabsService), () => import('./UserProfileOptionsPanelService.js').then(m => m.UserProfileOptionsPanelService), - () => import('./UserProfileForm/UserProfileFormBootstrap.js').then(m => m.UserProfileFormBootstrap), - () => import('./UserProfileForm/UserProfileFormService.js').then(m => m.UserProfileFormService), () => import('./UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.js').then(m => m.UserProfileFormInfoPartBootstrap), - () => import('./UserProfileForm/UserInfoPart/UserProfileFormInfoPartService.js').then(m => m.UserProfileFormInfoPartService), () => import('./UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartBootstrap.js').then( m => m.UserProfileFormAuthenticationPartBootstrap, ), - () => - import('./UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartService.js').then( - m => m.UserProfileFormAuthenticationPartService, - ), ], }; diff --git a/webapp/packages/product-default/package.json b/webapp/packages/product-default/package.json index f311b824ab..f9d26128e4 100644 --- a/webapp/packages/product-default/package.json +++ b/webapp/packages/product-default/package.json @@ -6,7 +6,7 @@ "src/**/*.scss", "public/**/*" ], - "version": "24.3.3", + "version": "24.3.4", "description": "CloudBeaver Community", "license": "Apache-2.0", "main": "dist/index.js",