diff --git a/README.md b/README.md index a8cc4417a6..7a35231288 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,14 @@ You can see live demo of CloudBeaver here: https://demo.cloudbeaver.io ## Changelog +### CloudBeaver 20.0.6 - 2021-03-31 + +- Value panel for Data Viewer was added: a full text of your cell value on a special panel to view and edit. +- Spatial data support was added: single and multi objects can be shown on a map. +- Possibility to Show or Hide system objects was added to the context menu in the Navigator. +- All product information can be found in public settings. +- A lot of minor UI fixes and improvements. + ### CloudBeaver 20.0.5 - 2021-03-18 - New design for the connection form diff --git a/samples/sample-databases/cloudbeaver.conf b/samples/sample-databases/cloudbeaver.conf index 323bad43f6..cd1b803729 100644 --- a/samples/sample-databases/cloudbeaver.conf +++ b/samples/sample-databases/cloudbeaver.conf @@ -34,7 +34,6 @@ }, app: { anonymousAccessEnabled: true, - authenticationEnabled: true, anonymousUserRole: "user", supportsCustomConnections: false, diff --git a/server/bundles/io.cloudbeaver.connect.provider.sample/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.connect.provider.sample/META-INF/MANIFEST.MF index bb5b609afd..47a1d03d0c 100644 --- a/server/bundles/io.cloudbeaver.connect.provider.sample/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.connect.provider.sample/META-INF/MANIFEST.MF @@ -3,8 +3,8 @@ Bundle-ManifestVersion: 2 Bundle-Vendor: %Bundle-Vendor Bundle-Name: %Bundle-Name Bundle-SymbolicName: io.cloudbeaver.connect.provider.sample;singleton:=true -Bundle-Version: 1.0.24.qualifier -Bundle-Release-Date: 20210322 +Bundle-Version: 1.0.25.qualifier +Bundle-Release-Date: 20210405 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-ActivationPolicy: lazy Bundle-ClassPath: . diff --git a/server/bundles/io.cloudbeaver.connect.provider.sample/pom.xml b/server/bundles/io.cloudbeaver.connect.provider.sample/pom.xml index ad4d275461..419737adee 100644 --- a/server/bundles/io.cloudbeaver.connect.provider.sample/pom.xml +++ b/server/bundles/io.cloudbeaver.connect.provider.sample/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.connect.provider.sample - 1.0.24-SNAPSHOT + 1.0.25-SNAPSHOT eclipse-plugin 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 805213a842..329fbe68cc 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: %Bundle-Vendor Bundle-Name: %Bundle-Name Bundle-SymbolicName: io.cloudbeaver.product.ce;singleton:=true -Bundle-Version: 20.0.5.qualifier -Bundle-Release-Date: 20210322 +Bundle-Version: 20.0.6.qualifier +Bundle-Release-Date: 20210405 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 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 7ff2fe0dd4..1056772825 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 - 20.0.5-SNAPSHOT + 20.0.6-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 b5d38eb0be..75f0fdde64 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.21.qualifier -Bundle-Release-Date: 20210322 +Bundle-Version: 1.0.22.qualifier +Bundle-Release-Date: 20210405 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 5ce61b1cc1..45c0e036fa 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.21-SNAPSHOT + 1.0.22-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF index 2876e771fb..b8dd6b2d82 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: %Bundle-Vendor Bundle-Name: %Bundle-Name Bundle-SymbolicName: io.cloudbeaver.server;singleton:=true -Bundle-Version: 20.0.5.qualifier -Bundle-Release-Date: 20210322 +Bundle-Version: 20.0.6.qualifier +Bundle-Release-Date: 20210405 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-ActivationPolicy: lazy Bundle-Activator: io.cloudbeaver.server.WebPlatformActivator diff --git a/server/bundles/io.cloudbeaver.server/pom.xml b/server/bundles/io.cloudbeaver.server/pom.xml index 1c2775b202..2717504305 100644 --- a/server/bundles/io.cloudbeaver.server/pom.xml +++ b/server/bundles/io.cloudbeaver.server/pom.xml @@ -10,7 +10,7 @@ ../ io.cloudbeaver.server - 20.0.5-SNAPSHOT + 20.0.6-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 cb5ae28e38..5425bb9bca 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -88,9 +88,10 @@ type ProductInfo { type ServerConfig { name: String! version: String! + workspaceId: ID! anonymousAccessEnabled: Boolean - authenticationEnabled: Boolean + authenticationEnabled: Boolean @deprecated supportsCustomConnections: Boolean supportsConnectionBrowser: Boolean supportsWorkspaces: Boolean @@ -432,7 +433,7 @@ extend type Mutation { deleteConnection( id: ID! ): Boolean! - createConnectionFromTemplate( templateId: ID! ): ConnectionInfo! + createConnectionFromTemplate( templateId: ID!, connectionName: String ): ConnectionInfo! # Copies connection configuration from node copyConnectionFromNode( nodePath: String!, config: ConnectionConfig ): ConnectionInfo! diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWAuthProvider.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWAuthProvider.java index c015c0ffa0..2dc8b83ac5 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWAuthProvider.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWAuthProvider.java @@ -17,6 +17,7 @@ package io.cloudbeaver; import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.access.DBASession; @@ -28,10 +29,9 @@ import java.util.Map; public interface DBWAuthProvider { AUTH_SESSION openSession( - WebSession mainSession, - Map providerConfig, // Auth provider configuration (e.g. 3rd party auth server address) - Map userCredentials, // Saved user credentials (e.g. associated 3rd party provider user name or realm) - Map authParameters // Passed auth parameters (e.g. user name or password) + @NotNull WebSession mainSession, + @NotNull Map providerConfig, // Auth provider configuration (e.g. 3rd party auth server address) + @NotNull Map userCredentials // Saved user credentials (e.g. associated 3rd party provider user name or realm) ) throws DBException; void closeSession(AUTH_SESSION session) throws DBException; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWAuthProviderExternal.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWAuthProviderExternal.java index f781b2e3b0..286726eb75 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWAuthProviderExternal.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWAuthProviderExternal.java @@ -39,22 +39,27 @@ public interface DBWAuthProviderExternal extend * Returns new identifying credentials which can be used to find/create user in database */ @NotNull - Map readExternalCredentials( + Map authExternalUser( @NotNull DBRProgressMonitor monitor, @NotNull Map providerConfig, // Auth provider configuration (e.g. 3rd party auth server address) @NotNull Map authParameters // Passed auth parameters (e.g. user name or password) ) throws DBException; + /** + * Validates that external user may be associated with local user + * @param userCredentials credentials from authExternalUser + * @return new user ID. If activeUser is not null then it must be the same as activeUser ID. + */ @NotNull - WebUser registerNewUser( + String validateLocalAuth( @NotNull DBRProgressMonitor monitor, @NotNull DBWSecurityController securityController, @NotNull Map providerConfig, - @NotNull Map credentials, + @NotNull Map userCredentials, @Nullable WebUser activeUser) throws DBException; @Nullable - String getUserDisplayName( + DBWUserIdentity getUserIdentity( @NotNull DBRProgressMonitor monitor, @NotNull Map providerConfig, @NotNull Map credentials) throws DBException; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWSecurityController.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWSecurityController.java index 0c0f803238..e7babd2ae2 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWSecurityController.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWSecurityController.java @@ -70,6 +70,11 @@ public interface DBWSecurityController { */ Map getUserCredentials(String userId, WebAuthProviderDescriptor authProvider) throws DBCException; + /** + * Returns list of auth provider IDs associated with this user + */ + String[] getUserLinkedProviders(String userId) throws DBCException; + /////////////////////////////////////////// // Roles diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWUserIdentity.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWUserIdentity.java new file mode 100644 index 0000000000..b8c1d02217 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWUserIdentity.java @@ -0,0 +1,44 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2021 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; + +import org.jkiss.code.NotNull; + +/** + * User identity provided by auth provider. + */ +public class DBWUserIdentity { + @NotNull + private final String id; + @NotNull + private final String displayName; + + public DBWUserIdentity(@NotNull String id, @NotNull String displayName) { + this.id = id; + this.displayName = displayName; + } + + @NotNull + public String getId() { + return id; + } + + @NotNull + public String getDisplayName() { + return displayName; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebException.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebException.java index 725d05f9ba..16decbafb9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebException.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebException.java @@ -138,6 +138,18 @@ public class DBWebException extends DBException implements GraphQLError { } private static String makeMessage(String message, Throwable cause) { + if (CommonUtils.isEmpty(message)) { + if (cause != null) { + if (cause.getMessage() != null) { + return cause.getMessage(); + } + return cause.getClass().getName(); + } + return "Unknown internal error"; + } + if (CommonUtils.equalObjects(message, cause.getMessage())) { + return message; + } return message + ":\n" + cause.getMessage(); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java index a5b3defc36..fd2cfdc986 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/auth/provider/local/LocalAuthProvider.java @@ -18,7 +18,11 @@ package io.cloudbeaver.auth.provider.local; import io.cloudbeaver.DBWAuthProvider; import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.registry.WebAuthProviderDescriptor; import io.cloudbeaver.registry.WebAuthProviderPropertyEncryption; +import io.cloudbeaver.registry.WebServiceRegistry; +import io.cloudbeaver.server.CBApplication; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.utils.CommonUtils; import org.jkiss.utils.SecurityUtils; @@ -35,13 +39,20 @@ public class LocalAuthProvider implements DBWAuthProvider { public static final String CRED_PASSWORD = "password"; @Override - public LocalAuthSession openSession(WebSession mainSession, Map providerConfig, Map userCredentials, Map authParameters) throws DBException { - String userName = CommonUtils.toString(authParameters.get(CRED_USER), null); - String storedPasswordHash = CommonUtils.toString(userCredentials.get(CRED_PASSWORD), null); + public LocalAuthSession openSession(@NotNull WebSession mainSession, @NotNull Map providerConfig, @NotNull Map userCredentials) throws DBException { + String userName = CommonUtils.toString(userCredentials.get(CRED_USER), null); + + WebAuthProviderDescriptor authProvider = WebServiceRegistry.getInstance().getAuthProvider(PROVIDER_ID); + Map storedCredentials = CBApplication.getInstance().getSecurityController().getUserCredentials(userName, authProvider); + if (storedCredentials == null) { + throw new DBException("Invalid user name or password"); + } + + String storedPasswordHash = CommonUtils.toString(storedCredentials.get(CRED_PASSWORD), null); if (CommonUtils.isEmpty(storedPasswordHash)) { throw new DBException("User has no password (login restricted)"); } - String clientPassword = CommonUtils.toString(authParameters.get(CRED_PASSWORD), null); + String clientPassword = CommonUtils.toString(userCredentials.get(CRED_PASSWORD), null); if (CommonUtils.isEmpty(clientPassword)) { throw new DBException("No user password provided"); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebConnectionInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebConnectionInfo.java index fad24f06c3..9ffe17202d 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebConnectionInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebConnectionInfo.java @@ -297,6 +297,11 @@ public class WebConnectionInfo { this.savedNetworkCredentials = networkCredentials; } + public void clearSavedCredentials() { + this.savedAuthProperties = null; + this.savedNetworkCredentials = null; + } + @Property public Map getProviderProperties() { return dataSourceContainer.getConnectionConfiguration().getProviderProperties(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java index 8883fa2447..b65aae1202 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java @@ -24,7 +24,9 @@ import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings; import org.jkiss.dbeaver.registry.language.PlatformLanguageDescriptor; import org.jkiss.dbeaver.registry.language.PlatformLanguageRegistry; +import org.jkiss.dbeaver.runtime.DBWorkbench; import org.jkiss.dbeaver.utils.GeneralUtils; +import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; import java.util.ArrayList; @@ -52,6 +54,11 @@ public class WebServerConfig { return GeneralUtils.getProductVersion().toString(); } + @Property + public String getWorkspaceId() { + return DBWorkbench.getPlatform().getWorkspace().getWorkspaceId(); + } + @Property public boolean isAnonymousAccessEnabled() { return application.getAppConfiguration().isAnonymousAccessEnabled(); @@ -59,7 +66,8 @@ public class WebServerConfig { @Property public boolean isAuthenticationEnabled() { - return application.getAppConfiguration().isAuthenticationEnabled(); + String[] enabledAuthProviders = getEnabledAuthProviders(); + return enabledAuthProviders == null || !ArrayUtils.isEmpty(enabledAuthProviders); } @Property diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerMessage.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerMessage.java index 797a03651d..ddb27f97c9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerMessage.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerMessage.java @@ -18,6 +18,9 @@ package io.cloudbeaver.model; import io.cloudbeaver.server.CBConstants; +import java.io.PrintWriter; +import java.io.StringWriter; + /** * Web server message */ @@ -32,13 +35,24 @@ public class WebServerMessage { } private final MessageType type; - private long time; - private String message; + private final long time; + private final String message; + private final Throwable error; + public WebServerMessage(MessageType type, String message) { + this(type, message, null); + } + + public WebServerMessage(Throwable error) { + this(MessageType.ERROR, error.getMessage(), error); + } + + public WebServerMessage(MessageType type, String message, Throwable error) { this.type = type; this.time = System.currentTimeMillis(); this.message = message; + this.error = error; } public MessageType getType() { @@ -54,6 +68,11 @@ public class WebServerMessage { } public String getStackTrace() { + if (error != null) { + StringWriter buf = new StringWriter(); + error.printStackTrace(new PrintWriter(buf, true)); + return buf.toString(); + } return null; } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebAuthInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebAuthInfo.java index 33959d5617..0a20c9749d 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebAuthInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebAuthInfo.java @@ -17,13 +17,16 @@ package io.cloudbeaver.model.session; import io.cloudbeaver.DBWAuthProvider; +import io.cloudbeaver.DBWUserIdentity; import io.cloudbeaver.model.user.WebUser; import io.cloudbeaver.model.user.WebUserOriginInfo; import io.cloudbeaver.registry.WebAuthProviderDescriptor; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.access.DBASession; import java.time.OffsetDateTime; +import java.util.Map; /** * WebAuthInfo @@ -37,49 +40,59 @@ public class WebAuthInfo { private WebAuthProviderDescriptor authProvider; private DBASession authSession; private OffsetDateTime loginTime; + private DBWUserIdentity userIdentity; private String message; - public WebAuthInfo(WebSession session, WebUser user) { + private transient Map userCredentials; + + public WebAuthInfo( + @NotNull WebSession session, + @NotNull WebUser user, + @NotNull WebAuthProviderDescriptor authProvider, + @NotNull DBWUserIdentity userIdentity, + @NotNull DBASession authSession, + @NotNull OffsetDateTime loginTime) + { this.session = session; this.user = user; + this.authProvider = authProvider; + this.userIdentity = userIdentity; + this.authSession = authSession; + this.loginTime = loginTime; } public WebUser getUser() { return user; } + public DBWUserIdentity getUserIdentity() { + return userIdentity; + } + public String getUserId() { - return user.getUserId(); + return userIdentity.getId(); } public String getDisplayName() { - return user.getDisplayName(); + return userIdentity.getDisplayName(); } - public WebAuthProviderDescriptor getAuthProvider() { + public String getAuthProvider() { + return authProvider.getId(); + } + + public WebAuthProviderDescriptor getAuthProviderDescriptor() { return authProvider; } - public void setAuthProvider(WebAuthProviderDescriptor authProvider) { - this.authProvider = authProvider; - } - public DBASession getAuthSession() { return authSession; } - public void setAuthSession(DBASession authSession) { - this.authSession = authSession; - } - public OffsetDateTime getLoginTime() { return loginTime; } - public void setLoginTime(OffsetDateTime loginTime) { - this.loginTime = loginTime; - } - public String getMessage() { return message; } @@ -104,4 +117,13 @@ public class WebAuthInfo { } } } + + // Used to keep user credentials during server configuration + public Map getUserCredentials() { + return userCredentials; + } + + public void setUserCredentials(Map userCredentials) { + this.userCredentials = userCredentials; + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebSession.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebSession.java index 56f74fab8e..b64de35a17 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebSession.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/session/WebSession.java @@ -92,7 +92,7 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap private boolean cacheExpired; private final Map connections = new HashMap<>(); - private final List progressMessages = new ArrayList<>(); + private final List sessionMessages = new ArrayList<>(); private final Map asyncTasks = new HashMap<>(); private final Map attributes = new HashMap<>(); @@ -101,7 +101,7 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap private final List authTokens = new ArrayList<>(); private DBNModel navigatorModel; - private DBRProgressMonitor progressMonitor = new SessionProgressMonitor(); + private final DBRProgressMonitor progressMonitor = new SessionProgressMonitor(); private ProjectMetadata sessionProject; private final SessionContextImpl sessionAuthContext; @@ -202,9 +202,11 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap public void forceUserRefresh(WebUser user) { if (!CommonUtils.equalObjects(this.user, user)) { // User has changed. We need to reset all session attributes + clearAuthTokens(); try { resetSessionCache(); } catch (DBCException e) { + addSessionError(e); log.error(e); } } @@ -273,6 +275,7 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap try { this.refreshConnections(); } catch (Exception e) { + addSessionError(e); log.error("Error getting connection list", e); } } @@ -319,6 +322,7 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap .getSubjectConnectionAccess(new String[]{subjectId})) .map(DBWConnectionGrant::getConnectionId).collect(Collectors.toSet()); } catch (DBCException e) { + addSessionError(e); log.error("Error reading connection grants", e); return Collections.emptySet(); } @@ -373,6 +377,7 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap accessibleConnectionIds = readAccessibleConnectionIds(); } catch (Exception e) { + addSessionError(e); log.error("Error reading session permissions", e); } } @@ -394,10 +399,10 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap * Returns and clears progress messages */ @Association - public List getProgressMessages() { - synchronized (progressMessages) { - List copy = new ArrayList<>(progressMessages); - progressMessages.clear(); + public List getSessionMessages() { + synchronized (sessionMessages) { + List copy = new ArrayList<>(sessionMessages); + sessionMessages.clear(); return copy; } } @@ -416,10 +421,13 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap DBWSecurityController.getInstance().createSession(this); this.persisted = true; } else { - // Update record - DBWSecurityController.getInstance().updateSession(this); + if (!CBApplication.getInstance().isConfigurationMode()) { + // Update record + DBWSecurityController.getInstance().updateSession(this); + } } } catch (Exception e) { + addSessionError(e); log.error("Error persisting web session", e); } } @@ -544,7 +552,7 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap return true; } - public WebAsyncTaskInfo createAndRunAsyncTask(String taskName, WebAsyncTaskProcessor runnable) { + public WebAsyncTaskInfo createAndRunAsyncTask(String taskName, WebAsyncTaskProcessor runnable) { int taskId = TASK_ID.incrementAndGet(); WebAsyncTaskInfo asyncTask = getAsyncTask(String.valueOf(taskId), taskName, true); @@ -559,6 +567,7 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap asyncTask.setStatus("Finished"); asyncTask.setRunning(false); } catch (InvocationTargetException e) { + addSessionError(e.getTargetException()); asyncTask.setJobError(e.getTargetException()); } catch (InterruptedException e) { asyncTask.setJobError(e); @@ -573,19 +582,25 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap return asyncTask; } + public void addSessionError(Throwable exception) { + synchronized (sessionMessages) { + sessionMessages.add(new WebServerMessage(exception)); + } + } + public List readLog(Integer maxEntries, Boolean clearLog) { - synchronized (progressMessages) { + synchronized (sessionMessages) { List messages = new ArrayList<>(); int entryCount = CommonUtils.toInt(maxEntries); - if (entryCount == 0 || entryCount >= progressMessages.size()) { - messages.addAll(progressMessages); + if (entryCount == 0 || entryCount >= sessionMessages.size()) { + messages.addAll(sessionMessages); if (CommonUtils.toBoolean(clearLog)) { - progressMessages.clear(); + sessionMessages.clear(); } } else { - messages.addAll(progressMessages.subList(0, maxEntries)); + messages.addAll(sessionMessages.subList(0, maxEntries)); if (CommonUtils.toBoolean(clearLog)) { - progressMessages.removeAll(messages); + sessionMessages.removeAll(messages); } } return messages; @@ -635,7 +650,7 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap if (providerID != null) { for (WebAuthInfo ai : authTokens) { - if (ai.getAuthProvider().getId().equals(providerID)) { + if (ai.getAuthProvider().equals(providerID)) { return ai; } } @@ -660,7 +675,7 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap } - WebAuthInfo oldAuthInfo = getAuthInfo(authInfo.getAuthProvider().getId()); + WebAuthInfo oldAuthInfo = getAuthInfo(authInfo.getAuthProviderDescriptor().getId()); if (oldAuthInfo != null) { removeAuthInfo(oldAuthInfo); } @@ -744,6 +759,7 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap // credGson.fromJson(credGson.toJsonTree(authProperties), credentials.getClass()); // configuration.getAuthModel().saveCredentials(dataSourceContainer, configuration, credentials); } catch (DBException e) { + addSessionError(e); log.error(e); } return true; @@ -770,15 +786,15 @@ public class WebSession implements DBASession, DBAAuthCredentialsProvider, IAdap private class SessionProgressMonitor extends BaseProgressMonitor { @Override public void beginTask(String name, int totalWork) { - synchronized (progressMessages) { - progressMessages.add(new WebServerMessage(WebServerMessage.MessageType.INFO, name)); + synchronized (sessionMessages) { + sessionMessages.add(new WebServerMessage(WebServerMessage.MessageType.INFO, name)); } } @Override public void subTask(String name) { - synchronized (progressMessages) { - progressMessages.add(new WebServerMessage(WebServerMessage.MessageType.INFO, name)); + synchronized (sessionMessages) { + sessionMessages.add(new WebServerMessage(WebServerMessage.MessageType.INFO, name)); } } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java index 2a5b8720db..649850ced5 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBAppConfig.java @@ -33,7 +33,6 @@ public class CBAppConfig { public static final DataSourceNavigatorSettings DEFAULT_VIEW_SETTINGS = DataSourceNavigatorSettings.PRESET_FULL.getSettings(); private boolean anonymousAccessEnabled = true; - private boolean authenticationEnabled = true; private String anonymousUserRole = CBConstants.DEFAUL_APP_ANONYMOUS_ROLE_NAME; private String defaultUserRole = CBConstants.DEFAUL_APP_ANONYMOUS_ROLE_NAME; private boolean supportsCustomConnections = true; @@ -48,14 +47,6 @@ public class CBAppConfig { private DataSourceNavigatorSettings defaultNavigatorSettings = DEFAULT_VIEW_SETTINGS; private Map plugins = new LinkedHashMap<>(); - public boolean isAuthenticationEnabled() { - return authenticationEnabled; - } - - public void setAuthenticationEnabled(boolean authenticationEnabled) { - this.authenticationEnabled = authenticationEnabled; - } - public boolean isAnonymousAccessEnabled() { return anonymousAccessEnabled; } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index f09332d8dc..ce951b5766 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -23,6 +23,7 @@ import com.google.gson.stream.JsonWriter; import io.cloudbeaver.DBWConnectionGrant; import io.cloudbeaver.DBWSecurityController; import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.model.session.WebAuthInfo; import io.cloudbeaver.registry.WebServiceRegistry; import io.cloudbeaver.server.jetty.CBJettyServer; import io.cloudbeaver.service.DBWServiceInitializer; @@ -30,6 +31,7 @@ import org.eclipse.core.runtime.Platform; import org.eclipse.equinox.app.IApplicationContext; import org.eclipse.osgi.service.datalocation.Location; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.ModelPreferences; @@ -501,18 +503,19 @@ public class CBApplication extends BaseApplicationImpl { } public synchronized void finishConfiguration( - String newServerName, - String adminName, - String adminPassword, + @NotNull String newServerName, + @NotNull String adminName, + @Nullable String adminPassword, + @NotNull List authInfoList, long sessionExpireTime, - CBAppConfig appConfig) throws DBException + @NotNull CBAppConfig appConfig) throws DBException { if (!RECONFIGURATION_ALLOWED && !isConfigurationMode()) { throw new DBException("Application must be in configuration mode"); } if (isConfigurationMode()) { - database.finishConfiguration(adminName, adminPassword); + database.finishConfiguration(adminName, adminPassword, authInfoList); } // Save runtime configuration @@ -588,7 +591,6 @@ public class CBApplication extends BaseApplicationImpl { json.name("app"); json.beginObject(); JSONUtils.field(json, "anonymousAccessEnabled", appConfig.isAnonymousAccessEnabled()); - JSONUtils.field(json, "authenticationEnabled", appConfig.isAuthenticationEnabled()); JSONUtils.field(json, "supportsCustomConnections", appConfig.isSupportsCustomConnections()); JSONUtils.field(json, "publicCredentialsSaveEnabled", appConfig.isPublicCredentialsSaveEnabled()); JSONUtils.field(json, "adminCredentialsSaveEnabled", appConfig.isAdminCredentialsSaveEnabled()); @@ -616,6 +618,9 @@ public class CBApplication extends BaseApplicationImpl { json.endObject(); } + if (appConfig.getEnabledAuthProviders() != null) { + JSONUtils.serializeStringList(json, "enabledAuthProviders", Arrays.asList(appConfig.getEnabledAuthProviders())); + } if (!CommonUtils.isEmpty(appConfig.getPlugins())) { JSONUtils.serializeProperties(json, "plugins", appConfig.getPlugins()); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java index cd0ea89f96..76a2fba9df 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java @@ -57,6 +57,7 @@ public class CBConstants { public static final String DEFAULT_DRIVERS_LOCATION = DEFAULT_DEPLOY_LOCATION + "/drivers"; public static final String DEFAULT_WORKSPACE_LOCATION = DEFAULT_DEPLOY_LOCATION + "/workspace"; public static final String DEFAULT_PRODUCT_CONFIGURATION = "conf/product.conf"; + public static final String DEFAULT_ADMIN_NAME = "cbadmin"; // Default max idle time (10 minutes) public static final long MAX_SESSION_IDLE_TIME = 10 * 60 * 1000; @@ -69,4 +70,5 @@ public class CBConstants { public static final String VAR_HOST_DOCKER_INTERNAL = "host.docker.internal"; + } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBDatabase.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBDatabase.java index c3965404d3..fb97ebb251 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBDatabase.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBDatabase.java @@ -20,6 +20,7 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import io.cloudbeaver.DBWSecurityController; import io.cloudbeaver.auth.provider.local.LocalAuthProvider; +import io.cloudbeaver.model.session.WebAuthInfo; import io.cloudbeaver.model.user.WebRole; import io.cloudbeaver.model.user.WebUser; import io.cloudbeaver.registry.WebAuthProviderDescriptor; @@ -158,7 +159,7 @@ public class CBDatabase { checkDatabaseStructure(); } - void finishConfiguration(String adminName, String adminPassword) throws DBException { + void finishConfiguration(@NotNull String adminName, @Nullable String adminPassword, @NotNull List authInfoList) throws DBException { if (!application.isConfigurationMode()) { throw new DBException("Database is already configured"); } @@ -170,10 +171,17 @@ public class CBDatabase { application.getSecurityController().deleteUser(initialData.getAdminName()); } // Create new admin user - if (!CommonUtils.isEmpty(adminName) && !CommonUtils.isEmpty(adminPassword)) { - createAdminUser(adminName, adminPassword); - } else { - grantAdminPermissionsToUser(adminName); + createAdminUser(adminName, adminPassword); + + // Associate all auth credentials with admin user + for (WebAuthInfo ai : authInfoList) { + if (!ai.getAuthProvider().equals(LocalAuthProvider.PROVIDER_ID)) { + WebAuthProviderDescriptor authProvider = ai.getAuthProviderDescriptor(); + Map userCredentials = ai.getUserCredentials(); + if (!CommonUtils.isEmpty(userCredentials)) { + application.getSecurityController().setUserCredentials(adminName, authProvider, userCredentials); + } + } } } @@ -326,7 +334,7 @@ public class CBDatabase { } @NotNull - private WebUser createAdminUser(String adminName, String adminPassword) throws DBCException { + private WebUser createAdminUser(@NotNull String adminName, @Nullable String adminPassword) throws DBCException { DBWSecurityController serverController = application.getSecurityController(); WebUser adminUser = serverController.getUserById(adminName); if (adminUser == null) { @@ -334,16 +342,18 @@ public class CBDatabase { serverController.createUser(adminUser); } - // This is how client password will be transmitted from client - String clientPassword = LocalAuthProvider.makeClientPasswordHash(adminUser.getUserId(), adminPassword); + if (!CommonUtils.isEmpty(adminPassword)) { + // This is how client password will be transmitted from client + String clientPassword = LocalAuthProvider.makeClientPasswordHash(adminUser.getUserId(), adminPassword); - Map credentials = new LinkedHashMap<>(); - credentials.put(LocalAuthProvider.CRED_USER, adminUser.getUserId()); - credentials.put(LocalAuthProvider.CRED_PASSWORD, clientPassword); + Map credentials = new LinkedHashMap<>(); + credentials.put(LocalAuthProvider.CRED_USER, adminUser.getUserId()); + credentials.put(LocalAuthProvider.CRED_PASSWORD, clientPassword); - WebAuthProviderDescriptor authProvider = WebServiceRegistry.getInstance().getAuthProvider(LocalAuthProvider.PROVIDER_ID); - if (authProvider != null) { - serverController.setUserCredentials(adminUser.getUserId(), authProvider, credentials); + WebAuthProviderDescriptor authProvider = WebServiceRegistry.getInstance().getAuthProvider(LocalAuthProvider.PROVIDER_ID); + if (authProvider != null) { + serverController.setUserCredentials(adminUser.getUserId(), authProvider, credentials); + } } grantAdminPermissionsToUser(adminUser.getUserId()); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBSecurityController.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBSecurityController.java index 39741aae0c..1d38f60dbd 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBSecurityController.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBSecurityController.java @@ -373,6 +373,29 @@ class CBSecurityController implements DBWSecurityController { } } + @Override + public String[] getUserLinkedProviders(String userId) throws DBCException { + try (Connection dbCon = database.openConnection()) { + try (PreparedStatement dbStat = dbCon.prepareStatement( + "SELECT DISTINCT PROVIDER_ID FROM CB_USER_CREDENTIALS\n" + + "WHERE USER_ID=?")) { + dbStat.setString(1, userId); + + try (ResultSet dbResult = dbStat.executeQuery()) { + List providerIds = new ArrayList<>(); + + while (dbResult.next()) { + providerIds.add(dbResult.getString(1)); + } + + return providerIds.toArray(new String[0]); + } + } + } catch (SQLException e) { + throw new DBCException("Error saving role in database", e); + } + } + /////////////////////////////////////////// // Roles diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBWorkspace.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBWorkspace.java index 321c6f40e4..ea2464c668 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBWorkspace.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBWorkspace.java @@ -28,14 +28,17 @@ import org.jkiss.dbeaver.registry.BaseWorkspaceImpl; */ public class CBWorkspace extends BaseWorkspaceImpl { + private final String workspaceId; + public CBWorkspace(DBPPlatform platform, IWorkspace eclipseWorkspace) { super(platform, eclipseWorkspace); + workspaceId = readWorkspaceId(); } @NotNull @Override public String getWorkspaceId() { - return "web-workspace"; + return workspaceId; } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLEndpoint.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLEndpoint.java index 52e3ab602e..ef85af5748 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLEndpoint.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLEndpoint.java @@ -33,9 +33,11 @@ import graphql.schema.idl.SchemaParser; import graphql.schema.idl.TypeDefinitionRegistry; import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.registry.WebServiceRegistry; import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.service.DBWServiceBindingGraphQL; +import io.cloudbeaver.service.WebServiceBindingBase; import org.jkiss.dbeaver.Log; import org.jkiss.utils.IOUtils; @@ -256,7 +258,7 @@ public class GraphQLEndpoint extends HttpServlet { response.getWriter().print(resString); } - private class WebInstrumentation extends SimpleInstrumentation { + private static class WebInstrumentation extends SimpleInstrumentation { @Override public CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { return super.instrumentExecutionResult(executionResult, parameters); @@ -285,6 +287,12 @@ public class GraphQLEndpoint extends HttpServlet { } log.debug("GraphQL call failed at '" + handlerParameters.getPath() + "', " + handlerParameters.getArgumentValues(), exception); + // Log in session + WebSession webSession = WebServiceBindingBase.findWebSession(handlerParameters.getDataFetchingEnvironment()); + if (webSession != null) { + webSession.addSessionError(exception); + } + SourceLocation sourceLocation = handlerParameters.getSourceLocation(); ExecutionPath path = handlerParameters.getPath(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java index 547a746ab1..62f8d96611 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/WebServiceBindingBase.java @@ -26,6 +26,7 @@ import io.cloudbeaver.server.CBApplication; import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.server.graphql.GraphQLEndpoint; import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; @@ -45,6 +46,8 @@ import java.util.Set; */ public abstract class WebServiceBindingBase implements DBWServiceBindingGraphQL { + private static final Log log = Log.getLog(WebServiceBindingBase.class); + private final Class apiInterface; private final API_TYPE serviceImpl; private final String schemaFileName; @@ -103,16 +106,16 @@ public abstract class WebServiceBindingBase impleme getServletRequest(env), errorOnNotFound); } - protected static WebSession findWebSession(DataFetchingEnvironment env) { - return CBPlatform.getInstance().getSessionManager().findWebSession( - getServletRequest(env)); - } - @NotNull protected static WebConnectionInfo getWebConnection(DataFetchingEnvironment env) throws DBWebException { return getWebConnection(getWebSession(env), env.getArgument("connectionId")); } + public static WebSession findWebSession(DataFetchingEnvironment env) { + return CBPlatform.getInstance().getSessionManager().findWebSession( + getServletRequest(env)); + } + @NotNull public static WebConnectionInfo getWebConnection(WebSession session, String connectionId) throws DBWebException { return session.getWebConnectionInfo(connectionId); @@ -132,7 +135,7 @@ public abstract class WebServiceBindingBase impleme try { WebAction webAction = method.getAnnotation(WebAction.class); if (webAction != null) { - checkPermissions(webAction); + checkPermissions(method, webAction); } return method.invoke(impl, args); } catch (InvocationTargetException e) { @@ -140,7 +143,7 @@ public abstract class WebServiceBindingBase impleme } } - private void checkPermissions(WebAction webAction) throws DBWebException { + private void checkPermissions(@NotNull Method method, @NotNull WebAction webAction) throws DBWebException { String[] reqPermissions = webAction.requirePermissions(); if (reqPermissions.length == 0) { return; @@ -153,6 +156,7 @@ public abstract class WebServiceBindingBase impleme if (!application.isConfigurationMode()) { Set sessionPermissions = session.getSessionPermissions(); if (CommonUtils.isEmpty(sessionPermissions)) { + log.debug("Anonymous access to " + method.getName() + " restricted"); throw new DBWebExceptionAccessDenied("Anonymous access restricted"); } @@ -166,6 +170,7 @@ public abstract class WebServiceBindingBase impleme // Check permissions for (String rp : reqPermissions) { if (!sessionPermissions.contains(rp)) { + log.debug("Access to " + method.getName() + " denied for " + session.getUser()); throw new DBWebExceptionAccessDenied("Access denied"); } } 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 f3469a82e0..a355201a6e 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 @@ -115,21 +115,25 @@ public interface DBWServiceCore extends DBWService { @NotNull WebConnectionConfig connectionConfig) throws DBWebException; @WebAction - boolean deleteConnection(WebSession webSession, String connectionId) throws DBWebException; + boolean deleteConnection(@NotNull WebSession webSession, @NotNull String connectionId) throws DBWebException; @WebAction - WebConnectionInfo createConnectionFromTemplate(WebSession webSession, String templateId) throws DBWebException; + WebConnectionInfo createConnectionFromTemplate( + @NotNull WebSession webSession, + @NotNull String templateId, + @Nullable String connectionName) throws DBWebException; + @WebAction() WebConnectionInfo copyConnectionFromNode(@NotNull WebSession webSession, @NotNull String nodePath, @NotNull WebConnectionConfig config) throws DBWebException; @WebAction - WebConnectionInfo testConnection(WebSession webSession, WebConnectionConfig connectionConfig) throws DBWebException; + WebConnectionInfo testConnection(@NotNull WebSession webSession, @NotNull WebConnectionConfig connectionConfig) throws DBWebException; @WebAction - WebNetworkEndpointInfo testNetworkHandler(WebSession webSession, WebNetworkHandlerConfigInput nhConfig) throws DBWebException; + WebNetworkEndpointInfo testNetworkHandler(@NotNull WebSession webSession, @NotNull WebNetworkHandlerConfigInput nhConfig) throws DBWebException; @WebAction - WebConnectionInfo closeConnection(WebSession webSession, String connectionId) throws DBWebException; + WebConnectionInfo closeConnection(@NotNull WebSession webSession, @NotNull String connectionId) throws DBWebException; /////////////////////////////////////////// // Navigator settings 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 39a01f4ad0..479ec91de5 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 @@ -72,7 +72,7 @@ public class WebServiceBindingCore extends WebServiceBindingBase return Collections.emptyList(); } return getService(env).readSessionLog( - getWebSession(env), + session, env.getArgument("maxEntries"), env.getArgument("clearEntries")); }) @@ -89,7 +89,10 @@ public class WebServiceBindingCore extends WebServiceBindingBase .dataFetcher("createConnection", env -> getService(env).createConnection(getWebSession(env), getConnectionConfig(env))) .dataFetcher("updateConnection", env -> getService(env).updateConnection(getWebSession(env), getConnectionConfig(env))) .dataFetcher("deleteConnection", env -> getService(env).deleteConnection(getWebSession(env), env.getArgument("id"))) - .dataFetcher("createConnectionFromTemplate", env -> getService(env).createConnectionFromTemplate(getWebSession(env), env.getArgument("templateId"))) + .dataFetcher("createConnectionFromTemplate", env -> getService(env).createConnectionFromTemplate( + getWebSession(env), + env.getArgument("templateId"), + env.getArgument("connectionName"))) .dataFetcher("copyConnectionFromNode", env -> getService(env).copyConnectionFromNode( getWebSession(env), env.getArgument("nodePath"), 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 d9610d7cb4..6ee0eb2a53 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 @@ -260,8 +260,6 @@ public class WebServiceCore implements DBWServiceCore { if (dataSourceContainer.isConnected()) { throw new DBWebException("Datasource '" + dataSourceContainer.getName() + "' is already connected"); } -// -// WebServiceUtils.initAuthProperties(dataSourceContainer, authProperties); boolean oldSavePassword = dataSourceContainer.isSavePassword(); try { @@ -270,7 +268,7 @@ public class WebServiceCore implements DBWServiceCore { throw new DBWebException("Error connecting to database", e); } finally { dataSourceContainer.setSavePassword(oldSavePassword); - connectionInfo.setSavedCredentials(null, null); + connectionInfo.clearSavedCredentials(); } // Mark all specified network configs as saved if (networkCredentials != null) { @@ -288,8 +286,13 @@ public class WebServiceCore implements DBWServiceCore { } if (saveCredentials != null && saveCredentials) { // Save all passed credentials in the datasource container + WebServiceUtils.saveAuthProperties( + dataSourceContainer, + dataSourceContainer.getConnectionConfiguration(), + authProperties, + true); + WebServiceUtils.saveCredentialsInDataSource(connectionInfo, dataSourceContainer, dataSourceContainer.getConnectionConfiguration()); - dataSourceContainer.setSavePassword(true); dataSourceContainer.persistConfiguration(); } @@ -341,7 +344,7 @@ public class WebServiceCore implements DBWServiceCore { } @Override - public boolean deleteConnection(WebSession webSession, String connectionId) throws DBWebException { + public boolean deleteConnection(@NotNull WebSession webSession, @NotNull String connectionId) throws DBWebException { if (!CBApplication.getInstance().getAppConfiguration().isSupportsCustomConnections()) { throw new DBWebException("Connection delete is restricted by server configuration"); } @@ -350,7 +353,11 @@ public class WebServiceCore implements DBWServiceCore { } @Override - public WebConnectionInfo createConnectionFromTemplate(WebSession webSession, String templateId) throws DBWebException { + public WebConnectionInfo createConnectionFromTemplate( + @NotNull WebSession webSession, + @NotNull String templateId, + @Nullable String connectionName) throws DBWebException + { DBPDataSourceRegistry templateRegistry = WebServiceUtils.getGlobalDataSourceRegistry(); DBPDataSourceContainer dataSourceTemplate = templateRegistry.getDataSource(templateId); if (dataSourceTemplate == null) { @@ -361,6 +368,9 @@ public class WebServiceCore implements DBWServiceCore { DBPDataSourceContainer newDataSource = sessionRegistry.createDataSource(dataSourceTemplate); ((DataSourceDescriptor) newDataSource).setNavigatorSettings(CBApplication.getInstance().getAppConfiguration().getDefaultNavigatorSettings()); + if (!CommonUtils.isEmpty(connectionName)) { + newDataSource.setName(connectionName); + } sessionRegistry.addDataSource(newDataSource); WebConnectionInfo connectionInfo = new WebConnectionInfo(webSession, newDataSource); @@ -405,7 +415,7 @@ public class WebServiceCore implements DBWServiceCore { } @Override - public WebConnectionInfo testConnection(WebSession webSession, WebConnectionConfig connectionConfig) throws DBWebException { + public WebConnectionInfo testConnection(@NotNull WebSession webSession, @NotNull WebConnectionConfig connectionConfig) throws DBWebException { String connectionId = connectionConfig.getConnectionId(); connectionConfig.setSaveCredentials(true); // It is used in createConnectionFromConfig @@ -441,7 +451,7 @@ public class WebServiceCore implements DBWServiceCore { } @Override - public WebNetworkEndpointInfo testNetworkHandler(WebSession webSession, WebNetworkHandlerConfigInput nhConfig) throws DBWebException { + public WebNetworkEndpointInfo testNetworkHandler(@NotNull WebSession webSession, @NotNull WebNetworkHandlerConfigInput nhConfig) throws DBWebException { DBRProgressMonitor monitor = webSession.getProgressMonitor(); monitor.beginTask("Instantiate SSH tunnel", 2); @@ -494,7 +504,7 @@ public class WebServiceCore implements DBWServiceCore { } @Override - public WebConnectionInfo closeConnection(WebSession webSession, String connectionId) throws DBWebException { + public WebConnectionInfo closeConnection(@NotNull WebSession webSession, @NotNull String connectionId) throws DBWebException { return closeAndDeleteConnection(webSession, connectionId, false); } @@ -519,7 +529,7 @@ public class WebServiceCore implements DBWServiceCore { webSession.removeConnection(connectionInfo); } else { // Just reset saved credentials - connectionInfo.setSavedCredentials(null, null); + connectionInfo.clearSavedCredentials(); } return connectionInfo; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java index 5914ced9c1..81df579789 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java @@ -25,6 +25,7 @@ import org.jkiss.dbeaver.model.sql.SQLSyntaxManager; import org.jkiss.dbeaver.model.sql.completion.SQLCompletionContext; import org.jkiss.dbeaver.model.sql.completion.SQLCompletionProposalBase; import org.jkiss.dbeaver.model.sql.completion.SQLCompletionRequest; +import org.jkiss.dbeaver.model.sql.parser.SQLRuleManager; import java.util.Map; @@ -58,6 +59,11 @@ public class WebSQLCompletionContext implements SQLCompletionContext { return sqlContext.getProcessor().getSyntaxManager(); } + @Override + public SQLRuleManager getRuleManager() { + return sqlContext.getProcessor().getRuleManager(); + } + @Override public boolean isUseFQNames() { return false; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDialectInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDialectInfo.java index 1ad7e7e6a7..18696a03d2 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDialectInfo.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLDialectInfo.java @@ -19,6 +19,7 @@ package io.cloudbeaver.service.sql; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.DBPDataSource; import org.jkiss.dbeaver.model.sql.SQLDialect; +import org.jkiss.dbeaver.model.sql.SQLUtils; import org.jkiss.utils.Pair; /** @@ -77,7 +78,7 @@ public class WebSQLDialectInfo { } public String getScriptDelimiter() { - return dialect.getScriptDelimiter(); + return SQLUtils.getDefaultScriptDelimiter(dialect); } } 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 029aff7f51..25fbfee929 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 @@ -37,6 +37,7 @@ import org.jkiss.dbeaver.model.navigator.DBNNode; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; import org.jkiss.dbeaver.model.sql.SQLQuery; import org.jkiss.dbeaver.model.sql.SQLSyntaxManager; +import org.jkiss.dbeaver.model.sql.parser.SQLRuleManager; import org.jkiss.dbeaver.model.struct.*; import org.jkiss.utils.CommonUtils; @@ -56,6 +57,7 @@ public class WebSQLProcessor { private final WebSession webSession; private final WebConnectionInfo connection; private final SQLSyntaxManager syntaxManager; + private final SQLRuleManager ruleManager; private final Map contexts = new LinkedHashMap<>(); private AtomicInteger contextId = new AtomicInteger(); @@ -66,6 +68,9 @@ public class WebSQLProcessor { syntaxManager = new SQLSyntaxManager(); syntaxManager.init(connection.getDataSource()); + + ruleManager = new SQLRuleManager(syntaxManager); + ruleManager.loadRules(connection.getDataSource(), false); } void dispose() { @@ -87,6 +92,10 @@ public class WebSQLProcessor { return syntaxManager; } + SQLRuleManager getRuleManager() { + return ruleManager; + } + DBCExecutionContext getExecutionContext() { return DBUtils.getDefaultContext(connection.getDataSource(), false); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java index e78e2e7d23..bcf7abc17e 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLUtils.java @@ -23,6 +23,8 @@ import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.data.*; import org.jkiss.dbeaver.model.exec.DBCException; import org.jkiss.dbeaver.model.gis.DBGeometry; +import org.jkiss.dbeaver.model.gis.GisConstants; +import org.jkiss.dbeaver.model.gis.GisTransformUtils; import org.jkiss.dbeaver.model.struct.DBSAttributeBase; import org.jkiss.dbeaver.model.struct.DBSTypedObject; import org.jkiss.dbeaver.utils.ContentUtils; @@ -143,6 +145,11 @@ public class WebSQLUtils { map.put("srid", value.getSRID()); map.put("text", value.toString()); map.put("properties", value.getProperties()); + + DBGeometry xValue = GisTransformUtils.transformToSRID(value, GisConstants.SRID_4326); + if (xValue != null && xValue != value) { + map.put("mapText", xValue.toString()); + } return map; } 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 2e032529fd..463323d472 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: %Bundle-Vendor Bundle-Name: %Bundle-Name Bundle-SymbolicName: io.cloudbeaver.service.admin;singleton:=true -Bundle-Version: 1.0.20.qualifier -Bundle-Release-Date: 20210322 +Bundle-Version: 1.0.21.qualifier +Bundle-Release-Date: 20210405 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 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 0b698c3de0..66fc3d2657 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.20-SNAPSHOT + 1.0.21-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls index 79107e1416..c3c02bbe76 100644 --- a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls +++ b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls @@ -57,7 +57,7 @@ input ServerConfigInput { publicCredentialsSaveEnabled: Boolean adminCredentialsSaveEnabled: Boolean - enabledAuthProviders: [ID!]! + enabledAuthProviders: [ID!] sessionExpireTime: Int } diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java index fb77d3d77a..db231e48ce 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/AdminServerConfig.java @@ -35,7 +35,6 @@ public class AdminServerConfig { private String adminPassword; private boolean anonymousAccessEnabled; - private boolean authenticationEnabled; private boolean customConnectionsEnabled; private boolean publicCredentialsSaveEnabled; private boolean adminCredentialsSaveEnabled; @@ -50,7 +49,6 @@ public class AdminServerConfig { CBAppConfig appConfig = CBApplication.getInstance().getAppConfiguration(); this.anonymousAccessEnabled = JSONUtils.getBoolean(params, "anonymousAccessEnabled", appConfig.isAnonymousAccessEnabled()); - this.authenticationEnabled = JSONUtils.getBoolean(params, "authenticationEnabled", appConfig.isAuthenticationEnabled()); this.customConnectionsEnabled = JSONUtils.getBoolean(params, "customConnectionsEnabled", appConfig.isSupportsCustomConnections()); this.publicCredentialsSaveEnabled = JSONUtils.getBoolean(params, "publicCredentialsSaveEnabled", appConfig.isPublicCredentialsSaveEnabled()); this.adminCredentialsSaveEnabled = JSONUtils.getBoolean(params, "adminCredentialsSaveEnabled", appConfig.isAdminCredentialsSaveEnabled()); @@ -91,10 +89,6 @@ public class AdminServerConfig { return anonymousAccessEnabled; } - public boolean isAuthenticationEnabled() { - return authenticationEnabled; - } - public boolean isCustomConnectionsEnabled() { return customConnectionsEnabled; } 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 b23dda825c..8e1b10f27c 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 @@ -22,6 +22,7 @@ import io.cloudbeaver.WebServiceUtils; import io.cloudbeaver.auth.provider.local.LocalAuthProvider; import io.cloudbeaver.model.WebConnectionConfig; import io.cloudbeaver.model.WebConnectionInfo; +import io.cloudbeaver.model.session.WebAuthInfo; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.user.WebRole; import io.cloudbeaver.model.user.WebUser; @@ -31,6 +32,7 @@ import io.cloudbeaver.registry.WebServiceDescriptor; import io.cloudbeaver.registry.WebServiceRegistry; import io.cloudbeaver.server.CBAppConfig; import io.cloudbeaver.server.CBApplication; +import io.cloudbeaver.server.CBConstants; import io.cloudbeaver.server.CBPlatform; import io.cloudbeaver.service.DBWServiceServerConfigurator; import io.cloudbeaver.service.admin.*; @@ -352,7 +354,6 @@ public class WebServiceAdmin implements DBWServiceAdmin { try { CBAppConfig appConfig = new CBAppConfig(); appConfig.setAnonymousAccessEnabled(config.isAnonymousAccessEnabled()); - appConfig.setAuthenticationEnabled(config.isAuthenticationEnabled()); appConfig.setSupportsCustomConnections(config.isCustomConnectionsEnabled()); appConfig.setPublicCredentialsSaveEnabled(config.isPublicCredentialsSaveEnabled()); appConfig.setAdminCredentialsSaveEnabled(config.isAdminCredentialsSaveEnabled()); @@ -366,6 +367,8 @@ public class WebServiceAdmin implements DBWServiceAdmin { appConfig.setDefaultNavigatorSettings( CBApplication.getInstance().getAppConfiguration().getDefaultNavigatorSettings()); + List authInfoList = webSession.getAllAuthInfo(); + String adminName = config.getAdminName(); String adminPassword = config.getAdminPassword(); if (CommonUtils.isEmpty(adminName)) { @@ -374,6 +377,15 @@ public class WebServiceAdmin implements DBWServiceAdmin { adminName = curUser == null ? null : curUser.getUserId(); adminPassword = null; } + if (CommonUtils.isEmpty(adminName)) { + // Try to get admin name from existing authentications (first one) + if (!authInfoList.isEmpty()) { + adminName = authInfoList.get(0).getUserId(); + } + } + if (CommonUtils.isEmpty(adminName)) { + adminName = CBConstants.DEFAULT_ADMIN_NAME; + } // Patch configuration by services for (DBWServiceServerConfigurator wsc : WebServiceRegistry.getInstance().getWebServices(DBWServiceServerConfigurator.class)) { @@ -388,6 +400,7 @@ public class WebServiceAdmin implements DBWServiceAdmin { config.getServerName(), adminName, adminPassword, + authInfoList, config.getSessionExpireTime(), appConfig); 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 82ca4ac4cd..fbd4ce883f 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: %Bundle-Vendor Bundle-Name: %Bundle-Name Bundle-SymbolicName: io.cloudbeaver.service.auth;singleton:=true -Bundle-Version: 1.0.20.qualifier -Bundle-Release-Date: 20210322 +Bundle-Version: 1.0.21.qualifier +Bundle-Release-Date: 20210405 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 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 82e2c67f69..ab13094b68 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.20-SNAPSHOT + 1.0.21-SNAPSHOT eclipse-plugin diff --git a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls index 5f0eacf6b2..68f4637954 100644 --- a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls +++ b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls @@ -37,6 +37,12 @@ type UserAuthToken { # Authorization time loginTime: DateTime! + # User identity (aka user name) specific to auth provider + userId: String! + + # User display name specific to auth provider + displayName: String! + # Optional login message message: String @@ -44,43 +50,29 @@ type UserAuthToken { origin: ObjectOrigin! } -type UserAuthInfo { - # User unique identifier - userId: String! @deprecated - # Human readable display name. May be null - displayName: String @deprecated - # Auth provider ID - authProvider: String! @deprecated - - loginTime: DateTime! @deprecated - - # Optional login message - message: String @deprecated - - origin: ObjectOrigin! @deprecated -} - type UserInfo { # User unique identifier userId: String! - # Human readable display name. May be null + + # Human readable display name. It is taken from the first auth provider which was used for user login. displayName: String authTokens: [UserAuthToken!]! + + linkedAuthProviders: [String!]! } extend type Query { - # Authorize user using specified auth provider + # Authorize user using specified auth provider. If linkUser=true then associates new + authLogin(provider: ID!, credentials: Object!, linkUser: Boolean): UserAuthToken! - authLogin(provider: ID!, credentials: Object!): UserAuthInfo! # Logouts user. If provider not specified then all authorizations are revoked from session. authLogout(provider: ID): Boolean + # Active user information. null is no user was authorized within session activeUser: UserInfo - sessionUser: UserAuthInfo @deprecated - authProviders: [AuthProviderInfo!]! } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java index 2648e3b9cc..f2131c58f4 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java @@ -33,7 +33,11 @@ import java.util.Map; public interface DBWServiceAuth extends DBWService { @WebAction(requirePermissions = {} ) - WebAuthInfo authLogin(@NotNull WebSession webSession, @NotNull String providerId, @NotNull Map credentials) throws DBWebException; + WebAuthInfo authLogin( + @NotNull WebSession webSession, + @NotNull String providerId, + @NotNull Map credentials, + boolean linkWithActiveUser) throws DBWebException; @WebAction(requirePermissions = {} ) void authLogout(@NotNull WebSession webSession, @Nullable String providerId) throws DBWebException; @@ -41,11 +45,7 @@ public interface DBWServiceAuth extends DBWService { @WebAction(requirePermissions = {}) WebUserInfo activeUser(@NotNull WebSession webSession) throws DBWebException; - @Deprecated @WebAction(requirePermissions = {}) - WebAuthInfo sessionUser(@NotNull WebSession webSession) throws DBWebException; - - @WebAction(requirePermissions = {}) - WebAuthProviderInfo[] getAuthProviders(@NotNull WebSession webSession); + WebAuthProviderInfo[] getAuthProviders(); } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java index 87a3ba7e9b..4a98cd5dd7 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java @@ -20,6 +20,7 @@ import io.cloudbeaver.DBWebException; import io.cloudbeaver.service.DBWBindingContext; import io.cloudbeaver.service.WebServiceBindingBase; import io.cloudbeaver.service.auth.impl.WebServiceAuthImpl; +import org.jkiss.utils.CommonUtils; /** * Web service implementation @@ -38,14 +39,14 @@ public class WebServiceBindingAuth extends WebServiceBindingBase .dataFetcher("authLogin", env -> getService(env).authLogin( getWebSession(env, false), env.getArgument("provider"), - env.getArgument("credentials"))) + env.getArgument("credentials"), + CommonUtils.toBoolean(env.getArgument("linkUser")))) .dataFetcher("authLogout", env -> { getService(env).authLogout(getWebSession(env), env.getArgument("provider")); return true; }) .dataFetcher("activeUser", env -> getService(env).activeUser(getWebSession(env, false))) - .dataFetcher("sessionUser", env -> getService(env).sessionUser(getWebSession(env, false))) - .dataFetcher("authProviders", env -> getService(env).getAuthProviders(getWebSession(env))) + .dataFetcher("authProviders", env -> getService(env).getAuthProviders()) ; } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserAuthToken.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserAuthToken.java new file mode 100644 index 0000000000..90cd7d0873 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserAuthToken.java @@ -0,0 +1,62 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2021 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.service.auth; + +import io.cloudbeaver.model.session.WebAuthInfo; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.model.user.WebUser; +import io.cloudbeaver.model.user.WebUserOriginInfo; +import org.jkiss.code.NotNull; + +import java.time.OffsetDateTime; + +/** + * WebUserInfo + */ +public class WebUserAuthToken { + + private final WebSession session; + private final WebUser user; + private final WebAuthInfo authInfo; + + WebUserAuthToken(@NotNull WebSession session, @NotNull WebUser user, @NotNull WebAuthInfo authInfo) { + this.session = session; + this.user = user; + this.authInfo = authInfo; + } + + @NotNull + public String getAuthProvider() { + return authInfo.getAuthProviderDescriptor().getId(); + } + + @NotNull + public OffsetDateTime getLoginTime() { + return authInfo.getLoginTime(); + } + + @NotNull + public String getMessage() { + return authInfo.getMessage(); + } + + @NotNull + public WebUserOriginInfo getOrigin() { + return new WebUserOriginInfo(session, user, authInfo.getAuthProviderDescriptor()); + } + +} diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java index 53aec5fc71..3aaf40c243 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebUserInfo.java @@ -16,12 +16,17 @@ */ package io.cloudbeaver.service.auth; -import io.cloudbeaver.model.session.WebAuthInfo; +import io.cloudbeaver.DBWebException; import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.model.user.WebUser; +import io.cloudbeaver.server.CBApplication; import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.exec.DBCException; +import org.jkiss.dbeaver.model.meta.Property; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; /** * WebUserInfo @@ -32,22 +37,41 @@ public class WebUserInfo { private final WebSession session; private final WebUser user; + private String[] linkedProviders; public WebUserInfo(WebSession session, WebUser user) { this.session = session; this.user = user; } + @Property public String getUserId() { return user.getUserId(); } + @Property public String getDisplayName() { return user.getDisplayName(); } - public List getAuthTokens() { - return session.getAllAuthInfo(); + @Property + public List getAuthTokens() { + return session.getAllAuthInfo().stream() + .map(ai -> new WebUserAuthToken(session, user, ai)) + .collect(Collectors.toList()); + } + + @Property + public List getLinkedAuthProviders() throws DBWebException { + if (linkedProviders == null) { + try { + linkedProviders = CBApplication.getInstance().getSecurityController() + .getUserLinkedProviders(session.getUser().getUserId()); + } catch (DBCException e) { + throw new DBWebException("Error reading user linked providers"); + } + } + return Arrays.asList(linkedProviders); } } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java index 18e315e983..025ac157ff 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java @@ -33,6 +33,7 @@ import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.access.DBASession; import org.jkiss.dbeaver.model.exec.DBCException; +import org.jkiss.utils.ArrayUtils; import org.jkiss.utils.CommonUtils; import java.time.OffsetDateTime; @@ -47,12 +48,35 @@ public class WebServiceAuthImpl implements DBWServiceAuth { private static final Log log = Log.getLog(WebServiceAuthImpl.class); @Override - public WebAuthInfo authLogin(@NotNull WebSession webSession, @NotNull String providerId, @NotNull Map authParameters) throws DBWebException { - DBWSecurityController serverController = CBPlatform.getInstance().getApplication().getSecurityController(); + public WebAuthInfo authLogin( + @NotNull WebSession webSession, + @NotNull String providerId, + @NotNull Map authParameters, + boolean linkWithActiveUser) throws DBWebException { + DBWSecurityController securityController = CBPlatform.getInstance().getApplication().getSecurityController(); if (CommonUtils.isEmpty(providerId)) { throw new DBWebException("Missing auth provider parameter"); } + boolean configMode = CBApplication.getInstance().isConfigurationMode(); + + // Check enabled auth providers + boolean providerEnabled = true; + String[] enabledAuthProviders = CBApplication.getInstance().getAppConfiguration().getEnabledAuthProviders(); + if (enabledAuthProviders != null && !ArrayUtils.contains(enabledAuthProviders, providerId)) { + providerEnabled = false; + } + if (configMode || webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)) { + // 1. Admin can authorize in any providers + // 2. When it authorizes in non-local provider for the first time we force linkUser flag + if (!providerEnabled && webSession.getUser() != null) { + linkWithActiveUser = true; + } + } else { + if (!providerEnabled) { + throw new DBWebException("Authentication provider '" + providerId + "' is disabled"); + } + } WebAuthProviderDescriptor authProvider = WebServiceRegistry.getInstance().getAuthProvider(providerId); if (authProvider == null) { throw new DBWebException("Invalid auth provider '" + providerId + "'"); @@ -63,55 +87,116 @@ public class WebServiceAuthImpl implements DBWServiceAuth { DBWAuthProvider authProviderInstance = authProvider.getInstance(); DBWAuthProviderExternal authProviderExternal = authProviderInstance instanceof DBWAuthProviderExternal ? (DBWAuthProviderExternal) authProviderInstance : null; + Map userCredentials; if (authProviderExternal != null) { - authParameters = authProviderExternal.readExternalCredentials(webSession.getProgressMonitor(), providerConfig, authParameters); + userCredentials = authProviderExternal.authExternalUser(webSession.getProgressMonitor(), providerConfig, authParameters); + } else { + // User credentials are the same as auth parameters + userCredentials = authParameters; } - WebUser user = webSession.getUser(); - String userId = serverController.getUserByCredentials(authProvider, authParameters); - if (userId == null) { - // User doesn't exist. We can create new user automatically if auth provider supports this + WebUser user = null; + String userId; + DBASession authSession; + if (configMode) { if (authProviderExternal != null) { - user = authProviderExternal.registerNewUser(webSession.getProgressMonitor(), serverController, providerConfig, authParameters, user); - userId = user.getUserId(); + userId = authProviderExternal.validateLocalAuth( + webSession.getProgressMonitor(), + securityController, + providerConfig, + userCredentials, + webSession.getUser()); + } else { + userId = "temp_config_admin"; } - + } else { + WebUser curUser = webSession.getUser(); + userId = securityController.getUserByCredentials(authProvider, userCredentials); if (userId == null) { - throw new DBCException("Invalid user credentials"); + // User doesn't exist. We can create new user automatically if auth provider supports this + if (authProviderExternal != null) { + userId = authProviderExternal.validateLocalAuth( + webSession.getProgressMonitor(), + securityController, + providerConfig, + userCredentials, + curUser); + + if (curUser == null || !userId.equals(curUser.getUserId())) { + // Create new user + curUser = securityController.getUserById(userId); + if (curUser == null) { + curUser = new WebUser(userId); + securityController.createUser(curUser); + + String defaultRoleName = CBPlatform.getInstance().getApplication().getAppConfiguration().getDefaultUserRole(); + if (!CommonUtils.isEmpty(defaultRoleName)) { + securityController.setUserRoles( + userId, + new String[]{defaultRoleName}, + userId); + } + } + } else { + // We may need to associate new credentials with active user + if (linkWithActiveUser) { + securityController.setUserCredentials(userId, authProvider, userCredentials); + } + } + } + + if (userId == null) { + throw new DBCException("Invalid user credentials"); + } } - } - if (user != null && !user.getUserId().equals(userId)) { - log.debug("Attempt to authorize user '" + userId + "' while user '" + user.getUserId() + "' already authorized"); - throw new DBCException("You cannot authorize with different users credentials"); - } - - // Check for auth enabled. Auth is always enabled for admins - if (!CBApplication.getInstance().getAppConfiguration().isAuthenticationEnabled()) { - if (!serverController.getUserPermissions(userId).contains(DBWConstants.PERMISSION_ADMIN)) { - throw new DBWebException("Authentication was disabled for this server"); + if (curUser != null && !curUser.getUserId().equals(userId)) { + log.debug("Attempt to authorize user '" + userId + "' while user '" + curUser.getUserId() + "' already authorized"); + throw new DBCException("You cannot authorize with different users credentials"); } + + user = curUser; } - - Map userCredentials = serverController.getUserCredentials(userId, authProvider); - - DBASession authSession = authProviderInstance.openSession( - webSession, - providerConfig, - userCredentials, - authParameters); - if (user == null) { user = new WebUser(userId); } + + DBWUserIdentity userIdentity = null; + if (authProviderExternal != null) { - user.setDisplayName(authProviderExternal.getUserDisplayName(webSession.getProgressMonitor(), providerConfig, authParameters)); + try { + userIdentity = + authProviderExternal.getUserIdentity( + webSession.getProgressMonitor(), + providerConfig, + userCredentials); + } catch (DBException e) { + log.debug("Error reading auth display name from provider " + providerId, e); + } + } + if (userIdentity == null) { + userIdentity = new DBWUserIdentity(userId, userId); + } + if (CommonUtils.isEmpty(user.getDisplayName())) { + user.setDisplayName(userIdentity.getDisplayName()); } - WebAuthInfo authInfo = new WebAuthInfo(webSession, user); - authInfo.setLoginTime(OffsetDateTime.now()); - authInfo.setAuthProvider(authProvider); - authInfo.setAuthSession(authSession); + authSession = authProviderInstance.openSession( + webSession, + providerConfig, + userCredentials + ); + + WebAuthInfo authInfo = new WebAuthInfo( + webSession, + user, + authProvider, + userIdentity, + authSession, + OffsetDateTime.now()); authInfo.setMessage("Authenticated with " + authProvider.getLabel() + " provider"); + if (configMode) { + authInfo.setUserCredentials(userCredentials); + } webSession.addAuthInfo(authInfo); return authInfo; @@ -137,15 +222,7 @@ public class WebServiceAuthImpl implements DBWServiceAuth { } @Override - public WebAuthInfo sessionUser(@NotNull WebSession webSession) throws DBWebException { - if (webSession.getUser() == null) { - return null; - } - return webSession.getAuthInfo(null); - } - - @Override - public WebAuthProviderInfo[] getAuthProviders(@NotNull WebSession webSession) { + public WebAuthProviderInfo[] getAuthProviders() { return WebServiceRegistry.getInstance().getAuthProviders() .stream().map(WebAuthProviderInfo::new) .toArray(WebAuthProviderInfo[]::new); 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 2241e38607..376532c1e3 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: %Bundle-Vendor Bundle-Name: %Bundle-Name Bundle-SymbolicName: io.cloudbeaver.service.data.transfer;singleton:=true -Bundle-Version: 1.0.21.qualifier -Bundle-Release-Date: 20210322 +Bundle-Version: 1.0.22.qualifier +Bundle-Release-Date: 20210405 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 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 91009b675d..6f07da1396 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.21-SNAPSHOT + 1.0.22-SNAPSHOT eclipse-plugin 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 de411c7e2f..30df09fadf 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: %Bundle-Vendor Bundle-Name: %Bundle-Name Bundle-SymbolicName: io.cloudbeaver.service.metadata;singleton:=true -Bundle-Version: 1.0.24.qualifier -Bundle-Release-Date: 20210322 +Bundle-Version: 1.0.25.qualifier +Bundle-Release-Date: 20210405 Bundle-RequiredExecutionEnvironment: JavaSE-1.8 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 1a13cfa685..871278454d 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.24-SNAPSHOT + 1.0.25-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 1fd636359b..64b45ecf35 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 dd3880deed..4a4b5c8b6a 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.44-SNAPSHOT + 1.0.45-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 3bc4178b47..353c2768c9 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 1b400dadee..46d95e5385 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 - 20.0.5-SNAPSHOT + 20.0.6-SNAPSHOT eclipse-feature diff --git a/server/features/io.cloudbeaver.sample.feature/feature.xml b/server/features/io.cloudbeaver.sample.feature/feature.xml index b1283e5bb8..c6efa3b5c0 100644 --- a/server/features/io.cloudbeaver.sample.feature/feature.xml +++ b/server/features/io.cloudbeaver.sample.feature/feature.xml @@ -2,7 +2,7 @@ diff --git a/server/features/io.cloudbeaver.sample.feature/pom.xml b/server/features/io.cloudbeaver.sample.feature/pom.xml index e12ff598a6..9b21108671 100644 --- a/server/features/io.cloudbeaver.sample.feature/pom.xml +++ b/server/features/io.cloudbeaver.sample.feature/pom.xml @@ -9,6 +9,6 @@ ../ io.cloudbeaver.sample.feature - 1.0.24-SNAPSHOT + 1.0.25-SNAPSHOT eclipse-feature diff --git a/server/features/io.cloudbeaver.server.feature/feature.xml b/server/features/io.cloudbeaver.server.feature/feature.xml index f7c355255c..b0eee41f4b 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 8a6c966665..1987f11cf7 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 - 20.0.5-SNAPSHOT + 20.0.6-SNAPSHOT eclipse-feature diff --git a/server/pom.xml b/server/pom.xml index e0a851729e..262b9f85c7 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -19,7 +19,7 @@ CloudBeaver CE - 20.0.5 + 20.0.6 diff --git a/server/product/provided-sample/DBeaverWebProvidedSample.product b/server/product/provided-sample/DBeaverWebProvidedSample.product index 0871c94737..7173fe62a7 100644 --- a/server/product/provided-sample/DBeaverWebProvidedSample.product +++ b/server/product/provided-sample/DBeaverWebProvidedSample.product @@ -1,7 +1,7 @@ - + diff --git a/server/product/provided-sample/pom.xml b/server/product/provided-sample/pom.xml index fe2cdb58f7..946083d69e 100644 --- a/server/product/provided-sample/pom.xml +++ b/server/product/provided-sample/pom.xml @@ -9,7 +9,7 @@ 1.0.0-SNAPSHOT ../../ - 20.0.5-SNAPSHOT + 20.0.6-SNAPSHOT provided-sample eclipse-repository Cloudbeaver Provided Sample Product diff --git a/server/product/web-server/CloudbeaverServer.product b/server/product/web-server/CloudbeaverServer.product index 87d48d544e..0c1fb043e8 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/CloudbeaverServer.product.launch b/server/product/web-server/CloudbeaverServer.product.launch index 7058163bf8..21cff39161 100644 --- a/server/product/web-server/CloudbeaverServer.product.launch +++ b/server/product/web-server/CloudbeaverServer.product.launch @@ -51,10 +51,12 @@ + + diff --git a/server/product/web-server/pom.xml b/server/product/web-server/pom.xml index 0074d49057..a54e046171 100644 --- a/server/product/web-server/pom.xml +++ b/server/product/web-server/pom.xml @@ -9,7 +9,7 @@ 1.0.0-SNAPSHOT ../../ - 20.0.5-SNAPSHOT + 20.0.6-SNAPSHOT web-server eclipse-repository Cloudbeaver Server Product diff --git a/webapp/.eslintrc.js b/webapp/.eslintrc.js index 10d3d312f1..0e4c63b6be 100644 --- a/webapp/.eslintrc.js +++ b/webapp/.eslintrc.js @@ -85,7 +85,7 @@ module.exports = { ignoreTrailingComments: true, ignoreStrings: true, ignoreTemplateLiterals: true, - ignorePattern: "^export\\s(const\\s\\w+:?.+=.+function\\s|function\\s)\\w+\\s*\\(\\s*\\{$" + ignorePattern: "^export\\s(const\\s\\w+:?.+=.+function\\s|function\s)\\w+\\s*\\(.*\\{$" } ], 'import-helpers/order-imports': [ diff --git a/webapp/packages/core-administration/src/Administration/Administration.tsx b/webapp/packages/core-administration/src/Administration/Administration.tsx index 196f933e58..41debaffad 100644 --- a/webapp/packages/core-administration/src/Administration/Administration.tsx +++ b/webapp/packages/core-administration/src/Administration/Administration.tsx @@ -79,7 +79,7 @@ export const Administration: React.FC = observer(function Administration( const contentRef = useRef(null); const administrationItemService = useService(AdministrationItemService); const items = administrationItemService.getActiveItems(configurationWizard); - const hasOnlyActive = items.some(filterOnlyActive); + const hasOnlyActive = items.some(filterOnlyActive(configurationWizard)); useLayoutEffect(() => { contentRef.current?.scrollTo({ top: 0, left: 0 }); diff --git a/webapp/packages/core-administration/src/Administration/AdministrationTools/AdministrationTools.tsx b/webapp/packages/core-administration/src/Administration/AdministrationTools/AdministrationTools.tsx index e4fd13342b..095aa7b751 100644 --- a/webapp/packages/core-administration/src/Administration/AdministrationTools/AdministrationTools.tsx +++ b/webapp/packages/core-administration/src/Administration/AdministrationTools/AdministrationTools.tsx @@ -6,12 +6,14 @@ * you may not use this file except in compliance with the License. */ +import styled from 'reshadow'; + interface Props { className?: string; } export const AdministrationTools: React.FC = function AdministrationTools({ children, className }) { - return ( + return styled()( {children} diff --git a/webapp/packages/core-administration/src/Administration/DrawerItem.tsx b/webapp/packages/core-administration/src/Administration/DrawerItem.tsx index f837cc753d..7203ee69d6 100644 --- a/webapp/packages/core-administration/src/Administration/DrawerItem.tsx +++ b/webapp/packages/core-administration/src/Administration/DrawerItem.tsx @@ -24,7 +24,7 @@ export const DrawerItem = observer(function DrawerItem({ disabled = !configurationWizardService.isStepAvailable(item.name); } - const onlyActive = filterOnlyActive(item) ? false : disabled; + const onlyActive = filterOnlyActive(configurationWizard)(item) ? false : disabled; return ( = observer(function ItemContent({ return null; } + if (administrationItemService.itemActivating) { + return ; + } + if (activeScreen.sub) { const sub = administrationItemService.getItemSub(item, activeScreen.sub); if (sub) { diff --git a/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts b/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts index 086a29d14d..ef775be0eb 100644 --- a/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts +++ b/webapp/packages/core-administration/src/AdministrationItem/AdministrationItemService.ts @@ -9,6 +9,7 @@ import { observable, makeObservable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; +import { Executor, IExecutor, IExecutorHandler } from '@cloudbeaver/core-executor'; import type { RouterState } from '@cloudbeaver/core-routing'; import { filterConfigurationWizard } from './filterConfigurationWizard'; @@ -18,19 +19,76 @@ import { import type { IAdministrationItemRoute } from './IAdministrationItemRoute'; import { orderAdministrationItems } from './orderAdministrationItems'; +interface IActivationData { + screen: IAdministrationItemRoute; + configurationWizard: boolean; + outside: boolean; +} + @injectable() export class AdministrationItemService { items: IAdministrationItem[] = []; + itemActivating: boolean; + itemDeactivating: boolean; + + private activationTask: IExecutor; + private deActivationTask: IExecutor; + constructor() { makeObservable(this, { items: observable, + itemActivating: observable, + itemDeactivating: observable, }); + + this.itemActivating = false; + this.itemDeactivating = false; + this.activationTask = new Executor(); + this.deActivationTask = new Executor(); + + this.activationTask + .addHandler(() => { this.itemActivating = true; }) + .addHandler(this.activateHandler) + .addPostHandler(() => { this.itemActivating = false; }); + + this.deActivationTask + .addHandler(() => { this.itemDeactivating = true; }) + .addHandler(this.deActivateHandler) + .addPostHandler(() => { this.itemDeactivating = false; }); + } + + getUniqueItems(configurationWizard: boolean): IAdministrationItem[] { + const items: IAdministrationItem[] = []; + + const orderedByPriority = this.items.slice() + .sort((a, b) => { + if (a.name !== b.name) { + return a.name.localeCompare(b.name); + } + + if (a.replace && b.replace) { + return (b.replace.priority ?? Number.MIN_SAFE_INTEGER) - (a.replace.priority ?? Number.MIN_SAFE_INTEGER); + } + return 0; + }) + .filter(item => !item.replace?.condition || item.replace.condition(configurationWizard)); + + let lastItem: IAdministrationItem | null = null; + for (const item of orderedByPriority) { + if (lastItem?.name === item.name) { + continue; + } + items.push(item); + lastItem = item; + } + + return items; } getActiveItems(configurationWizard: boolean): IAdministrationItem[] { - return this.items.filter(item => - filterHiddenAdministrationItem(item) + return this.getUniqueItems(configurationWizard).filter(item => + filterHiddenAdministrationItem(configurationWizard)(item) && filterConfigurationWizard(configurationWizard)(item) ).sort(orderAdministrationItems(configurationWizard)); } @@ -42,7 +100,7 @@ export class AdministrationItemService { return null; } - const onlyActive = items.find(filterOnlyActive); + const onlyActive = items.find(filterOnlyActive(configurationWizard)); if (onlyActive) { return onlyActive.name; @@ -60,11 +118,7 @@ export class AdministrationItemService { } getItem(name: string, configurationWizard: boolean): IAdministrationItem | null { - const item = this.items.find(item => ( - filterHiddenAdministrationItem(item) - && filterConfigurationWizard(configurationWizard)(item) - && item.name === name - )); + const item = this.getActiveItems(configurationWizard).find(item => item.name === name); if (!item) { return null; @@ -82,7 +136,7 @@ export class AdministrationItemService { return sub; } - create(options: IAdministrationItemOptions, replace?: boolean): void { + create(options: IAdministrationItemOptions): void { const type = options.type ?? AdministrationItemType.Administration; const existedIndex = this.items.findIndex(item => item.name === options.name && ( @@ -91,7 +145,7 @@ export class AdministrationItemService { || type === AdministrationItemType.Default )); - if (!replace && existedIndex !== -1) { + if (!options.replace && existedIndex !== -1) { throw new Error(`Administration item "${options.name}" already exists in the same visibility scope`); } @@ -101,11 +155,7 @@ export class AdministrationItemService { sub: options.sub ?? [], order: options.order ?? Number.MAX_SAFE_INTEGER, }; - if (replace && existedIndex !== -1) { - this.items.splice(existedIndex, 1, item); - } else { - this.items.push(item); - } + this.items.push(item); } async activate( @@ -113,20 +163,7 @@ export class AdministrationItemService { configurationWizard: boolean, outside: boolean ): Promise { - const item = this.getItem(screen.item, configurationWizard); - if (!item) { - return; - } - - if (!filterHiddenAdministrationItem(item)) { - return; - } - - await item.onActivate?.(configurationWizard, outside); - - if (screen.sub) { - await this.getItemSub(item, screen.sub)?.onActivate?.(screen.param, configurationWizard, outside); - } + await this.activationTask.execute({ screen, configurationWizard, outside }); } async deActivate( @@ -134,20 +171,7 @@ export class AdministrationItemService { configurationWizard: boolean, outside: boolean ): Promise { - const item = this.getItem(screen.item, configurationWizard); - if (!item) { - return; - } - - if (!filterHiddenAdministrationItem(item)) { - return; - } - - await item.onDeActivate?.(configurationWizard, outside); - - if (screen.sub) { - await this.getItemSub(item, screen.sub)?.onDeActivate?.(screen.param, configurationWizard, outside); - } + await this.deActivationTask.execute({ screen, configurationWizard, outside }); } async canActivate( @@ -160,10 +184,6 @@ export class AdministrationItemService { return false; } - if (!filterHiddenAdministrationItem(item)) { - return false; - } - if (item.canActivate && !(await item.canActivate(configurationWizard, outside))) { return false; } @@ -177,20 +197,62 @@ export class AdministrationItemService { return true; } + + private activateHandler: IExecutorHandler = async ({ screen, configurationWizard, outside }) => { + if (configurationWizard) { + let item = 0; + while (true) { + const items = this.getActiveItems(configurationWizard); + if (item === items.length) { + break; + } + await items[item].configurationWizardOptions?.onLoad?.(); + item++; + } + } + + const item = this.getItem(screen.item, configurationWizard); + if (!item) { + return; + } + + await item.onActivate?.(configurationWizard, outside); + + if (screen.sub) { + await this.getItemSub(item, screen.sub)?.onActivate?.(screen.param, configurationWizard, outside); + } + }; + + private deActivateHandler: IExecutorHandler = async ({ screen, configurationWizard, outside }) => { + const item = this.getItem(screen.item, configurationWizard); + if (!item) { + return; + } + + await item.onDeActivate?.(configurationWizard, outside); + + if (screen.sub) { + await this.getItemSub(item, screen.sub)?.onDeActivate?.(screen.param, configurationWizard, outside); + } + }; } -export function filterHiddenAdministrationItem(item: IAdministrationItem): boolean { - if (typeof item.isHidden === 'function') { - return !item.isHidden(); - } +export function filterHiddenAdministrationItem(configurationWizard: boolean): (item: IAdministrationItem) => boolean { + return function filterHiddenAdministrationItem(item: IAdministrationItem) { + if (typeof item.isHidden === 'function') { + return !item.isHidden(configurationWizard); + } - return !item.isHidden; + return !item.isHidden; + }; } -export function filterOnlyActive(item: IAdministrationItem): boolean { - if (typeof item.isOnlyActive === 'function') { - return item.isOnlyActive(); - } +export function filterOnlyActive(configurationWizard: boolean): (item: IAdministrationItem) => boolean { + return function filterOnlyActive(item: IAdministrationItem) { + if (typeof item.isOnlyActive === 'function') { + return item.isOnlyActive(configurationWizard); + } - return item.isOnlyActive === true; + return item.isOnlyActive === true; + }; } diff --git a/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts b/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts index 6f0ec32ee2..61b370d814 100644 --- a/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts +++ b/webapp/packages/core-administration/src/AdministrationItem/IAdministrationItem.ts @@ -16,6 +16,11 @@ export enum AdministrationItemType { ConfigurationWizard } +export interface IAdministrationItemReplaceOptions { + priority: number; + condition?: (configurationWizard: boolean) => boolean; +} + export interface AdministrationItemDrawerProps { item: IAdministrationItem; configurationWizard: boolean; @@ -69,8 +74,9 @@ export interface IConfigurationWizardItemOptions { isDisabled?: () => boolean; isHidden?: () => boolean; isDone?: () => boolean; + onLoad?: () => Promise | void; onFinish?: () => Promise | boolean; - onConfigurationFinish?: () => Promise | void; + onConfigurationFinish?: () => Promise | any; } export interface IAdministrationItemOptions { @@ -80,8 +86,9 @@ export interface IAdministrationItemOptions { configurationWizardOptions?: IConfigurationWizardItemOptions; order?: number; sub?: IAdministrationItemSubItem[]; - isHidden?: (() => boolean) | boolean; - isOnlyActive?: (() => boolean) | boolean; + isHidden?: ((configurationWizard: boolean) => boolean) | boolean; + isOnlyActive?: ((configurationWizard: boolean) => boolean) | boolean; + replace?: IAdministrationItemReplaceOptions; getDrawerComponent: () => AdministrationItemDrawerComponent; getContentComponent: () => AdministrationItemContentComponent; onActivate?: AdministrationItemEvent; diff --git a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts index 5f82c99d00..20eae196eb 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts @@ -13,6 +13,7 @@ import { IExecutor, Executor } from '@cloudbeaver/core-executor'; import { PermissionsResource, PermissionsService, ServerConfigResource } from '@cloudbeaver/core-root'; import { ScreenService, RouterState } from '@cloudbeaver/core-routing'; import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; +import { BuildVersion } from '@cloudbeaver/core-utils'; import { AdministrationItemService } from '../AdministrationItem/AdministrationItemService'; import type { IAdministrationItemRoute } from '../AdministrationItem/IAdministrationItemRoute'; @@ -20,10 +21,13 @@ import type { IRouteParams } from '../AdministrationItem/IRouteParams'; import { EAdminPermission } from '../EAdminPermission'; const ADMINISTRATION_ITEMS_STATE = 'administration_items_state'; -const ADMINISTRATION_INFO = 'administration_mode'; +const ADMINISTRATION_INFO = 'administration_info'; interface IAdministrationScreenInfo { - mode: boolean; + workspaceId: string; + version: string; + serverVersion: string; + configurationMode: boolean; } @injectable() @@ -46,16 +50,11 @@ export class AdministrationScreenService { } get isConfigurationMode(): boolean { - return !!this.serverConfigResource.data?.configurationMode; + return this.serverConfigResource.configurationMode; } get publicDisabled(): boolean { - if (this.serverConfigResource.data?.configurationMode - || (this.serverConfigResource.data?.licenseRequired && !this.serverConfigResource.data?.licenseValid)) { - return true; - } - - return false; + return this.serverConfigResource.publicDisabled; } readonly ensurePermissions: IExecutor; @@ -76,7 +75,10 @@ export class AdministrationScreenService { }); this.info = { - mode: false, + workspaceId: '', + version: BuildVersion.version || '', + serverVersion: '', + configurationMode: false, }; this.itemState = new Map(); this.activationEvent = new Executor(); @@ -84,7 +86,7 @@ export class AdministrationScreenService { this.autoSaveService.withAutoSave(this.itemState, ADMINISTRATION_ITEMS_STATE); this.autoSaveService.withAutoSave(this.info, ADMINISTRATION_INFO); - this.permissionsResource.onDataUpdate.addHandler(() => { + this.permissionsResource.onDataUpdate.addPostHandler(() => { this.checkPermissions(this.screenService.routerService.state); }); } @@ -140,16 +142,25 @@ export class AdministrationScreenService { getItemState(name: string): T | undefined getItemState(name: string, defaultState: () => T, update?: boolean, validate?: (state: T) => boolean): T getItemState( - name: string, defaultState?: () => T, + name: string, + defaultState?: () => T, update?: boolean, validate?: (state: T) => boolean ): T | undefined { if (!this.serverConfigResource.isLoaded()) { throw new Error('Administration screen getItemState can be used only after server configuration loaded'); } - if (this.info.mode !== this.isConfigurationMode) { + if ( + this.info.workspaceId !== this.serverConfigResource.workspaceId + || this.info.configurationMode !== this.isConfigurationMode + || this.info.serverVersion !== this.serverConfigResource.serverVersion + || this.info.version !== BuildVersion.version + ) { this.clearItemsState(); - this.info.mode = this.isConfigurationMode; + this.info.workspaceId = this.serverConfigResource.workspaceId; + this.info.configurationMode = this.isConfigurationMode; + this.info.serverVersion = this.serverConfigResource.serverVersion; + this.info.version = BuildVersion.version || ''; } if (defaultState) { @@ -245,7 +256,7 @@ export class AdministrationScreenService { return true; } - private async isAccessProvided(state: RouterState) { + private async isAccessProvided(state: RouterState): Promise { await this.serverConfigResource.load(); if (this.isConfigurationMode) { @@ -253,8 +264,7 @@ export class AdministrationScreenService { } if (this.screenService.isActive(state.name, AdministrationScreenService.setupName)) { - this.navigateToRoot(); - return; + return false; } await this.ensurePermissions.execute(); diff --git a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.ts b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.ts index 8ce6b5abd1..5d5ddb776b 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/ConfigurationWizard/ConfigurationWizardService.ts @@ -11,7 +11,7 @@ import { computed, makeObservable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { ScreenService } from '@cloudbeaver/core-routing'; -import { AdministrationItemService } from '../../AdministrationItem/AdministrationItemService'; +import { AdministrationItemService, filterHiddenAdministrationItem } from '../../AdministrationItem/AdministrationItemService'; import { filterConfigurationWizard } from '../../AdministrationItem/filterConfigurationWizard'; import type { IAdministrationItem } from '../../AdministrationItem/IAdministrationItem'; import { orderAdministrationItems } from '../../AdministrationItem/orderAdministrationItems'; @@ -20,8 +20,11 @@ import { AdministrationScreenService } from '../AdministrationScreenService'; @injectable() export class ConfigurationWizardService { get steps(): IAdministrationItem[] { - return this.administrationItemService.items - .filter(filterConfigurationWizard(true)) + return this.administrationItemService.getUniqueItems(true) + .filter(item => + filterConfigurationWizard(true)(item) + && filterHiddenAdministrationItem(true)(item) + ) .sort(orderAdministrationItems(true)); } @@ -163,7 +166,7 @@ export class ConfigurationWizardService { } private getStep(name: string) { - return this.administrationItemService.items + return this.administrationItemService.getUniqueItems(true) .find(step => filterConfigurationWizard(true)(step) && step.name === name); } diff --git a/webapp/packages/core-app/public/icons/product-logo_dark.svg b/webapp/packages/core-app/public/icons/product-logo_dark.svg new file mode 100644 index 0000000000..966881ade7 --- /dev/null +++ b/webapp/packages/core-app/public/icons/product-logo_dark.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/webapp/packages/core-app/public/icons/product-logo_light.svg b/webapp/packages/core-app/public/icons/product-logo_light.svg new file mode 100644 index 0000000000..693353bef4 --- /dev/null +++ b/webapp/packages/core-app/public/icons/product-logo_light.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/webapp/packages/core-app/src/CoreSettingsService.ts b/webapp/packages/core-app/src/CoreSettingsService.ts index a6abacd327..4d5917a883 100644 --- a/webapp/packages/core-app/src/CoreSettingsService.ts +++ b/webapp/packages/core-app/src/CoreSettingsService.ts @@ -14,6 +14,7 @@ const defaultSettings = { 'app.logViewer.maxLogRecords': 1000, 'app.logViewer.logBatchSize': 2000, 'app.logViewer.maxFailedRequests': 3, + // temporary limit for all nodes children in app 'app.navigationTree.childrenLimit': 500, }; diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts index ade2a43743..a4101e7d7d 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeContextMenuService.ts @@ -6,10 +6,11 @@ * you may not use this file except in compliance with the License. */ -import { ConnectionInfoResource, CONNECTION_NAVIGATOR_VIEW_SETTINGS, isNavigatorViewSettingsEqual, NavigatorViewSettings } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; +import { ConnectionInfoResource } from '@cloudbeaver/core-connections'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ContextMenuService, IMenuPanel } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; +import { isNavigatorViewSettingsEqual, CONNECTION_NAVIGATOR_VIEW_SETTINGS, NavigatorViewSettings } from '@cloudbeaver/core-root'; import type { NavNode } from '../shared/NodesManager/EntityTypes'; import { EObjectFeature } from '../shared/NodesManager/EObjectFeature'; @@ -17,7 +18,7 @@ import { NavNodeManagerService } from '../shared/NodesManager/NavNodeManagerServ import { NodeManagerUtils } from '../shared/NodesManager/NodeManagerUtils'; @injectable() -export class NavigationTreeContextMenuService { +export class NavigationTreeContextMenuService extends Bootstrap { static nodeContextType = 'NodeWithParent'; private static nodeViewMenuItemToken = 'nodeView'; private static menuToken = 'navTreeMenu'; @@ -27,7 +28,9 @@ export class NavigationTreeContextMenuService { private navNodeManagerService: NavNodeManagerService, private notificationService: NotificationService, private connectionInfoResource: ConnectionInfoResource - ) { } + ) { + super(); + } getMenuToken(): string { return NavigationTreeContextMenuService.menuToken; @@ -80,6 +83,10 @@ export class NavigationTreeContextMenuService { return context.contextType === NavigationTreeContextMenuService.nodeContextType && context.data.objectFeatures.includes(EObjectFeature.dataSource); }, + isHidden: context => { + const connection = this.getConnectionFromNodeId(context.data.id); + return !connection?.connected; + }, order: 2, title: 'app_navigationTree_connection_view', isPanel: true, @@ -144,7 +151,7 @@ export class NavigationTreeContextMenuService { ); } - registerMenuItems() { + register(): void { this.contextMenuService.addMenuItem( this.contextMenuService.getRootMenuToken(), { @@ -170,13 +177,19 @@ export class NavigationTreeContextMenuService { }, order: Number.MAX_SAFE_INTEGER, title: 'app_navigationTree_refreshNode', - onClick: context => { + onClick: async context => { const node = context.data; - this.navNodeManagerService.refreshTree(node.id); + try { + await this.navNodeManagerService.refreshTree(node.id); + } catch (exception) { + this.notificationService.logException(exception, 'Failed to refresh node'); + } }, } ); this.registerNodeViewMenuItem(); } + + load(): void { } } diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx index 8f44f9cdeb..9016180952 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx @@ -7,7 +7,7 @@ */ import { observer } from 'mobx-react-lite'; -import { useContext } from 'react'; +import { useCallback, useContext } from 'react'; import styled, { css } from 'reshadow'; import { TreeNodeContext, TreeNodeControl, TreeNodeExpand, TreeNodeIcon, TreeNodeName, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks'; @@ -71,8 +71,12 @@ export const NavigationNodeControl: React.FC = observer(function Navigati connected = !!connectionInfo?.connected; } + const onClickHandler = useCallback((event: React.MouseEvent) => { + context?.select(event.ctrlKey || event.metaKey); + }, [context]); + return styled(useStyles(TREE_NODE_STYLES, styles))( - + {connected && } diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts index 1299db75af..96cdfb80c5 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { action, computed, makeObservable } from 'mobx'; +import { action, makeObservable } from 'mobx'; import { ConnectionAuthService } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; @@ -39,10 +39,6 @@ export class NavigationTreeService { readonly navigationTreeMetadata: MetadataMap; readonly nodeSelectionTask: IExecutor; - get childrenLimit(): number { - return this.coreSettingsService.settings.getValue('app.navigationTree.childrenLimit'); - } - constructor( private navNodeManagerService: NavNodeManagerService, private notificationService: NotificationService, @@ -53,7 +49,6 @@ export class NavigationTreeService { private navNodeInfoResource: NavNodeInfoResource ) { makeObservable(this, { - childrenLimit: computed, unselectAll: action, }); @@ -77,13 +72,7 @@ export class NavigationTreeService { } getChildren(id: string): string[] | undefined { - const children = this.navTreeResource.get(id); - - if (children) { - return children.slice(0, this.childrenLimit); - } - - return children; + return this.navTreeResource.get(id); } async navToNode(id: string, parentId: string): Promise { @@ -92,7 +81,7 @@ export class NavigationTreeService { async loadNestedNodes(id = ROOT_NODE_PATH): Promise { try { - if (this.isConnectionNode(id) && !await this.tryInitConnection(id)) { + if (this.isConnectionNode(id) && !(await this.tryInitConnection(id))) { return false; } await this.navNodeManagerService.loadTree(id); @@ -158,10 +147,11 @@ export class NavigationTreeService { return node?.objectFeatures.includes(EObjectFeature.dataSource); } - private async tryInitConnection(navNodeId: string) { + private async tryInitConnection(navNodeId: string): Promise { const connection = await this.connectionAuthService.auth( - NodeManagerUtils.connectionNodeIdToConnectionId(navNodeId)); + NodeManagerUtils.connectionNodeIdToConnectionId(navNodeId) + ); - return connection.connected; + return connection?.connected || false; } } diff --git a/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts b/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts index 8524ba3094..48f0276314 100644 --- a/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts +++ b/webapp/packages/core-app/src/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts @@ -8,7 +8,6 @@ import { computed, observable, makeObservable } from 'mobx'; -import { AppAuthService } from '@cloudbeaver/core-authentication'; import { ConnectionInfoResource, ConnectionsManagerService, @@ -17,9 +16,10 @@ import { isConnectionProvider, IConnectionProvider, isConnectionSetter, IConnectionSetter } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { ExtensionUtils, IExtension } from '@cloudbeaver/core-extensions'; +import { SessionDataResource } from '@cloudbeaver/core-root'; import type { ITab } from '../../shared/NavigationTabs/ITab'; import { NavigationTabsService } from '../../shared/NavigationTabs/NavigationTabsService'; @@ -45,7 +45,7 @@ interface IActiveItem { } @injectable() -export class ConnectionSchemaManagerService { +export class ConnectionSchemaManagerService extends Bootstrap { get currentConnectionId(): string | null | undefined { if (!this.activeItem?.getCurrentConnectionId) { return null; @@ -125,8 +125,9 @@ export class ConnectionSchemaManagerService { private connectionsManagerService: ConnectionsManagerService, private dbDriverResource: DBDriverResource, private notificationService: NotificationService, - private appAuthService: AppAuthService + private sessionDataResource: SessionDataResource ) { + super(); makeObservable(this, { currentObjectCatalog: computed, currentObjectSchema: computed, @@ -136,16 +137,19 @@ export class ConnectionSchemaManagerService { }); } - registerCallbacks(): void { + register(): void { + this.sessionDataResource.onDataUpdate + .addHandler(this.reset.bind(this)); + this.navigationTabsService.onTabSelect .subscribe(this.onTabSelect.bind(this)); this.navigationTabsService.onTabClose .subscribe(this.onTabClose.bind(this)); - - this.appAuthService.auth.addHandler(() => this.reset()); } + load(): void {} + /** * Trigger when user select connection in dropdown */ diff --git a/webapp/packages/core-app/src/TopNavBar/shared/topMenuStyles.ts b/webapp/packages/core-app/src/TopNavBar/shared/topMenuStyles.ts index 1559149a4b..8c45278b29 100644 --- a/webapp/packages/core-app/src/TopNavBar/shared/topMenuStyles.ts +++ b/webapp/packages/core-app/src/TopNavBar/shared/topMenuStyles.ts @@ -15,6 +15,10 @@ export const topMenuStyles = composes( Menu { composes: theme-text-on-primary from global; } + + checkbox { + composes: theme-checkbox_surface from global; + } `, css` Menu { diff --git a/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts b/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts index e66a731557..211eb0a9b8 100644 --- a/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts +++ b/webapp/packages/core-app/src/shared/NavigationTabs/NavigationTabsService.ts @@ -6,13 +6,14 @@ * you may not use this file except in compliance with the License. */ -import { observable, action, computed, IKeyValueMap, makeObservable } from 'mobx'; +import { observable, action, computed, makeObservable } from 'mobx'; import { Subject } from 'rxjs'; import { AdministrationScreenService } from '@cloudbeaver/core-administration'; import { AppAuthService, UserInfoResource } from '@cloudbeaver/core-authentication'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; +import { SessionDataResource } from '@cloudbeaver/core-root'; import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; import type { IActiveView } from '@cloudbeaver/core-view'; @@ -68,6 +69,7 @@ export class NavigationTabsService { private autoSaveService: LocalStorageSaveService, private userInfoResource: UserInfoResource, private administrationScreenService: AdministrationScreenService, + sessionDataResource: SessionDataResource, appAuthService: AppAuthService ) { makeObservable(this, { @@ -120,10 +122,10 @@ export class NavigationTabsService { } ); - appAuthService.auth + sessionDataResource.onDataUpdate .addHandler(() => this.unloadTabs()) - .addPostHandler(async state => { - if (state || appAuthService.authenticated) { + .addPostHandler(async () => { + if (appAuthService.authenticated) { await this.restoreTabs(); } }); @@ -313,6 +315,10 @@ export class NavigationTabsService { } private async restoreTab(tab: ITab, removedTabs: string[]): Promise { + if (tab.restored) { + return; + } + let restoreFail = false; try { const handler = this.handlers.get(tab.handlerId); diff --git a/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts b/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts index 0773200672..f2943953f6 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts @@ -7,8 +7,9 @@ */ import { ConnectionInfoResource, ConnectionsManagerService, EConnectionFeature } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ContextMenuService, IMenuContext } from '@cloudbeaver/core-dialogs'; +import { NotificationService } from '@cloudbeaver/core-events'; import { NavigationTreeContextMenuService } from '../../NavigationTree/NavigationTreeContextMenuService'; import { EMainMenu, MainMenuService } from '../../TopNavBar/MainMenu/MainMenuService'; @@ -17,15 +18,18 @@ import { EObjectFeature } from './EObjectFeature'; import { NodeManagerUtils } from './NodeManagerUtils'; @injectable() -export class ConnectionDialogsService { +export class ConnectionDialogsService extends Bootstrap { constructor( private mainMenuService: MainMenuService, private contextMenuService: ContextMenuService, private connectionsManagerService: ConnectionsManagerService, - private connectionInfoResource: ConnectionInfoResource - ) {} + private connectionInfoResource: ConnectionInfoResource, + private notificationService: NotificationService, + ) { + super(); + } - registerMenuItems(): void { + register(): void { this.mainMenuService.registerMenuItem( EMainMenu.mainMenuConnectionsPanel, { @@ -74,13 +78,19 @@ export class ConnectionDialogsService { || !connection?.features.includes(EConnectionFeature.manageable); }, title: 'ui_delete', - onClick: (context: IMenuContext) => { + onClick: async (context: IMenuContext) => { const node = context.data; - this.connectionsManagerService.deleteConnection( - NodeManagerUtils.connectionNodeIdToConnectionId(node.id) - ); + try { + await this.connectionsManagerService.deleteConnection( + NodeManagerUtils.connectionNodeIdToConnectionId(node.id) + ); + } catch (exception) { + this.notificationService.logException(exception, 'Failed to delete connection'); + } }, } ); } + + load(): void { } } diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts index f8bd258354..682d0db38c 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts @@ -44,6 +44,7 @@ export class NavNodeInfoResource extends CachedMapResource { outdated: true, loading: false, withDetails: false, + exception: null, includes: [], })); } diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts index 452bd334d6..558c9ee759 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts @@ -6,7 +6,6 @@ * you may not use this file except in compliance with the License. */ -import { AppAuthService } from '@cloudbeaver/core-authentication'; import { ConnectionAuthService, Connection, ConnectionInfoResource } from '@cloudbeaver/core-connections'; @@ -14,7 +13,7 @@ import { injectable, Bootstrap } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; import { IExecutor, Executor, IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { - PermissionsService, EPermission, ServerService + PermissionsService, EPermission, ServerService, SessionDataResource } from '@cloudbeaver/core-root'; import { GraphQLService, resourceKeyList, ResourceKey, ResourceKeyUtils @@ -101,7 +100,7 @@ export class NavNodeManagerService extends Bootstrap { private connectionAuthService: ConnectionAuthService, private notificationService: NotificationService, private serverService: ServerService, - private appAuthService: AppAuthService, + private sessionDataResource: SessionDataResource, navigationService: NavigationService ) { super(); @@ -118,7 +117,7 @@ export class NavNodeManagerService extends Bootstrap { } register(): void { - this.appAuthService.auth.addHandler(this.refreshRoot.bind(this)); + this.sessionDataResource.onDataUpdate.addHandler(this.refreshRoot.bind(this)); this.connectionInfo.onSessionUpdate.addHandler(this.connectionRefreshHandler.bind(this)); this.connectionInfo.onItemAdd.addHandler(this.connectionUpdateHandler.bind(this)); this.connectionInfo.onItemDelete.addHandler(this.connectionRemoveHandler.bind(this)); @@ -420,7 +419,7 @@ export class NavNodeManagerService extends Bootstrap { const nodeInfo = await contexts.getContext(this.navigationNavNodeContext); if (NodeManagerUtils.isDatabaseObject(nodeInfo.nodeId) && nodeInfo.connection) { - let connection: Connection | undefined; + let connection: Connection | null; try { connection = await this.connectionAuthService.auth(nodeInfo.connection.id); } catch (exception) { diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts index 9bed835acb..fe7d329cf0 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { action, makeObservable } from 'mobx'; +import { action, computed, makeObservable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { @@ -22,6 +22,7 @@ import { } from '@cloudbeaver/core-sdk'; import { MetadataMap } from '@cloudbeaver/core-utils'; +import { CoreSettingsService } from '../../CoreSettingsService'; import { NavNodeInfoResource } from './NavNodeInfoResource'; // TODO: so much dirty @@ -39,13 +40,19 @@ interface INodeMetadata extends ICachedMapResourceMetadata { export class NavTreeResource extends CachedMapResource { protected metadata: MetadataMap; + get childrenLimit(): number { + return this.coreSettingsService.settings.getValue('app.navigationTree.childrenLimit'); + } + constructor( private graphQLService: GraphQLService, - private navNodeInfoResource: NavNodeInfoResource + private navNodeInfoResource: NavNodeInfoResource, + private coreSettingsService: CoreSettingsService, ) { super(); makeObservable(this, { + childrenLimit: computed, setDetails: action, }); @@ -53,6 +60,7 @@ export class NavTreeResource extends CachedMapResource { outdated: true, loading: false, withDetails: false, + exception: null, includes: [], })); this.onDataOutdated.addHandler(navNodeInfoResource.markOutdated.bind(navNodeInfoResource)); @@ -241,6 +249,6 @@ export class NavTreeResource extends CachedMapResource { withDetails: metadata.withDetails, }); - return { navNodeChildren, navNodeInfo, parentPath }; + return { navNodeChildren: navNodeChildren.slice(0, this.childrenLimit), navNodeInfo, parentPath }; } } diff --git a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx index 94e4eb64d3..bc9e6c9c1e 100644 --- a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx +++ b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogEntry/LogEntry.tsx @@ -9,7 +9,7 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; -import { Icon } from '@cloudbeaver/core-blocks'; +import { Link } from '@cloudbeaver/core-blocks'; import { useController } from '@cloudbeaver/core-di'; import { useStyles } from '@cloudbeaver/core-theming'; @@ -26,25 +26,15 @@ const style = css` align-items: center; } message { - flex: 1; word-break: break-word; white-space: nowrap; overflow: hidden; padding-right: 16px; text-overflow: ellipsis; } - - snack { - display: flex; + Link:hover { cursor: pointer; } - - snack Icon { - display: block; - width: 16px; - height: 10px; - fill: rgba(255, 255, 255, 0); - } tr:hover Icon { fill: #338fcc; @@ -61,11 +51,8 @@ export const LogEntry = observer(function LogEntry({ item }: LogEntryProps) { - {item.message} + {item.stackTrace ? {item.message} : item.message} - - - diff --git a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerMenuService.ts b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerMenuService.ts index 87aa0d30a3..27d3896e53 100644 --- a/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerMenuService.ts +++ b/webapp/packages/core-app/src/shared/ToolsPanel/LogViewTab/LogViewerMenuService.ts @@ -6,23 +6,28 @@ * you may not use this file except in compliance with the License. */ -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { EMainMenu, MainMenuService } from '../../../TopNavBar/MainMenu/MainMenuService'; import { LogViewerService } from './LogViewerService'; @injectable() -export class LogViewerMenuService { - constructor(private mainMenuService: MainMenuService, - private logViewerService: LogViewerService) { +export class LogViewerMenuService extends Bootstrap { + constructor( + private mainMenuService: MainMenuService, + private logViewerService: LogViewerService + ) { + super(); } - registerMenuItems() { + register(): void { this.mainMenuService.registerMenuItem( EMainMenu.mainMenuToolsPanel, { id: 'openLogViewer', order: 1, + type: 'checkbox', + isChecked: () => this.logViewerService.isActive, title: 'app_shared_toolsMenu_logViewer', onClick: () => this.toggleLogViewer(), } @@ -32,4 +37,6 @@ export class LogViewerMenuService { toggleLogViewer() { this.logViewerService.toggle(); } + + load(): void {} } diff --git a/webapp/packages/core-app/src/useAppVersion.tsx b/webapp/packages/core-app/src/useAppVersion.tsx index 2b9d42809a..aa96bdce83 100644 --- a/webapp/packages/core-app/src/useAppVersion.tsx +++ b/webapp/packages/core-app/src/useAppVersion.tsx @@ -8,19 +8,19 @@ import { useService } from '@cloudbeaver/core-di'; import { ServerService } from '@cloudbeaver/core-root'; +import { BuildVersion } from '@cloudbeaver/core-utils'; interface IAppVersion { backendVersion: string; frontendVersion: string; } -declare const _VERSION_: string | undefined; const VERSION_REGEX = /(\d+\.\d+\.\d+)/; export function useAppVersion(short = false): IAppVersion { const serverService = useService(ServerService); let backendVersion = serverService.config.data?.version || ''; - let frontendVersion = _VERSION_ || ''; + let frontendVersion = BuildVersion.version || ''; if (short) { backendVersion = VERSION_REGEX.exec(backendVersion)?.[1] ?? backendVersion; diff --git a/webapp/packages/core-authentication/src/AUTH_PROVIDER_LOCAL_ID.ts b/webapp/packages/core-authentication/src/AUTH_PROVIDER_LOCAL_ID.ts new file mode 100644 index 0000000000..d514ef403c --- /dev/null +++ b/webapp/packages/core-authentication/src/AUTH_PROVIDER_LOCAL_ID.ts @@ -0,0 +1,9 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 const AUTH_PROVIDER_LOCAL_ID = 'local'; diff --git a/webapp/packages/core-authentication/src/AppAuthService.ts b/webapp/packages/core-authentication/src/AppAuthService.ts index ede9492e54..a4c40b30f1 100644 --- a/webapp/packages/core-authentication/src/AppAuthService.ts +++ b/webapp/packages/core-authentication/src/AppAuthService.ts @@ -8,7 +8,7 @@ import { injectable, Bootstrap } from '@cloudbeaver/core-di'; import { Executor, IExecutor } from '@cloudbeaver/core-executor'; -import { ServerService, SessionService } from '@cloudbeaver/core-root'; +import { ServerService } from '@cloudbeaver/core-root'; import { UserInfoResource } from './UserInfoResource'; @@ -24,14 +24,14 @@ export class AppAuthService extends Bootstrap { } readonly auth: IExecutor; + constructor( private serverService: ServerService, - private sessionService: SessionService, private userInfoResource: UserInfoResource, ) { super(); this.auth = new Executor(); - this.sessionService.session.onDataUpdate.addHandler(async () => { await this.authUser(); }); + this.userInfoResource.onDataUpdate.addHandler(this.authUser.bind(this)); } async isAuthNeeded(): Promise { diff --git a/webapp/packages/core-authentication/src/AuthInfoService.ts b/webapp/packages/core-authentication/src/AuthInfoService.ts index cecfb6692f..f8d0769674 100644 --- a/webapp/packages/core-authentication/src/AuthInfoService.ts +++ b/webapp/packages/core-authentication/src/AuthInfoService.ts @@ -7,7 +7,7 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import type { UserAuthInfo } from '@cloudbeaver/core-sdk'; +import type { UserInfo } from '@cloudbeaver/core-sdk'; import { UserInfoResource } from './UserInfoResource'; @@ -17,12 +17,12 @@ export class AuthInfoService { private userInfoResource: UserInfoResource, ) { } - get userInfo(): UserAuthInfo | null { + get userInfo(): UserInfo | null { return this.userInfoResource.data; } - async login(provider: string, credentials: Record): Promise { - return this.userInfoResource.login(provider, credentials); + async login(provider: string, credentials: Record, link?: boolean): Promise { + return this.userInfoResource.login(provider, credentials, link); } async logout(): Promise { diff --git a/webapp/packages/core-authentication/src/AuthProviderService.ts b/webapp/packages/core-authentication/src/AuthProviderService.ts index 1407a6ef14..3967ad02df 100644 --- a/webapp/packages/core-authentication/src/AuthProviderService.ts +++ b/webapp/packages/core-authentication/src/AuthProviderService.ts @@ -7,19 +7,40 @@ */ import { injectable } from '@cloudbeaver/core-di'; +import { Executor, IExecutor } from '@cloudbeaver/core-executor'; +import type { ObjectOrigin } from '@cloudbeaver/core-sdk'; import { md5 } from '@cloudbeaver/core-utils'; import { AuthProvidersResource } from './AuthProvidersResource'; @injectable() export class AuthProviderService { + readonly requestAuthProvider: IExecutor; constructor( - private providers: AuthProvidersResource - ) { } + private readonly authProvidersResource: AuthProvidersResource + ) { + this.requestAuthProvider = new Executor(); + } + + async requireProvider(type: string, subType?: string): Promise + async requireProvider(origin: ObjectOrigin): Promise + async requireProvider(origin: ObjectOrigin | string, subType?: string): Promise { + if (typeof origin === 'string') { + origin = { + displayName: '', + type: origin, + subType, + }; + } + + const contexts = await this.requestAuthProvider.execute(origin); + const provider = contexts.getContext(AuthProviderContext); + + return provider.get(); + } async processCredentials(providerId: string, credentials: Record): Promise> { - const providers = await this.providers.load(); - const provider = providers.find(provider => provider.id === providerId); + const provider = await this.authProvidersResource.load(providerId); if (!provider) { return credentials; @@ -37,3 +58,19 @@ export class AuthProviderService { return credentialsProcessed; } } + +interface IAuthProviderContext{ + get: () => boolean; + auth: () => void; +} + +export function AuthProviderContext(): IAuthProviderContext { + let state = false; + + return { + get: () => state, + auth: () => { + state = true; + }, + }; +} diff --git a/webapp/packages/core-authentication/src/AuthProvidersResource.ts b/webapp/packages/core-authentication/src/AuthProvidersResource.ts index a44ee0e1a4..3eda613f76 100644 --- a/webapp/packages/core-authentication/src/AuthProvidersResource.ts +++ b/webapp/packages/core-authentication/src/AuthProvidersResource.ts @@ -7,28 +7,92 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import { PermissionsResource } from '@cloudbeaver/core-root'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; import { GraphQLService, - CachedDataResource, - AuthProviderInfo + AuthProviderInfo, + CachedMapResource, + ResourceKey, + resourceKeyList, + ResourceKeyUtils } from '@cloudbeaver/core-sdk'; +import { MetadataMap } from '@cloudbeaver/core-utils'; -export type AuthProvider = Omit; +export type AuthProvider = AuthProviderInfo; @injectable() -export class AuthProvidersResource extends CachedDataResource { +export class AuthProvidersResource extends CachedMapResource { + static keyAll = resourceKeyList(['all']); + private loadedKeyMetadata: MetadataMap; + constructor( private graphQLService: GraphQLService, - private permissionsResource: PermissionsResource + private serverConfigResource: ServerConfigResource ) { - super([]); + super(); + this.loadedKeyMetadata = new MetadataMap(() => false); - this.permissionsResource.onDataUpdate.addHandler(() => this.markOutdated()); + this.serverConfigResource.onDataOutdated.addHandler(() => this.markOutdated()); + this.serverConfigResource.onDataUpdate.addHandler(async () => { await this.load(AuthProvidersResource.keyAll); }); + this.addAlias(AuthProvidersResource.keyAll, key => { + if (this.keys.length > 0) { + return resourceKeyList(this.keys); + } + return key; + }); } - protected async loader(): Promise { + has(id: string): boolean { + if (this.loadedKeyMetadata.has(id)) { + return this.loadedKeyMetadata.get(id); + } + + return this.data.has(id); + } + + getEnabledProviders(): AuthProvider[] { + return this.get(resourceKeyList(this.serverConfigResource.enabledAuthProviders)) as AuthProvider[]; + } + + isEnabled(id: string): boolean { + return this.serverConfigResource.enabledAuthProviders.includes(id); + } + + async loadAll(): Promise { + this.resetIncludes(); + await this.load(AuthProvidersResource.keyAll); + + return this.values; + } + + async refreshAll(): Promise { + this.resetIncludes(); + await this.refresh(AuthProvidersResource.keyAll); + return this.values; + } + + refreshAllLazy(): void { + this.resetIncludes(); + this.markOutdated(AuthProvidersResource.keyAll); + this.loadedKeyMetadata.set(AuthProvidersResource.keyAll.list[0], false); + } + + protected async loader(key: ResourceKey): Promise> { const { providers } = await this.graphQLService.sdk.getAuthProviders(); - return providers; + + const all = ResourceKeyUtils.includes(AuthProvidersResource.keyAll, key); + + if (all) { + this.data.clear(); + } + + for (const provider of providers) { + this.data.set(provider.id, provider as AuthProvider); + } + + if (all) { + this.loadedKeyMetadata.set(AuthProvidersResource.keyAll.list[0], true); + } + return this.data; } } diff --git a/webapp/packages/core-authentication/src/UserInfoResource.ts b/webapp/packages/core-authentication/src/UserInfoResource.ts index a2021d013d..34bba9c15b 100644 --- a/webapp/packages/core-authentication/src/UserInfoResource.ts +++ b/webapp/packages/core-authentication/src/UserInfoResource.ts @@ -7,61 +7,82 @@ */ import { injectable } from '@cloudbeaver/core-di'; -import { SessionResource } from '@cloudbeaver/core-root'; -import { CachedDataResource, GraphQLService, UserAuthInfo } from '@cloudbeaver/core-sdk'; +import { SessionDataResource, SessionResource } from '@cloudbeaver/core-root'; +import { CachedDataResource, GraphQLService, UserAuthToken, UserInfo } from '@cloudbeaver/core-sdk'; import { AuthProviderService } from './AuthProviderService'; @injectable() -export class UserInfoResource extends CachedDataResource { +export class UserInfoResource extends CachedDataResource { constructor( private graphQLService: GraphQLService, private authProviderService: AuthProviderService, - private sessionResource: SessionResource + private sessionResource: SessionResource, + private sessionDataResource: SessionDataResource ) { super(null); - this.sessionResource.onDataOutdated.addHandler(() => this.markOutdated()); - this.sessionResource.onDataUpdate.addHandler(async () => { await this.load(); }); + this.sync(this.sessionResource); + this.sessionDataResource.beforeLoad.addHandler(() => this.load()); + this.onDataOutdated.addHandler(() => this.sessionDataResource.markOutdated()); + } + + isLinked(provideId: string): boolean { + return this.data?.linkedAuthProviders.includes(provideId) || false; } getId(): string { return this.data?.userId || 'anonymous'; } - async login(provider: string, credentials: Record): Promise { - if (this.data) { - throw new Error('User already logged in'); + hasToken(type: string, subType?: string): boolean { + if (!this.data) { + return false; } - const processedCredentials = await this.authProviderService.processCredentials(provider, credentials); + // TODO: will be changed due wrong origin in authTokens + return this.data.authTokens.some(token => token.origin.type === (subType ?? type)) + || this.data.authTokens.some(token => token.origin.type === type && token.origin.subType === subType); + } - const { user } = await this.graphQLService.sdk.authLogin({ - provider, - credentials: processedCredentials, + async login(provider: string, credentials: Record, link?: boolean): Promise { + await this.performUpdate(undefined, undefined, async () => { + const processedCredentials = await this.authProviderService.processCredentials(provider, credentials); + + // TODO: will be replaced with another function + const { authToken } = await this.graphQLService.sdk.authLogin({ + provider, + credentials: processedCredentials, + linkUser: link, + customIncludeOriginDetails: true, + }); + + if (this.data === null || link) { + this.data = await this.loader(); + } else { + this.data.authTokens.push(authToken as UserAuthToken); + } }); - this.data = user as UserAuthInfo; + await this.sessionDataResource.refresh(); - await this.updateSession(); return this.data; } async logout(): Promise { - if (this.data) { - await this.graphQLService.sdk.authLogout(); - this.data = null; - await this.updateSession(); - } + await this.performUpdate(undefined, undefined, async () => { + if (this.data) { + await this.graphQLService.sdk.authLogout(); + this.data = null; + } + }); + await this.sessionDataResource.refresh(); } - protected async loader(): Promise { - await this.sessionResource.load(); - const { user } = await this.graphQLService.sdk.getSessionUser(); + protected async loader(): Promise { + const { user } = await this.graphQLService.sdk.getActiveUser({ + customIncludeOriginDetails: true, + }); - return (user as UserAuthInfo | null) ?? null; - } - - private async updateSession() { - await this.sessionResource.refresh(); + return (user as UserInfo | null) ?? null; } } diff --git a/webapp/packages/core-authentication/src/UsersResource.ts b/webapp/packages/core-authentication/src/UsersResource.ts index be9a35c99e..816731e2d0 100644 --- a/webapp/packages/core-authentication/src/UsersResource.ts +++ b/webapp/packages/core-authentication/src/UsersResource.ts @@ -7,6 +7,7 @@ */ import { injectable } from '@cloudbeaver/core-di'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; import { GraphQLService, CachedMapResource, @@ -19,6 +20,7 @@ import { } from '@cloudbeaver/core-sdk'; import { MetadataMap } from '@cloudbeaver/core-utils'; +import { AUTH_PROVIDER_LOCAL_ID } from './AUTH_PROVIDER_LOCAL_ID'; import { AuthInfoService } from './AuthInfoService'; import { AuthProviderService } from './AuthProviderService'; @@ -42,11 +44,13 @@ export class UsersResource extends CachedMapResource; constructor( private graphQLService: GraphQLService, + private serverConfigResource: ServerConfigResource, private authProviderService: AuthProviderService, private authInfoService: AuthInfoService ) { super(); this.loadedKeyMetadata = new MetadataMap(() => false); + this.serverConfigResource.onDataUpdate.addHandler(this.refreshAllLazy.bind(this)); } isNew(id: string): boolean { @@ -72,7 +76,7 @@ export class UsersResource extends CachedMapResource): Promise { - const provider = 'local'; - const processedCredentials = await this.authProviderService.processCredentials(provider, credentials); + const processedCredentials = await this.authProviderService.processCredentials(AUTH_PROVIDER_LOCAL_ID, credentials); await this.graphQLService.sdk.setUserCredentials({ - providerId: provider, + providerId: AUTH_PROVIDER_LOCAL_ID, userId, credentials: processedCredentials, }); @@ -188,8 +191,6 @@ export class UsersResource extends CachedMapResource = function Button({ tag = 'button', disabled = false, loading, + className, ...rest }) { if (loading) { @@ -110,7 +111,7 @@ export const Button: React.FC = function Button({ const Button = tag; return styled(useStyles(buttonStyles, ...(mod || []).map(mod => buttonMod[mod])))( - - - - + {translate('connections_network_handler_ssh_tunnel_host')} + + + {translate('connections_network_handler_ssh_tunnel_port')} + + + + + {translate('connections_network_handler_ssh_tunnel_user')} + + + {translate('connections_network_handler_ssh_tunnel_password')} + + + {credentialsSavingEnabled && ( + {translate('connections_connection_edit_save_credentials')} + + )} + + + + ); diff --git a/webapp/packages/core-connections/src/ConnectionInfoResource.ts b/webapp/packages/core-connections/src/ConnectionInfoResource.ts index 0197656119..8c2e79b403 100644 --- a/webapp/packages/core-connections/src/ConnectionInfoResource.ts +++ b/webapp/packages/core-connections/src/ConnectionInfoResource.ts @@ -8,10 +8,9 @@ import { action, makeObservable } from 'mobx'; -import { AppAuthService } from '@cloudbeaver/core-authentication'; import { injectable } from '@cloudbeaver/core-di'; import { Executor, ExecutorInterrupter, IExecutor } from '@cloudbeaver/core-executor'; -import { SessionResource } from '@cloudbeaver/core-root'; +import { NavigatorViewSettings, SessionDataResource } from '@cloudbeaver/core-root'; import { GraphQLService, CachedMapResource, @@ -27,7 +26,6 @@ import { } from '@cloudbeaver/core-sdk'; import { ConnectionsResource, DatabaseConnection } from './Administration/ConnectionsResource'; -import type { NavigatorViewSettings } from './ConnectionNavigatorViewSettings'; export type Connection = DatabaseConnection & { authProperties?: UserConnectionAuthPropertiesFragment[] }; export type ConnectionInitConfig = Omit; @@ -47,18 +45,21 @@ export const DEFAULT_NAVIGATOR_VIEW_SETTINGS: NavigatorSettingsInput = { export class ConnectionInfoResource extends CachedMapResource { readonly onConnectionCreate: IExecutor; readonly onConnectionClose: IExecutor; + + /** + * @deprecated Should be refactored according new SessionDataResource + */ readonly onSessionUpdate: IExecutor; private sessionUpdate: boolean; constructor( private graphQLService: GraphQLService, private connectionsResource: ConnectionsResource, - appAuthService: AppAuthService, - sessionResource: SessionResource + sessionDataResource: SessionDataResource ) { super(); makeObservable(this, { - refreshSession: action, + refreshUserConnections: action, createFromTemplate: action, createConnection: action, createFromNode: action, @@ -76,11 +77,12 @@ export class ConnectionInfoResource extends CachedMapResource this.sessionUpdate)); this.onItemDelete.addHandler(ExecutorInterrupter.interrupter(() => this.sessionUpdate)); this.onConnectionCreate.addHandler(ExecutorInterrupter.interrupter(() => this.sessionUpdate)); - sessionResource.onDataOutdated.addHandler(() => this.markOutdated()); - appAuthService.auth.addHandler(() => this.refreshSession(true)); + + sessionDataResource.onDataOutdated.addHandler(() => this.markOutdated()); + sessionDataResource.onDataUpdate.addHandler(() => this.refreshUserConnections(true)); } - async refreshSession(sessionUpdate?: boolean): Promise { + async refreshUserConnections(sessionUpdate?: boolean): Promise { this.sessionUpdate = sessionUpdate === true; try { const connectionsList = resourceKeyList(Array.from(this.data.keys())); @@ -118,9 +120,10 @@ export class ConnectionInfoResource extends CachedMapResource { + async createFromTemplate(templateId: string, connectionName: string): Promise { const { connection } = await this.graphQLService.sdk.createConnectionFromTemplate({ templateId, + connectionName, ...this.getDefaultIncludes(), ...this.getIncludesMap(), }); @@ -259,7 +262,7 @@ export class ConnectionInfoResource extends CachedMapResource { const context = await this.connectionExecutor.execute(connectionId); + const connection = context.getContext(this.connectionContext); - return (await context.getContext(this.connectionContext)).connection; + return connection.connection; } - async addOpenedConnection(connection: Connection) { + async addOpenedConnection(connection: Connection): Promise { this.addConnection(connection); } @@ -62,7 +63,7 @@ export class ConnectionsManagerService { ); } - async deleteConnection(id: string) { + async deleteConnection(id: string): Promise { const connection = await this.connectionInfo.load(id); if (!connection.features.includes(EConnectionFeature.manageable)) { @@ -96,7 +97,8 @@ export class ConnectionsManagerService { } private async connectionDialog(connectionId: string | null, context: IExecutionContextProvider) { - const connection = await context.getContext(this.connectionContext); + const connection = context.getContext(this.connectionContext); + if (!connectionId) { if (!this.hasAnyConnection()) { return; @@ -105,13 +107,9 @@ export class ConnectionsManagerService { } try { - if (!this.connectionInfo.has(connectionId)) { - return; - } - const tempConnection = await this.connectionAuthService.auth(connectionId); - if (!tempConnection.connected) { + if (!tempConnection?.connected) { return; } connection.connection = tempConnection; diff --git a/webapp/packages/core-connections/src/ContainerResource.ts b/webapp/packages/core-connections/src/ContainerResource.ts index c6c8658f4a..185e36c47e 100644 --- a/webapp/packages/core-connections/src/ContainerResource.ts +++ b/webapp/packages/core-connections/src/ContainerResource.ts @@ -44,7 +44,13 @@ string private connectionInfoResource: ConnectionInfoResource ) { super(new MetadataMap(() => ({ }))); - this.metadata = new MetadataMap(() => ({ outdated: true, loading: false, outdatedData: [], loadingData: [] })); + this.metadata = new MetadataMap(() => ({ + outdated: true, + loading: false, + exception: null, + outdatedData: [], + loadingData: [], + })); this.connectionInfoResource.onItemDelete.addHandler( key => ResourceKeyUtils.forEach(key, key => this.data.delete(key)) ); @@ -92,19 +98,20 @@ string metadata.loadingData = metadata.loadingData.filter(id => id !== catalogId); } - markOutdated(param: ObjectContainerParams): void { + async markOutdated(param: ObjectContainerParams): Promise { const catalogId = param.catalogId ?? defaultCatalog; const metadata = this.metadata.get(param.connectionId); if (!metadata.outdatedData.includes(catalogId)) { metadata.outdatedData.push(catalogId); } - this.onDataOutdated.execute(param); + await this.onDataOutdated.execute(param); } markUpdated(param: ObjectContainerParams): void { const metadata = this.metadata.get(param.connectionId); const catalogId = param.catalogId ?? defaultCatalog; + metadata.exception = null; metadata.outdatedData = metadata.outdatedData.filter(id => id !== catalogId); } diff --git a/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx b/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx index 8ae53df07a..f4d851298a 100644 --- a/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx +++ b/webapp/packages/core-connections/src/DatabaseAuthDialog/DatabaseAuthDialog.tsx @@ -124,7 +124,7 @@ export const DatabaseAuthDialog = observer(function DatabaseAuthDialog({ diff --git a/webapp/packages/core-connections/src/DatabaseAuthDialog/SSHAuthForm.tsx b/webapp/packages/core-connections/src/DatabaseAuthDialog/SSHAuthForm.tsx index 9adc3657ff..72c9ecfb56 100644 --- a/webapp/packages/core-connections/src/DatabaseAuthDialog/SSHAuthForm.tsx +++ b/webapp/packages/core-connections/src/DatabaseAuthDialog/SSHAuthForm.tsx @@ -79,7 +79,7 @@ export const SSHAuthForm: React.FC = observer(function SSHAuthForm({ name="savePassword" value={sshHandlerId + ' savePassword'} state={state} - checkboxLabel={translate('connections_network_handler_ssh_tunnel_save_password')} + label={translate('connections_connection_edit_save_credentials')} disabled={disabled} /> diff --git a/webapp/packages/core-connections/src/index.ts b/webapp/packages/core-connections/src/index.ts index 59b51d44b2..22a5209671 100644 --- a/webapp/packages/core-connections/src/index.ts +++ b/webapp/packages/core-connections/src/index.ts @@ -32,4 +32,3 @@ export * from './NetworkHandlerResource'; export * from './useConnectionInfo'; export * from './useDBDriver'; export * from './getUniqueConnectionName'; -export * from './ConnectionNavigatorViewSettings'; diff --git a/webapp/packages/core-connections/src/locales/en.ts b/webapp/packages/core-connections/src/locales/en.ts index 842f3aa5fb..e06144e497 100644 --- a/webapp/packages/core-connections/src/locales/en.ts +++ b/webapp/packages/core-connections/src/locales/en.ts @@ -42,7 +42,6 @@ export default [ ['connections_network_handler_ssh_tunnel_port', 'Port'], ['connections_network_handler_ssh_tunnel_user', 'User'], ['connections_network_handler_ssh_tunnel_password', 'Password'], - ['connections_network_handler_ssh_tunnel_save_password', 'Save Password'], ['connections_driver_search_placeholder', 'Type driver name...'], ['connections_not_found', 'No database connections were found'], ]; diff --git a/webapp/packages/core-connections/src/locales/ru.ts b/webapp/packages/core-connections/src/locales/ru.ts index 0447b919b2..4a772b878c 100644 --- a/webapp/packages/core-connections/src/locales/ru.ts +++ b/webapp/packages/core-connections/src/locales/ru.ts @@ -41,7 +41,6 @@ export default [ ['connections_network_handler_ssh_tunnel_port', 'Порт'], ['connections_network_handler_ssh_tunnel_user', 'Пользователь'], ['connections_network_handler_ssh_tunnel_password', 'Пароль'], - ['connections_network_handler_ssh_tunnel_save_password', 'Сохранить пароль'], ['connections_driver_search_placeholder', 'Введите название драйвера...'], ['connections_not_found', 'Подключения к базам данных не найдены'], ]; diff --git a/webapp/packages/core-di/src/App.ts b/webapp/packages/core-di/src/App.ts index 7433c9e133..250ed1145e 100644 --- a/webapp/packages/core-di/src/App.ts +++ b/webapp/packages/core-di/src/App.ts @@ -7,6 +7,7 @@ */ import { Bootstrap } from './Bootstrap'; +import { Dependency } from './Dependency'; import type { DIContainer } from './DIContainer'; import { RootContainerService } from './entities/RootContainerService'; import type { IServiceCollection, IServiceInjector } from './IApp'; @@ -44,9 +45,6 @@ export class App { // first phase register all dependencies registerServices(): void { for (const plugin of this.plugins) { - if (plugin.registerServices) { - plugin.registerServices(this.getServiceCollection()); - } if (plugin.providers?.length) { plugin.providers.forEach(provider => { // console.log('provider', provider.name); @@ -65,6 +63,8 @@ export class App { if ('register' in serviceInstance) { await serviceInstance.register(); } + } else if (service.prototype instanceof Dependency) { + this.diWrapper.injector.getServiceByClass(service); } } } @@ -83,23 +83,4 @@ export class App { } } } - - // second phase - run init scripts todo run it based on dependency tree - async initializePlugins(): Promise { - for (const plugin of this.plugins) { - if (plugin.initialize) { - await plugin.initialize(this.getServiceInjector()); - } - } - } - - // third initialization phase? (never called) - async load(): Promise { - for (const plugin of this.plugins) { - if (plugin.load) { - // todo run it based on dependency tree - await plugin.load(); - } - } - } } diff --git a/webapp/packages/core-di/src/Dependency.ts b/webapp/packages/core-di/src/Dependency.ts new file mode 100644 index 0000000000..e95f7fc2ff --- /dev/null +++ b/webapp/packages/core-di/src/Dependency.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 './injectable'; + +@injectable() +export abstract class Dependency { +} diff --git a/webapp/packages/core-di/src/PluginManifest.ts b/webapp/packages/core-di/src/PluginManifest.ts index b399026023..145a158c3a 100644 --- a/webapp/packages/core-di/src/PluginManifest.ts +++ b/webapp/packages/core-di/src/PluginManifest.ts @@ -6,9 +6,7 @@ * you may not use this file except in compliance with the License. */ -import type { - IServiceConstructor, IServiceCollection, IServiceInjector -} from './IApp'; +import type { IServiceConstructor } from './IApp'; export interface PluginManifest { info: { @@ -16,24 +14,8 @@ export interface PluginManifest { defaultSettings?: Record; }; - /** - * First phase, only register services in DI here - */ - registerServices?: (services: IServiceCollection) => void; - providers: Array>; - /** - * Second phase. - * You can be sure that all services of all plugins are already registered here and you can use any service - */ - initialize?: (services: IServiceInjector) => Promise | void; - - /** - * Third phase but we still don't know what is it :) - */ - load?: () => Promise | void; - /** * The list of plugins which your plugin depends on */ diff --git a/webapp/packages/core-di/src/index.ts b/webapp/packages/core-di/src/index.ts index af0b07fbd7..94c7ec9e62 100644 --- a/webapp/packages/core-di/src/index.ts +++ b/webapp/packages/core-di/src/index.ts @@ -2,6 +2,7 @@ export * from './IApp'; export * from './App'; export * from './AppContext'; export * from './Bootstrap'; +export * from './Dependency'; export * from './injectable'; export * from './PluginManifest'; export * from './useService'; diff --git a/webapp/packages/core-dialogs/src/CommonDialog/CommonDialogService.ts b/webapp/packages/core-dialogs/src/CommonDialog/CommonDialogService.ts index 22732aec9c..c9f8528602 100644 --- a/webapp/packages/core-dialogs/src/CommonDialog/CommonDialogService.ts +++ b/webapp/packages/core-dialogs/src/CommonDialog/CommonDialogService.ts @@ -40,7 +40,7 @@ export interface DialogInternal { @injectable() export class CommonDialogService { - dialogs: DialogInternal[] = observable([]); + dialogs: DialogInternal[] = observable([], { deep: false }); // note that if dialog is closed by user it will be resolved with DialogueStateResult.Rejected async open( diff --git a/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuPanelItem.tsx b/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuPanelItem.tsx index bdf8f57420..747ef69949 100644 --- a/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuPanelItem.tsx +++ b/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuPanelItem.tsx @@ -12,14 +12,14 @@ import styled from 'reshadow'; import { Checkbox, Icon, IconOrImage, Radio } from '@cloudbeaver/core-blocks'; import type { IMenuItem } from '@cloudbeaver/core-dialogs'; import { Translate } from '@cloudbeaver/core-localization'; -import { useStyles, Style } from '@cloudbeaver/core-theming'; +import { useStyles, ComponentStyle } from '@cloudbeaver/core-theming'; import { use } from '@reshadow/react'; import { menuPanelStyles } from './menuPanelStyles'; interface MenuPanelItemProps { menuItem: IMenuItem; - style?: Style[]; + style?: ComponentStyle; } export const MenuPanelItem: React.FC = function MenuPanelItem({ @@ -40,10 +40,10 @@ export const MenuPanelItem: React.FC = function MenuPanelIte if (controlType === 'radio') { control = ; } else if (controlType === 'checkbox') { - control = ; + control = ; } - return styled(useStyles(menuPanelStyles, ...style))( + return styled(useStyles(menuPanelStyles, style))( {icon ? ( diff --git a/webapp/packages/core-events/src/ExceptionsCatcherService.ts b/webapp/packages/core-events/src/ExceptionsCatcherService.ts index ce626adabe..ea0771126f 100644 --- a/webapp/packages/core-events/src/ExceptionsCatcherService.ts +++ b/webapp/packages/core-events/src/ExceptionsCatcherService.ts @@ -6,24 +6,28 @@ * you may not use this file except in compliance with the License. */ -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { NotificationService } from './NotificationService'; @injectable() -export class ExceptionsCatcherService { +export class ExceptionsCatcherService extends Bootstrap { baseCatcher: OnErrorEventHandler | null = null; private ignored: string[] = []; private messageTitle = 'Uncatched exception'; - constructor(private notificationService: NotificationService) { } + constructor(private notificationService: NotificationService) { + super(); + } - subscribe() { + register(): void { this.baseCatcher = window.onerror; window.onerror = this.catcher; } + load(): void {} + unsubscribe() { window.onerror = this.baseCatcher; } diff --git a/webapp/packages/core-executor/src/Executor.ts b/webapp/packages/core-executor/src/Executor.ts index 72feb4182a..cec5e13c83 100644 --- a/webapp/packages/core-executor/src/Executor.ts +++ b/webapp/packages/core-executor/src/Executor.ts @@ -26,12 +26,12 @@ export class Executor implements IExecutor { this.collection = new ExecutorHandlersCollection(); } - before(executor: IExecutor, map?: (data: T) => TNext): this { + before(executor: IExecutor, map?: (data: T) => TNext): this { this.collection.before(executor, map); return this; } - next(executor: IExecutor, map?: (data: T) => TNext): this { + next(executor: IExecutor, map?: (data: T) => TNext): this { this.collection.next(executor, map); return this; } diff --git a/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts b/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts index dd8553c1c2..9800370666 100644 --- a/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts +++ b/webapp/packages/core-executor/src/ExecutorHandlersCollection.ts @@ -38,7 +38,7 @@ export class ExecutorHandlersCollection implements IExecutorHandler return this.links.get(link) || scoped?.getLinkHandlers(link, scoped.getLinkHandlers(link)); } - before(executor: IExecutor, map?: (data: T) => TNext): this { + before(executor: IExecutor, map?: (data: T) => TNext): this { this.chain.push({ executor, map, @@ -47,7 +47,7 @@ export class ExecutorHandlersCollection implements IExecutorHandler return this; } - next(executor: IExecutor, map?: (data: T) => TNext): this { + next(executor: IExecutor, map?: (data: T) => TNext): this { this.chain.push({ executor, map, diff --git a/webapp/packages/core-executor/src/ExecutorInterrupter.ts b/webapp/packages/core-executor/src/ExecutorInterrupter.ts index fba33286ee..704fe21d92 100644 --- a/webapp/packages/core-executor/src/ExecutorInterrupter.ts +++ b/webapp/packages/core-executor/src/ExecutorInterrupter.ts @@ -14,6 +14,12 @@ export interface IExecutorInterrupter { } export const ExecutorInterrupter = { + isInterrupted(contexts: IExecutionContextProvider): boolean { + const interrupt = contexts.getContext(ExecutorInterrupter.interruptContext); + + return interrupt.interrupted; + }, + interrupt(contexts: IExecutionContextProvider): void { const interrupt = contexts.getContext(ExecutorInterrupter.interruptContext); @@ -29,11 +35,11 @@ export const ExecutorInterrupter = { }; }, - interrupter(flag: () => boolean) { - return (data: any, contexts: IExecutionContextProvider): void => { + interrupter(flag: () => Promise | boolean) { + return async (data: any, contexts: IExecutionContextProvider): Promise => { const interrupt = contexts.getContext(ExecutorInterrupter.interruptContext); - if (flag()) { + if ((await flag())) { interrupt.interrupt(); } }; diff --git a/webapp/packages/core-executor/src/IExecutionContext.ts b/webapp/packages/core-executor/src/IExecutionContext.ts index 97963cdc0e..06e6dc06d9 100644 --- a/webapp/packages/core-executor/src/IExecutionContext.ts +++ b/webapp/packages/core-executor/src/IExecutionContext.ts @@ -12,7 +12,7 @@ export interface IContextGetter { } export interface IExecutionContextProvider { - hasContext: (token: IContextLoader) => boolean; + hasContext: (token: IContextLoader) => boolean; getContext: IContextGetter; } diff --git a/webapp/packages/core-executor/src/IExecutor.ts b/webapp/packages/core-executor/src/IExecutor.ts index d5068df5b7..ad575943b8 100644 --- a/webapp/packages/core-executor/src/IExecutor.ts +++ b/webapp/packages/core-executor/src/IExecutor.ts @@ -11,8 +11,8 @@ import type { IExecutorHandler } from './IExecutorHandler'; import type { IExecutorHandlersCollection } from './IExecutorHandlersCollection'; export interface IExecutor { - before: (executor: IExecutor, map?: (data: T) => TNext) => this; - next: (executor: IExecutor, map?: (data: T) => TNext) => this; + before: (executor: IExecutor, map?: (data: T) => TNext) => this; + next: (executor: IExecutor, map?: (data: T) => TNext) => this; execute: ( data: T, context?: IExecutionContext, diff --git a/webapp/packages/core-executor/src/IExecutorHandler.ts b/webapp/packages/core-executor/src/IExecutorHandler.ts index 8d3dc7b8c2..a6af14a878 100644 --- a/webapp/packages/core-executor/src/IExecutorHandler.ts +++ b/webapp/packages/core-executor/src/IExecutorHandler.ts @@ -11,4 +11,4 @@ import type { IExecutionContextProvider } from './IExecutionContext'; export type IExecutorHandler = ( data: T, contexts: IExecutionContextProvider -) => void | Promise; +) => any | Promise; diff --git a/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts b/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts index edbc79473a..61d3010747 100644 --- a/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts +++ b/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts @@ -23,8 +23,8 @@ export interface IExecutorHandlersCollection { readonly postHandlers: Array>; readonly chain: Array>; - before: (executor: IExecutor, map?: (data: T) => TNext) => this; - next: (executor: IExecutor, map?: (data: T) => TNext) => this; + before: (executor: IExecutor, map?: (data: T) => TNext) => this; + next: (executor: IExecutor, map?: (data: T) => TNext) => this; execute: ( data: T, context: IExecutionContext, diff --git a/webapp/packages/core-executor/src/TaskScheduler/TaskScheduler.ts b/webapp/packages/core-executor/src/TaskScheduler/TaskScheduler.ts index 177eef0bd6..a9b860db49 100644 --- a/webapp/packages/core-executor/src/TaskScheduler/TaskScheduler.ts +++ b/webapp/packages/core-executor/src/TaskScheduler/TaskScheduler.ts @@ -12,11 +12,17 @@ import type { ITask } from './ITask'; export type BlockedExecution = (active: T, current: T) => boolean; +const queueLimit = 100; + export class TaskScheduler { get activeList(): TIdentifier[] { return this.queue.map(task => task.id); } + get executing(): boolean { + return this.queue.length > 0; + } + private readonly queue: Array>; private readonly isBlocked: BlockedExecution | null; @@ -34,19 +40,28 @@ export class TaskScheduler { async schedule( id: TIdentifier, promise: () => Promise, - after?: () => Promise | void, + after?: () => Promise | any, + success?: () => Promise | any, + error?: (exception: Error) => Promise | any, ): Promise { const task: ITask = { id, task: this.scheduler(id, promise), }; + if (this.queue.length > queueLimit) { + throw new Error('Execution queue limit is reached'); + } this.queue.push(task); try { - return await task.task; + const value = await task.task; + await success?.(); + return value; + } catch (exception) { + await error?.(exception); + throw exception; } finally { - this.queue.splice(this.queue.indexOf(task), 1); await after?.(); } } @@ -61,19 +76,26 @@ export class TaskScheduler { } } - private async scheduler(id: TIdentifier, promise: () => Promise) { - if (!this.isBlocked) { - return promise(); + private async scheduler( + id: TIdentifier, + promise: () => Promise, + ) { + try { + if (!this.isBlocked) { + return await promise(); + } + + const queueList = this.queue.filter(active => this.isBlocked!(active.id, id)); + + for (const task of queueList) { + try { + await task.task; + } catch {} + } + + return await promise(); + } finally { + this.queue.splice(this.queue.findIndex(task => task.id === id), 1); } - - const queueList = this.queue.filter(active => this.isBlocked!(active.id, id)); - - for (const task of queueList) { - try { - await task.task; - } catch {} - } - - return promise(); } } diff --git a/webapp/packages/core-executor/src/index.ts b/webapp/packages/core-executor/src/index.ts index f246f6ca14..230527d799 100644 --- a/webapp/packages/core-executor/src/index.ts +++ b/webapp/packages/core-executor/src/index.ts @@ -8,4 +8,3 @@ export * from './IExecutionContext'; export * from './IExecutor'; export * from './IExecutorHandler'; export * from './IExecutorHandlersCollection'; -export * from './useFormValidator'; diff --git a/webapp/packages/core-executor/src/useFormValidator.ts b/webapp/packages/core-executor/src/useFormValidator.ts deleted file mode 100644 index 5a12697db0..0000000000 --- a/webapp/packages/core-executor/src/useFormValidator.ts +++ /dev/null @@ -1,44 +0,0 @@ - -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2021 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 { useEffect, useRef } from 'react'; - -import type { IExecutor } from './IExecutor'; -import type { IExecutorHandlersCollection } from './IExecutorHandlersCollection'; - -export function useFormValidator( - validationTask: IExecutor | IExecutorHandlersCollection, - ref: React.RefObject, - callback?: () => void -): void { - const callbackRef = useRef({ - callback, - ref, - }); - callbackRef.current.callback = callback; - callbackRef.current.ref = ref; - - useEffect(() => { - if (!validationTask) { - return; - } - - function validate() { - if (callbackRef.current) { - callbackRef.current.ref.current?.checkValidity(); - callbackRef.current.ref.current?.reportValidity(); - - callbackRef.current.callback?.(); - } - } - validationTask.addHandler(validate); - - return () => validationTask.removeHandler(validate); - }, [validationTask]); -} diff --git a/webapp/packages/core-connections/src/ConnectionNavigatorViewSettings.ts b/webapp/packages/core-root/src/ConnectionNavigatorViewSettings.ts similarity index 100% rename from webapp/packages/core-connections/src/ConnectionNavigatorViewSettings.ts rename to webapp/packages/core-root/src/ConnectionNavigatorViewSettings.ts diff --git a/webapp/packages/core-root/src/PermissionsResource.ts b/webapp/packages/core-root/src/PermissionsResource.ts index 1da86f6602..6d8875d754 100644 --- a/webapp/packages/core-root/src/PermissionsResource.ts +++ b/webapp/packages/core-root/src/PermissionsResource.ts @@ -9,27 +9,29 @@ import { injectable } from '@cloudbeaver/core-di'; import { GraphQLService, CachedDataResource } from '@cloudbeaver/core-sdk'; -import { SessionResource } from './SessionResource'; +import { SessionDataResource } from './SessionDataResource'; @injectable() export class PermissionsResource extends CachedDataResource, void> { constructor( private graphQLService: GraphQLService, - private sessionResource: SessionResource + sessionDataResource: SessionDataResource ) { super(new Set()); - this.sessionResource.onDataOutdated.addHandler(this.markOutdated.bind(this)); - this.sessionResource.onDataUpdate.addHandler(() => { this.load(); }); + this.sync(sessionDataResource); } has(id: string): boolean { return this.data.has(id); } - protected async loader(): Promise> { - await this.sessionResource.load(); + async hasAsync(id: string): Promise { + await this.load(); + return this.has(id); + } + protected async loader(): Promise> { const { permissions } = await this.graphQLService.sdk.sessionPermissions(); this.data.clear(); diff --git a/webapp/packages/core-root/src/PermissionsService.ts b/webapp/packages/core-root/src/PermissionsService.ts index 5a89188cc1..abae2d4d1d 100644 --- a/webapp/packages/core-root/src/PermissionsService.ts +++ b/webapp/packages/core-root/src/PermissionsService.ts @@ -26,8 +26,7 @@ export class PermissionsService { } async hasAsync(id: string): Promise { - await this.permissions.load(); - return this.has(id); + return this.permissions.hasAsync(id); } async update(): Promise { diff --git a/webapp/packages/core-root/src/ServerConfigResource.ts b/webapp/packages/core-root/src/ServerConfigResource.ts index 3e9015c68b..ea133fb7b7 100644 --- a/webapp/packages/core-root/src/ServerConfigResource.ts +++ b/webapp/packages/core-root/src/ServerConfigResource.ts @@ -6,24 +6,141 @@ * you may not use this file except in compliance with the License. */ +import { makeObservable, observable } from 'mobx'; + import { injectable } from '@cloudbeaver/core-di'; -import { GraphQLService, CachedDataResource, ServerConfig } from '@cloudbeaver/core-sdk'; +import { GraphQLService, CachedDataResource, ServerConfig, ServerConfigInput, NavigatorSettingsInput } from '@cloudbeaver/core-sdk'; + +import { isNavigatorViewSettingsEqual } from './ConnectionNavigatorViewSettings'; @injectable() export class ServerConfigResource extends CachedDataResource { + update: ServerConfigInput; + constructor( private graphQLService: GraphQLService ) { super(null); + makeObservable(this, { + update: observable, + }); + + this.update = {}; } - async update(): Promise { - await this.refresh(); + get serverVersion(): string { + return this.data?.version || ''; + } + + get workspaceId(): string { + return this.data?.workspaceId || ''; + } + + get configurationMode(): boolean { + return !!this.data?.configurationMode; + } + + get publicDisabled(): boolean { + if (this.data?.configurationMode + || (this.data?.licenseRequired && !this.data?.licenseValid)) { + return true; + } + + return false; + } + + get enabledAuthProviders(): string[] { + return this.update.enabledAuthProviders || this.data?.enabledAuthProviders || []; + } + + get credentialsSaveEnabled(): boolean { + return this.update.adminCredentialsSaveEnabled ?? this.data?.adminCredentialsSaveEnabled ?? false; + } + + get userCredentialsSaveEnabled(): boolean { + return this.update.publicCredentialsSaveEnabled ?? this.data?.publicCredentialsSaveEnabled ?? false; + } + + isChanged(): boolean { + if (!this.data) { + return false; + } + + if (this.update.adminName || this.update.adminPassword) { + return true; + } + + return ( + this.update.serverName !== this.data.name + || this.update.sessionExpireTime !== this.data.sessionExpireTime + + || this.update.anonymousAccessEnabled !== this.data.anonymousAccessEnabled + || this.update.authenticationEnabled !== this.data.authenticationEnabled + + || this.update.adminCredentialsSaveEnabled !== this.data.adminCredentialsSaveEnabled + || this.update.publicCredentialsSaveEnabled !== this.data.publicCredentialsSaveEnabled + + || this.update.customConnectionsEnabled !== this.data.supportsCustomConnections + || this.update.enabledAuthProviders !== this.data.enabledAuthProviders + ); + } + + isNavigatorSettingsChanged(settings: NavigatorSettingsInput): boolean { + if (!this.data?.defaultNavigatorSettings) { + return false; + } + + return !isNavigatorViewSettingsEqual(this.data.defaultNavigatorSettings, settings); + } + + setDataUpdate(update: ServerConfigInput): void { + this.update = update; + } + + async setDefaultNavigatorSettings(settings: NavigatorSettingsInput): Promise { + await this.performUpdate(undefined, undefined, async () => { + await this.graphQLService.sdk.setDefaultNavigatorSettings({ settings }); + + if (this.data) { + this.data.defaultNavigatorSettings = { ...settings }; + } else { + this.data = await this.loader(); + } + }, () => !this.isNavigatorSettingsChanged(settings)); + } + + async save(onlyRestart = false): Promise { + await this.performUpdate(undefined, undefined, async () => { + await this.graphQLService.sdk.configureServer({ + configuration: onlyRestart ? {} : this.update, + }); + + this.data = await this.loader(); + }, () => !this.isChanged() && !onlyRestart); } protected async loader(): Promise { const { serverConfig } = await this.graphQLService.sdk.serverConfig(); + this.syncUpdateData(serverConfig); + return serverConfig as ServerConfig; } + + private syncUpdateData(serverConfig: ServerConfig) { + this.update.serverName = serverConfig.name; + this.update.sessionExpireTime = serverConfig.sessionExpireTime; + + this.update.adminName = undefined; + this.update.adminPassword = undefined; + + this.update.anonymousAccessEnabled = serverConfig.anonymousAccessEnabled; + this.update.authenticationEnabled = serverConfig.authenticationEnabled; + + this.update.adminCredentialsSaveEnabled = serverConfig.adminCredentialsSaveEnabled; + this.update.publicCredentialsSaveEnabled = serverConfig.publicCredentialsSaveEnabled; + + this.update.customConnectionsEnabled = serverConfig.supportsCustomConnections; + this.update.enabledAuthProviders = serverConfig.enabledAuthProviders; + } } diff --git a/webapp/packages/core-root/src/SessionDataResource.ts b/webapp/packages/core-root/src/SessionDataResource.ts new file mode 100644 index 0000000000..2c43f7bacd --- /dev/null +++ b/webapp/packages/core-root/src/SessionDataResource.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { CachedDataResource } from '@cloudbeaver/core-sdk'; + +import { SessionResource } from './SessionResource'; + +@injectable() +export class SessionDataResource extends CachedDataResource { + constructor( + sessionResource: SessionResource + ) { + super(null); + + this.sync(sessionResource); + } + + protected async loader(): Promise { + return null; + } +} diff --git a/webapp/packages/core-root/src/SessionError.ts b/webapp/packages/core-root/src/SessionError.ts index 08303c1288..ba38f84209 100644 --- a/webapp/packages/core-root/src/SessionError.ts +++ b/webapp/packages/core-root/src/SessionError.ts @@ -1,3 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 class SessionError extends Error { constructor(message: string) { super(message); diff --git a/webapp/packages/core-root/src/SessionExpireService.ts b/webapp/packages/core-root/src/SessionExpireService.ts index 29d88aa9d9..f74fc3f2dc 100644 --- a/webapp/packages/core-root/src/SessionExpireService.ts +++ b/webapp/packages/core-root/src/SessionExpireService.ts @@ -8,7 +8,7 @@ import { Subject } from 'rxjs'; -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { GQLError, GraphQLService, EServerErrorCode } from '@cloudbeaver/core-sdk'; @@ -16,19 +16,22 @@ import { import { SessionError } from './SessionError'; @injectable() -export class SessionExpireService { +export class SessionExpireService extends Bootstrap { private isNotifiedAboutExpiredSession = false; onSessionExpire = new Subject(); constructor( private graphQLService: GraphQLService ) { + super(); } - subscribe(): void { + register(): void { this.graphQLService.registerInterceptor(this.sessionExpiredInterceptor.bind(this)); } + load(): void {} + private async sessionExpiredInterceptor(request: Promise): Promise { try { return await request; diff --git a/webapp/packages/core-root/src/SessionResource.ts b/webapp/packages/core-root/src/SessionResource.ts index 9dd964553b..531e9d4f71 100644 --- a/webapp/packages/core-root/src/SessionResource.ts +++ b/webapp/packages/core-root/src/SessionResource.ts @@ -21,21 +21,14 @@ export type SessionState = SessionStateFragment; export class SessionResource extends CachedDataResource { constructor( private graphQLService: GraphQLService, - private serverConfiguration: ServerConfigResource + serverConfiguration: ServerConfigResource ) { super(null); - this.serverConfiguration.onDataOutdated.addHandler(this.markOutdated.bind(this)); - this.serverConfiguration.onDataUpdate.addHandler(() => { this.load(); }); - } - - async update(): Promise { - await this.refresh(); + this.sync(serverConfiguration); } protected async loader(): Promise { - await this.serverConfiguration.load(); - const { session } = await this.graphQLService.sdk.openSession(); return session; diff --git a/webapp/packages/core-root/src/SessionSettingsService.ts b/webapp/packages/core-root/src/SessionSettingsService.ts index fc2c3f06df..9776fe48f0 100644 --- a/webapp/packages/core-root/src/SessionSettingsService.ts +++ b/webapp/packages/core-root/src/SessionSettingsService.ts @@ -19,7 +19,7 @@ export class SessionSettingsService extends SettingsSource { } } - setValue(key: string, value: string) { + setValue(key: string, value: string): void { super.setValue(key, value); localStorage.setItem(this.localStorageKey, JSON.stringify(this.store)); } diff --git a/webapp/packages/core-root/src/index.ts b/webapp/packages/core-root/src/index.ts index 3948e481d7..54c1c80c4f 100644 --- a/webapp/packages/core-root/src/index.ts +++ b/webapp/packages/core-root/src/index.ts @@ -1,10 +1,12 @@ +export * from './ConnectionNavigatorViewSettings'; export * from './PermissionsResource'; export * from './PermissionsService'; export * from './ServerConfigResource'; export * from './SessionService'; export * from './SessionResource'; export * from './ServerService'; -export * from './usePermission'; export * from './ServerSettingsService'; +export * from './SessionDataResource'; export * from './SessionSettingsService'; export * from './SessionExpireService'; +export * from './usePermission'; diff --git a/webapp/packages/core-root/src/usePermission.ts b/webapp/packages/core-root/src/usePermission.ts index 72ca28f07a..dd6a2376a5 100644 --- a/webapp/packages/core-root/src/usePermission.ts +++ b/webapp/packages/core-root/src/usePermission.ts @@ -10,7 +10,7 @@ import { useService } from '@cloudbeaver/core-di'; import { PermissionsService } from './PermissionsService'; -export function usePermission(key: string) { +export function usePermission(key: string): boolean { const permissionsService = useService(PermissionsService); return permissionsService.has(key); diff --git a/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts b/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts index b0b2da2496..4b24063928 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedDataResource.ts @@ -35,6 +35,7 @@ export abstract class CachedDataResource< markUpdated(param: TParam): void { const metadata = this.metadata.get(param as unknown as TKey); metadata.outdated = false; + metadata.exception = null; this.loaded = true; } diff --git a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts index fa33e01f91..ae307c3ffc 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedMapResource.ts @@ -72,6 +72,7 @@ export abstract class CachedMapResource< this.metadata = new MetadataMap(() => ({ outdated: true, loading: false, + exception: null, includes: [...this.defaultIncludes], })); @@ -82,6 +83,7 @@ export abstract class CachedMapResource< } isIncludes(key: ResourceKey, includes: CachedResourceIncludeArgs): boolean { + key = this.transformParam(key); return ResourceKeyUtils.every(key, key => { const metadata = this.metadata.get(key); @@ -90,14 +92,17 @@ export abstract class CachedMapResource< } isOutdated(key: ResourceKey): boolean { + key = this.transformParam(key); return ResourceKeyUtils.some(key, key => this.metadata.get(key).outdated); } isDataLoading(key: ResourceKey): boolean { + key = this.transformParam(key); return ResourceKeyUtils.some(key, key => this.metadata.get(key).loading); } markDataLoading(key: ResourceKey, includes?: string[]): void { + key = this.transformParam(key); ResourceKeyUtils.forEach(key, key => { const metadata = this.metadata.get(key); metadata.loading = true; @@ -105,6 +110,8 @@ export abstract class CachedMapResource< } markDataLoaded(key: ResourceKey, includes?: string[]): void { + key = this.transformParam(key); + if (includes) { this.commitIncludes(key, includes); } @@ -115,18 +122,32 @@ export abstract class CachedMapResource< }); } - markOutdated(): void - markOutdated(key: ResourceKey): void - markOutdated(key?: ResourceKey): void { + async markDataError(exception: Error, key: ResourceKey): Promise { + key = this.transformParam(key); + + ResourceKeyUtils.forEach(key, key => { + const metadata = this.metadata.get(key); + metadata.exception = exception; + }); + + await this.onDataError.execute({ param: key, exception }); + } + + markOutdated(): Promise + markOutdated(key: ResourceKey): Promise + async markOutdated(key?: ResourceKey): Promise { if (!key) { key = resourceKeyList(Array.from(this.data.keys())); + } else { + key = this.transformParam(key); } ResourceKeyUtils.forEach(key, key => { const metadata = this.metadata.get(key); metadata.outdated = true; }); - this.onDataOutdated.execute(key); + + await this.onDataOutdated.execute(key); } markUpdated(): void @@ -134,15 +155,19 @@ export abstract class CachedMapResource< markUpdated(key?: ResourceKey): void { if (!key) { key = resourceKeyList(Array.from(this.data.keys())); + } else { + key = this.transformParam(key); } ResourceKeyUtils.forEach(key, key => { const metadata = this.metadata.get(key); metadata.outdated = false; + metadata.exception = null; }); } isLoaded(key: ResourceKey, includes?: CachedResourceIncludeArgs): boolean { + key = this.transformParam(key); return ResourceKeyUtils.every(key, key => { if (!this.has(key)) { return false; @@ -163,12 +188,14 @@ export abstract class CachedMapResource< get(key: ResourceKeyList): Array; get(key: ResourceKey): Array| TValue | undefined; get(key: ResourceKey): Array| TValue | undefined { + key = this.transformParam(key); return ResourceKeyUtils.map(key, key => this.data.get(key)); } set(key: TKey, value: TValue): void; set(key: ResourceKeyList, value: TValue[]): void; set(key: ResourceKey, value: TValue | TValue[]): void { + key = this.transformParam(key); ResourceKeyUtils.forEach(key, (key, i) => { if (i === -1) { this.data.set(key, value as TValue); @@ -184,6 +211,7 @@ export abstract class CachedMapResource< delete(key: ResourceKeyList): void; delete(key: ResourceKey): void; delete(key: ResourceKey): void { + key = this.transformParam(key); ResourceKeyUtils.forEach(key, key => this.data.delete(key)); this.markUpdated(key); this.onItemDelete.execute(key); @@ -230,6 +258,7 @@ export abstract class CachedMapResource< } has(key: TKey): boolean { + key = this.transformParam(key) as TKey; return this.data.has(key); } @@ -241,6 +270,7 @@ export abstract class CachedMapResource< if (!key) { return this.defaultIncludes; } + key = this.transformParam(key); const metadata = this.metadata.get(ResourceKeyUtils.first(key)); @@ -266,6 +296,7 @@ export abstract class CachedMapResource< } protected commitIncludes(key: ResourceKey, includes: string[]): void { + key = this.transformParam(key); ResourceKeyUtils.forEach(key, key => { const metadata = this.metadata.get(key); diff --git a/webapp/packages/core-sdk/src/Resource/CachedResource.ts b/webapp/packages/core-sdk/src/Resource/CachedResource.ts index 41c2abd6e8..9c5ff0e9a8 100644 --- a/webapp/packages/core-sdk/src/Resource/CachedResource.ts +++ b/webapp/packages/core-sdk/src/Resource/CachedResource.ts @@ -6,14 +6,26 @@ * you may not use this file except in compliance with the License. */ -import { observable, makeObservable } from 'mobx'; +import { observable, makeObservable, action } from 'mobx'; -import { Executor, IExecutor, TaskScheduler } from '@cloudbeaver/core-executor'; +import { Dependency } from '@cloudbeaver/core-di'; +import { Executor, ExecutorInterrupter, IExecutor, TaskScheduler } from '@cloudbeaver/core-executor'; import { MetadataMap } from '@cloudbeaver/core-utils'; export interface ICachedResourceMetadata { outdated: boolean; loading: boolean; + exception: Error | null; +} + +export interface IDataError { + param: TParam; + exception: Error; +} + +export interface IParamAlias { + param: TParam; + getAlias: (param: TParam) => TParam; } export type CachedResourceData< @@ -33,32 +45,55 @@ export abstract class CachedResource< TParam, TKey = TParam, TContext = void -> { +> extends Dependency { data: TData; readonly onDataOutdated: IExecutor; - readonly onDataUpdate: IExecutor; + readonly onDataUpdate: IExecutor; + readonly onDataError: IExecutor>; + readonly beforeLoad: IExecutor; protected metadata: MetadataMap; - protected loading = false; + protected get loading(): boolean { + return this.scheduler.executing; + } protected scheduler: TaskScheduler; + protected paramAliases: Array>; constructor(defaultValue: TData) { - makeObservable, 'loading'>(this, { + super(); + makeObservable, 'loader'>(this, { data: observable, - loading: observable, + loader: action, + markDataLoading: action, + markDataLoaded: action, + markDataError: action, + markOutdated: action, + markUpdated: action, }); this.includes = this.includes.bind(this); this.loadingTask = this.loadingTask.bind(this); - this.metadata = new MetadataMap(() => ({ outdated: true, loading: false })); + this.paramAliases = []; + this.metadata = new MetadataMap(() => ({ outdated: true, loading: false, exception: null })); this.scheduler = new TaskScheduler(this.includes); this.data = defaultValue; + this.beforeLoad = new Executor(null, this.includes); this.onDataOutdated = new Executor(null, this.includes); - this.onDataUpdate = new Executor(); + this.onDataUpdate = new Executor(null, this.includes); + this.onDataError = new Executor>(null, (a, b) => this.includes(a.param, b.param)); + } + + sync( + resource: CachedResource, + context: TContext + ): void { + resource.onDataOutdated.addHandler(this.markOutdated.bind(this)); + resource.onDataUpdate.addHandler(param => this.load(param, context)); + this.beforeLoad.addHandler(param => resource.load(param)); } abstract isLoaded(param: TParam, context: TContext): boolean; @@ -72,45 +107,87 @@ export abstract class CachedResource< } isOutdated(param: TParam): boolean { + param = this.transformParam(param); return this.metadata.get(param as unknown as TKey).outdated; } isDataLoading(param: TParam): boolean { + param = this.transformParam(param); return this.metadata.get(param as unknown as TKey).loading; } markDataLoading(param: TParam, context: TContext): void { + param = this.transformParam(param); const metadata = this.metadata.get(param as unknown as TKey); metadata.loading = true; } markDataLoaded(param: TParam, context: TContext): void { + param = this.transformParam(param); const metadata = this.metadata.get(param as unknown as TKey); metadata.loading = false; } - markOutdated(param: TParam): void { + async markDataError(exception: Error, param: TParam, context: TContext): Promise { + param = this.transformParam(param); + const metadata = this.metadata.get(param as unknown as TKey); + metadata.exception = exception; + await this.onDataError.execute({ param, exception }); + } + + async markOutdated(param: TParam): Promise { + param = this.transformParam(param); const metadata = this.metadata.get(param as unknown as TKey); metadata.outdated = true; - this.onDataOutdated.execute(param); + await this.onDataOutdated.execute(param); } markUpdated(param: TParam): void { + param = this.transformParam(param); const metadata = this.metadata.get(param as unknown as TKey); metadata.outdated = false; + metadata.exception = null; + } + + addAlias(param: TParam, getAlias: (param: TParam) => TParam): void { + this.paramAliases.push({ param, getAlias }); + } + + transformParam(param: TParam): TParam { + let deep = 0; + // eslint-disable-next-line no-labels + transform: + + if (deep < 10) { + for (const alias of this.paramAliases) { + if (this.includes(alias.param, param)) { + param = alias.getAlias(param); + deep++; + // eslint-disable-next-line no-labels + break transform; + } + } + } else { + console.warn('CachedResource: parameter transform was stopped'); + } + return param; } async refresh(param: TParam, context: TContext): Promise { + param = this.transformParam(param); await this.loadData(param, true, context); return this.data; } async load(param: TParam, context: TContext): Promise { + param = this.transformParam(param); await this.loadData(param, false, context); return this.data; } protected includes(param: TParam, second: TParam): boolean { + param = this.transformParam(param); + second = this.transformParam(second); return param === second; } @@ -138,46 +215,59 @@ export abstract class CachedResource< return; } - this.markDataLoading(param, context); - this.loading = true; - return this.scheduler.schedule(param, async () => { - // repeated because previous task maybe has been load requested data - if (exitCheck?.()) { - return; - } + param = this.transformParam(param); + const contexts = await this.beforeLoad.execute(param); - return await this.taskWrapper(param, context, update); - }, () => { - this.markDataLoaded(param, context); - this.loading = false; - }); + if (ExecutorInterrupter.isInterrupted(contexts)) { + return; + } + + this.markDataLoading(param, context); + return this.scheduler.schedule( + param, + async () => { + // repeated because previous task maybe has been load requested data + if (exitCheck?.()) { + return; + } + + return await this.taskWrapper(param, context, update); + }, + () => this.markDataLoaded(param, context), + () => this.onDataUpdate.execute(param), + exception => this.markDataError(exception, param, context)); } protected async loadData(param: TParam, refresh: boolean, context: TContext): Promise { + param = this.transformParam(param); + const contexts = await this.beforeLoad.execute(param); + + if (ExecutorInterrupter.isInterrupted(contexts) && !refresh) { + return; + } + if (this.isLoaded(param, context) && !this.isOutdated(param) && !refresh) { return; } this.markDataLoading(param, context); - this.loading = true; - await this.scheduler.schedule(param, async () => { - // repeated because previous task maybe has been load requested data - if (this.isLoaded(param, context) && !this.isOutdated(param) && !refresh) { - return; - } + await this.scheduler.schedule( + param, + async () => { + // repeated because previous task maybe has been load requested data + if (this.isLoaded(param, context) && !this.isOutdated(param) && !refresh) { + return; + } - await this.taskWrapper(param, context, this.loadingTask); - }, () => { - this.markDataLoaded(param, context); - this.loading = false; - }); + await this.taskWrapper(param, context, this.loadingTask); + }, + () => this.markDataLoaded(param, context), + () => this.onDataUpdate.execute(param), + exception => this.markDataError(exception, param, context)); } private async loadingTask(param: TParam, context: TContext) { this.data = await this.loader(param, context); - - // TODO: seems should be moved to scheduler `after` callback - this.onDataUpdate.execute(this.data); } private async taskWrapper( @@ -185,7 +275,7 @@ export abstract class CachedResource< context: TContext, promise: (param: TParam, context: TContext) => Promise ) { - this.markOutdated(param); + await this.markOutdated(param); const value = await promise(param, context); this.markUpdated(param); return value; diff --git a/webapp/packages/core-sdk/src/queries/authentication/authLogin.gql b/webapp/packages/core-sdk/src/queries/authentication/authLogin.gql index 58d62196da..601c32a823 100644 --- a/webapp/packages/core-sdk/src/queries/authentication/authLogin.gql +++ b/webapp/packages/core-sdk/src/queries/authentication/authLogin.gql @@ -1,12 +1,10 @@ query authLogin( $provider: ID! $credentials: Object! + $linkUser: Boolean + $customIncludeOriginDetails: Boolean! ) { - user: authLogin(provider: $provider, credentials: $credentials) { - userId - displayName - authProvider - loginTime - message + authToken: authLogin(provider: $provider, credentials: $credentials, linkUser: $linkUser) { + ...AuthToken } } \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/authentication/getActiveUser.gql b/webapp/packages/core-sdk/src/queries/authentication/getActiveUser.gql new file mode 100644 index 0000000000..134e6717af --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/authentication/getActiveUser.gql @@ -0,0 +1,10 @@ +query getActiveUser($customIncludeOriginDetails: Boolean!) { + user: activeUser { + userId + displayName + linkedAuthProviders + authTokens { + ...AuthToken + } + } +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/authentication/getSessionUser.gql b/webapp/packages/core-sdk/src/queries/authentication/getSessionUser.gql deleted file mode 100644 index ee457061b2..0000000000 --- a/webapp/packages/core-sdk/src/queries/authentication/getSessionUser.gql +++ /dev/null @@ -1,9 +0,0 @@ -query getSessionUser { - user: sessionUser { - userId - displayName - authProvider - loginTime - message - } -} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/connections/createConnectionFromTemplate.gql b/webapp/packages/core-sdk/src/queries/connections/createConnectionFromTemplate.gql index 6abc3245df..ce3efeec5f 100644 --- a/webapp/packages/core-sdk/src/queries/connections/createConnectionFromTemplate.gql +++ b/webapp/packages/core-sdk/src/queries/connections/createConnectionFromTemplate.gql @@ -1,12 +1,13 @@ mutation createConnectionFromTemplate( $templateId: ID!, + $connectionName: String!, $includeOrigin: Boolean!, $customIncludeOriginDetails: Boolean!, $includeAuthProperties: Boolean!, $customIncludeNetworkHandlerCredentials: Boolean! ) { - connection: createConnectionFromTemplate(templateId: $templateId) { + connection: createConnectionFromTemplate(templateId: $templateId, connectionName: $connectionName) { ...DatabaseConnection } } diff --git a/webapp/packages/core-sdk/src/queries/fragments/AuthToken.gql b/webapp/packages/core-sdk/src/queries/fragments/AuthToken.gql new file mode 100644 index 0000000000..589a0d6506 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/fragments/AuthToken.gql @@ -0,0 +1,8 @@ +fragment AuthToken on UserAuthToken { + authProvider + loginTime + message + origin { + ...ObjectOriginInfo + } +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/queries/fragments/ObjectOriginInfo.gql b/webapp/packages/core-sdk/src/queries/fragments/ObjectOriginInfo.gql index c955eac864..0bc13bba02 100644 --- a/webapp/packages/core-sdk/src/queries/fragments/ObjectOriginInfo.gql +++ b/webapp/packages/core-sdk/src/queries/fragments/ObjectOriginInfo.gql @@ -3,6 +3,7 @@ fragment ObjectOriginInfo on ObjectOrigin { subType displayName icon + details @include(if: $customIncludeOriginDetails) { id displayName diff --git a/webapp/packages/core-sdk/src/queries/session/serverConfig.gql b/webapp/packages/core-sdk/src/queries/session/serverConfig.gql index 6fe22573f6..048783dab7 100644 --- a/webapp/packages/core-sdk/src/queries/session/serverConfig.gql +++ b/webapp/packages/core-sdk/src/queries/session/serverConfig.gql @@ -2,6 +2,7 @@ query serverConfig { serverConfig { name version + workspaceId productConfiguration supportsCustomConnections supportsConnectionBrowser @@ -20,6 +21,7 @@ query serverConfig { configurationMode developmentMode + enabledAuthProviders supportedLanguages { isoCode displayName diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 831c7e5685..0e42946493 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -17,8 +17,9 @@ export interface Scalars { } export interface Query { + activeUser?: Maybe; allConnections: ConnectionInfo[]; - authLogin: UserAuthInfo; + authLogin: UserAuthToken; authLogout?: Maybe; authModels: DatabaseAuthModel[]; authProviders: AuthProviderInfo[]; @@ -56,7 +57,6 @@ export interface Query { serverConfig: ServerConfig; sessionPermissions: Array>; sessionState: SessionInfo; - sessionUser?: Maybe; setConnectionSubjectAccess?: Maybe; setDefaultNavigatorSettings: Scalars['Boolean']; setSubjectConnectionAccess?: Maybe; @@ -77,6 +77,11 @@ export interface QueryAllConnectionsArgs { export interface QueryAuthLoginArgs { provider: Scalars['ID']; credentials: Scalars['Object']; + linkUser?: Maybe; +} + +export interface QueryAuthLogoutArgs { + provider?: Maybe; } export interface QueryConfigureServerArgs { @@ -336,6 +341,7 @@ export interface MutationCreateConnectionArgs { export interface MutationCreateConnectionFromTemplateArgs { templateId: Scalars['ID']; + connectionName?: Maybe; } export interface MutationDeleteConnectionArgs { @@ -473,7 +479,9 @@ export interface ProductInfo { export interface ServerConfig { name: Scalars['String']; version: Scalars['String']; + workspaceId: Scalars['ID']; anonymousAccessEnabled?: Maybe; + /** @deprecated Field no longer supported */ authenticationEnabled?: Maybe; supportsCustomConnections?: Maybe; supportsConnectionBrowser?: Maybe; @@ -486,6 +494,7 @@ export interface ServerConfig { localHostAddress?: Maybe; configurationMode?: Maybe; developmentMode?: Maybe; + enabledAuthProviders: Array; supportedLanguages: ServerLanguage[]; services?: Maybe>>; productConfiguration: Scalars['Object']; @@ -887,6 +896,7 @@ export interface ServerConfigInput { customConnectionsEnabled?: Maybe; publicCredentialsSaveEnabled?: Maybe; adminCredentialsSaveEnabled?: Maybe; + enabledAuthProviders?: Maybe>; sessionExpireTime?: Maybe; } @@ -915,15 +925,22 @@ export interface AuthProviderInfo { credentialParameters: AuthCredentialInfo[]; } -export interface UserAuthInfo { - userId: Scalars['String']; - displayName?: Maybe; - authProvider: Scalars['String']; +export interface UserAuthToken { + authProvider: Scalars['ID']; loginTime: Scalars['DateTime']; + userId: Scalars['String']; + displayName: Scalars['String']; message?: Maybe; origin: ObjectOrigin; } +export interface UserInfo { + userId: Scalars['String']; + displayName?: Maybe; + authTokens: UserAuthToken[]; + linkedAuthProviders: Array; +} + export interface DataTransferProcessorInfo { id: Scalars['ID']; name?: Maybe; @@ -954,14 +971,27 @@ export interface AsyncTaskCancelMutation { result: Mutation['asyncTaskCancel'] } export type AuthLoginQueryVariables = Exact<{ provider: Scalars['ID']; credentials: Scalars['Object']; + linkUser?: Maybe; + customIncludeOriginDetails: Scalars['Boolean']; }>; -export interface AuthLoginQuery { user: Pick } +export interface AuthLoginQuery { authToken: AuthTokenFragment } export type AuthLogoutQueryVariables = Exact<{ [key: string]: never }>; export type AuthLogoutQuery = Pick; +export type GetActiveUserQueryVariables = Exact<{ + customIncludeOriginDetails: Scalars['Boolean']; +}>; + +export interface GetActiveUserQuery { + user?: Maybe<( + Pick + & { authTokens: AuthTokenFragment[] } + )>; +} + export type GetAuthProvidersQueryVariables = Exact<{ [key: string]: never }>; export interface GetAuthProvidersQuery { @@ -971,10 +1001,6 @@ export interface GetAuthProvidersQuery { )>; } -export type GetSessionUserQueryVariables = Exact<{ [key: string]: never }>; - -export interface GetSessionUserQuery { user?: Maybe> } - export type CreateUserQueryVariables = Exact<{ userId: Scalars['ID']; customIncludeOriginDetails: Scalars['Boolean']; @@ -1142,6 +1168,7 @@ export interface CreateConnectionFromNodeMutation { connection: DatabaseConnecti export type CreateConnectionFromTemplateMutationVariables = Exact<{ templateId: Scalars['ID']; + connectionName: Scalars['String']; includeOrigin: Scalars['Boolean']; customIncludeOriginDetails: Scalars['Boolean']; includeAuthProperties: Scalars['Boolean']; @@ -1304,6 +1331,11 @@ export type AdminUserInfoFragment = ( export type AllNavigatorSettingsFragment = Pick; +export type AuthTokenFragment = ( + Pick + & { origin: ObjectOriginInfoFragment } +); + export type DatabaseConnectionFragment = ( Pick & { origin: ObjectOriginInfoFragment; authProperties: UserConnectionAuthPropertiesFragment[]; networkHandlersConfig: Array, 'userName' | 'password' | 'properties'>>; navigatorSettings: AllNavigatorSettingsFragment } @@ -1539,7 +1571,7 @@ export type ServerConfigQueryVariables = Exact<{ [key: string]: never }>; export interface ServerConfigQuery { serverConfig: ( - Pick + Pick & { supportedLanguages: Array>; defaultNavigatorSettings: AllNavigatorSettingsFragment; productInfo: Pick } ); } @@ -1617,6 +1649,16 @@ export const AdminUserInfoFragmentDoc = ` } } ${ObjectOriginInfoFragmentDoc}`; +export const AuthTokenFragmentDoc = ` + fragment AuthToken on UserAuthToken { + authProvider + loginTime + message + origin { + ...ObjectOriginInfo + } +} + ${ObjectOriginInfoFragmentDoc}`; export const UserConnectionAuthPropertiesFragmentDoc = ` fragment UserConnectionAuthProperties on ObjectPropertyInfo { id @@ -1783,21 +1825,33 @@ export const AsyncTaskCancelDocument = ` } `; export const AuthLoginDocument = ` - query authLogin($provider: ID!, $credentials: Object!) { - user: authLogin(provider: $provider, credentials: $credentials) { - userId - displayName - authProvider - loginTime - message + query authLogin($provider: ID!, $credentials: Object!, $linkUser: Boolean, $customIncludeOriginDetails: Boolean!) { + authToken: authLogin( + provider: $provider + credentials: $credentials + linkUser: $linkUser + ) { + ...AuthToken } } - `; + ${AuthTokenFragmentDoc}`; export const AuthLogoutDocument = ` query authLogout { authLogout } `; +export const GetActiveUserDocument = ` + query getActiveUser($customIncludeOriginDetails: Boolean!) { + user: activeUser { + userId + displayName + linkedAuthProviders + authTokens { + ...AuthToken + } + } +} + ${AuthTokenFragmentDoc}`; export const GetAuthProvidersDocument = ` query getAuthProviders { providers: authProviders { @@ -1818,17 +1872,6 @@ export const GetAuthProvidersDocument = ` } } `; -export const GetSessionUserDocument = ` - query getSessionUser { - user: sessionUser { - userId - displayName - authProvider - loginTime - message - } -} - `; export const CreateUserDocument = ` query createUser($userId: ID!, $customIncludeOriginDetails: Boolean!) { user: createUser(userId: $userId) { @@ -1983,8 +2026,11 @@ export const CreateConnectionFromNodeDocument = ` } ${DatabaseConnectionFragmentDoc}`; export const CreateConnectionFromTemplateDocument = ` - mutation createConnectionFromTemplate($templateId: ID!, $includeOrigin: Boolean!, $customIncludeOriginDetails: Boolean!, $includeAuthProperties: Boolean!, $customIncludeNetworkHandlerCredentials: Boolean!) { - connection: createConnectionFromTemplate(templateId: $templateId) { + mutation createConnectionFromTemplate($templateId: ID!, $connectionName: String!, $includeOrigin: Boolean!, $customIncludeOriginDetails: Boolean!, $includeAuthProperties: Boolean!, $customIncludeNetworkHandlerCredentials: Boolean!) { + connection: createConnectionFromTemplate( + templateId: $templateId + connectionName: $connectionName + ) { ...DatabaseConnection } } @@ -2453,6 +2499,7 @@ export const ServerConfigDocument = ` serverConfig { name version + workspaceId productConfiguration supportsCustomConnections supportsConnectionBrowser @@ -2466,6 +2513,7 @@ export const ServerConfigDocument = ` licenseValid configurationMode developmentMode + enabledAuthProviders supportedLanguages { isoCode displayName @@ -2556,12 +2604,12 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = authLogout(variables?: AuthLogoutQueryVariables): Promise { return withWrapper(() => client.request(AuthLogoutDocument, variables)); }, + getActiveUser(variables: GetActiveUserQueryVariables): Promise { + return withWrapper(() => client.request(GetActiveUserDocument, variables)); + }, getAuthProviders(variables?: GetAuthProvidersQueryVariables): Promise { return withWrapper(() => client.request(GetAuthProvidersDocument, variables)); }, - getSessionUser(variables?: GetSessionUserQueryVariables): Promise { - return withWrapper(() => client.request(GetSessionUserDocument, variables)); - }, createUser(variables: CreateUserQueryVariables): Promise { return withWrapper(() => client.request(CreateUserDocument, variables)); }, diff --git a/webapp/packages/core-theming/src/ThemeService.ts b/webapp/packages/core-theming/src/ThemeService.ts index cfbd049e37..1b0681244c 100644 --- a/webapp/packages/core-theming/src/ThemeService.ts +++ b/webapp/packages/core-theming/src/ThemeService.ts @@ -13,7 +13,7 @@ import './styles/main/app-loading-screen.css'; import './styles/main/elevation.scss'; import './styles/main/typography.scss'; import './styles/main/color.scss'; -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { DbeaverError, NotificationService } from '@cloudbeaver/core-events'; import { SettingsService } from '@cloudbeaver/core-settings'; @@ -33,7 +33,7 @@ const THEME_SETTINGS_KEY = 'themeSettings'; const DEFAULT_THEME_ID = 'light'; @injectable() -export class ThemeService { +export class ThemeService extends Bootstrap { get themes(): ITheme[] { return Array.from(this.themeMap.values()); } @@ -60,6 +60,8 @@ export class ThemeService { private notificationService: NotificationService, private settingsService: SettingsService ) { + super(); + makeObservable(this, { themes: computed, currentTheme: computed, @@ -67,11 +69,13 @@ export class ThemeService { settings: observable, setCurrentThemeId: action, }); + } + register(): void { this.loadAllThemes(); } - async init() { + async load(): Promise { this.settingsService.registerSettings(this.settings, THEME_SETTINGS_KEY); await this.changeThemeAsync(this.currentThemeId); } diff --git a/webapp/packages/core-theming/src/styles/_checkbox.scss b/webapp/packages/core-theming/src/styles/_checkbox.scss index 8066cc7b31..f030fae77a 100644 --- a/webapp/packages/core-theming/src/styles/_checkbox.scss +++ b/webapp/packages/core-theming/src/styles/_checkbox.scss @@ -52,6 +52,18 @@ $mdc-checkbox-icon-size: 16px; ); } + .theme-checkbox_surface { + $mdc-checkbox-mark-color: $mdc-theme-primary; + @include mdc-checkbox-ink-color($mdc-checkbox-mark-color, $query); + @include mdc-checkbox-disabled-ink-color(rgba($mdc-checkbox-mark-color, 0.5), $query); + @include mdc-checkbox-container-colors( + $mdc-theme-surface, + transparent, + $mdc-theme-surface, + $mdc-theme-surface + ); + } + .theme-checkbox_small { @include mdc-checkbox-density(-3); } diff --git a/webapp/packages/core-theming/src/styles/main/normalize.css b/webapp/packages/core-theming/src/styles/main/normalize.css index 5e1a0484d4..8e4b4da433 100644 --- a/webapp/packages/core-theming/src/styles/main/normalize.css +++ b/webapp/packages/core-theming/src/styles/main/normalize.css @@ -379,7 +379,7 @@ template { */ [hidden] { - display: none; + display: none !important; } .icon { diff --git a/webapp/packages/core-ui/package.json b/webapp/packages/core-ui/package.json index 663209eacc..3298d98f50 100644 --- a/webapp/packages/core-ui/package.json +++ b/webapp/packages/core-ui/package.json @@ -15,9 +15,13 @@ "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix" }, "peerDependencies": { + "@cloudbeaver/core-authentication": "^0.1.0", "@cloudbeaver/core-di": "^0.1.0", "@cloudbeaver/core-executor": "^0.1.0", + "@cloudbeaver/core-blocks": "^0.1.0", + "@cloudbeaver/core-sdk": "^0.1.0", "@cloudbeaver/core-dialogs": "^0.1.0", - "mobx": "^5.x.x" + "mobx": "^5.x.x", + "mobx-react-lite": "^3.x.x" } } \ No newline at end of file diff --git a/webapp/packages/core-ui/src/AuthenticationProvider.tsx b/webapp/packages/core-ui/src/AuthenticationProvider.tsx new file mode 100644 index 0000000000..78781c48cd --- /dev/null +++ b/webapp/packages/core-ui/src/AuthenticationProvider.tsx @@ -0,0 +1,78 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 styled, { css } from 'reshadow'; + +import { AuthProviderService, UserInfoResource } from '@cloudbeaver/core-authentication'; +import { BASE_CONTAINERS_STYLES, Button, Container, GroupItem } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import type { ObjectOrigin } from '@cloudbeaver/core-sdk'; +import { useStyles } from '@cloudbeaver/core-theming'; + +const styles = css` + Container { + justify-content: center; + align-items: center; + } +`; + +export type Props = { + children?: () => React.ReactNode; + className?: string; +} & ({ + origin: ObjectOrigin; +} | { + type: string; + subType?: string; +}); + +export const AuthenticationProvider: React.FC = observer(function AuthenticationProvider(props) { + const translate = useTranslate(); + const style = useStyles(styles, BASE_CONTAINERS_STYLES); + const authProviderService = useService(AuthProviderService); + const userInfoService = useService(UserInfoResource); + let type: string; + let subType: string | undefined; + + if ('origin' in props) { + type = props.origin.type; + subType = props.origin.subType; + } else { + type = props.type; + subType = props.subType; + } + + const authorized = userInfoService.hasToken(type, subType); + + if (authorized) { + return props.children?.() as null || null; + } + + function handleAuth() { + authProviderService.requireProvider(type, subType); + } + + return styled(style)( + + + {translate('authentication_request_token')} + + + + + + ); +}); diff --git a/webapp/packages/core-ui/src/index.ts b/webapp/packages/core-ui/src/index.ts index 7c725d6bed..11001f83a2 100644 --- a/webapp/packages/core-ui/src/index.ts +++ b/webapp/packages/core-ui/src/index.ts @@ -1,2 +1,3 @@ export * from './Screens/AppScreen/NavigationService'; export * from './Screens/AppScreen/OptionsPanelService'; +export * from './AuthenticationProvider'; diff --git a/webapp/packages/core-utils/src/BuildVersion.ts b/webapp/packages/core-utils/src/BuildVersion.ts new file mode 100644 index 0000000000..2322d7c756 --- /dev/null +++ b/webapp/packages/core-utils/src/BuildVersion.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +declare const _VERSION_: string | undefined; + +export const BuildVersion = { + get version(): string | undefined { + return _VERSION_; + }, +}; diff --git a/webapp/packages/core-utils/src/declensionOfNumber.ts b/webapp/packages/core-utils/src/declensionOfNumber.ts new file mode 100644 index 0000000000..0ff755c7e3 --- /dev/null +++ b/webapp/packages/core-utils/src/declensionOfNumber.ts @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 function declensionOfNumber(value: number, words: string[]) { + value = Math.abs(value) % 100; + const num = value % 10; + if (value > 10 && value < 20) { + return words[2]; + } + + if (num > 1 && num < 5) { + return words[1]; + } + + if (num === 1) { + return words[0]; + } + + return words[2]; +} diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index 337dd0750e..41bc0758dc 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -1,3 +1,4 @@ +export * from './BuildVersion'; export * from './flat'; export * from './MetadataMap'; export * from './OrderedMap'; @@ -7,6 +8,7 @@ export * from './generateFileName'; export * from './md5'; export * from './TextTools'; export * from './copyToClipboard'; +export * from './declensionOfNumber'; export * from './Promises/CancellablePromise'; export * from './Promises/Deferred'; diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts index 6bf0f8c170..4c83787b98 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ConfigurationWizardPagesBootstrapService.ts @@ -44,10 +44,15 @@ export class ConfigurationWizardPagesBootstrapService extends Bootstrap { configurationWizardOptions: { description: 'administration_configuration_wizard_configuration_step_description', order: 1.5, + onLoad: this.serverConfigurationService.loadConfig.bind(this.serverConfigurationService), isDone: this.serverConfigurationService.isDone.bind(this.serverConfigurationService), - onFinish: this.serverConfigurationService.validate.bind(this.serverConfigurationService), - onConfigurationFinish: this.serverConfigurationService.handleConfigurationFinish.bind( - this.serverConfigurationService + onFinish: this.serverConfigurationService.saveConfiguration.bind( + this.serverConfigurationService, + false + ), + onConfigurationFinish: this.serverConfigurationService.saveConfiguration.bind( + this.serverConfigurationService, + true ), }, order: 4, diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationAdminForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationAdminForm.tsx deleted file mode 100644 index 9f3ba6b17e..0000000000 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationAdminForm.tsx +++ /dev/null @@ -1,57 +0,0 @@ - -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2021 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 { FormGroup, InputGroup, InputField } from '@cloudbeaver/core-blocks'; -import { useTranslate } from '@cloudbeaver/core-localization'; -import type { ServerConfigInput } from '@cloudbeaver/core-sdk'; - -interface Props { - serverConfig: ServerConfigInput; -} - -export const ServerConfigurationAdminForm: React.FC = observer(function ServerConfigurationAdminForm({ - serverConfig, -}) { - const translate = useTranslate(); - return ( - <> - - {translate('administration_configuration_wizard_configuration_admin')} - - - - {translate('administration_configuration_wizard_configuration_admin_name')} - - - - - {translate('administration_configuration_wizard_configuration_admin_password')} - - - - ); -}); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationConfigurationForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationConfigurationForm.tsx index 96d1fabf0e..41ded94d66 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationConfigurationForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationConfigurationForm.tsx @@ -8,10 +8,12 @@ */ import { observer } from 'mobx-react-lite'; +import styled from 'reshadow'; -import { FormGroup, InputGroup, Switch } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, SwitchNew } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import type { ServerConfigInput } from '@cloudbeaver/core-sdk'; +import { useStyles } from '@cloudbeaver/core-theming'; interface Props { serverConfig: ServerConfigInput; @@ -21,48 +23,18 @@ export const ServerConfigurationConfigurationForm: React.FC = observer(fu serverConfig, }) { const translate = useTranslate(); - return ( + return styled(useStyles(BASE_CONTAINERS_STYLES))( <> - - {translate('administration_configuration_wizard_configuration_plugins')} - - - - {translate('administration_configuration_wizard_configuration_anonymous_access')} - - - - - {translate('administration_configuration_wizard_configuration_authentication')} - - - - - {translate('administration_configuration_wizard_configuration_custom_connections')} - - + + {translate('administration_configuration_wizard_configuration_custom_connections')} + ); }); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx index 0c4e582a7a..ce90213f68 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm.tsx @@ -8,48 +8,48 @@ */ import { observer } from 'mobx-react-lite'; +import styled from 'reshadow'; -import { FormGroup, InputGroup, InputField } from '@cloudbeaver/core-blocks'; +import { Group, GroupTitle, BASE_CONTAINERS_STYLES, InputFieldNew } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; -import type { ServerConfigInput } from '@cloudbeaver/core-sdk'; +import { useStyles } from '@cloudbeaver/core-theming'; + +import type { IServerConfigurationPageState } from '../IServerConfigurationPageState'; interface Props { - serverConfig: ServerConfigInput; + state: IServerConfigurationPageState; } export const ServerConfigurationInfoForm: React.FC = observer(function ServerConfigurationInfoForm({ - serverConfig, + state, }) { const translate = useTranslate(); - return ( - <> - - {translate('administration_configuration_wizard_configuration_server_info')} - - - - {translate('administration_configuration_wizard_configuration_server_name')} - - - - - {translate('administration_configuration_wizard_configuration_server_session_lifetime')} - - - + return styled(useStyles(BASE_CONTAINERS_STYLES))( + + {translate('administration_configuration_wizard_configuration_server_info')} + + {translate('administration_configuration_wizard_configuration_server_name')} + + (v ?? 1800000) / 1000 / 60} + mapValue={v => (v ?? 30) * 1000 * 60} + required + tiny + > + {translate('administration_configuration_wizard_configuration_server_session_lifetime')} + + ); }); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx index 89575ede21..f24e95fca7 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm.tsx @@ -8,10 +8,12 @@ import { observer } from 'mobx-react-lite'; import React, { useCallback } from 'react'; +import styled from 'reshadow'; -import { FormGroup, Switch } from '@cloudbeaver/core-blocks'; -import { CONNECTION_NAVIGATOR_VIEW_SETTINGS, isNavigatorViewSettingsEqual } from '@cloudbeaver/core-connections'; +import { BASE_CONTAINERS_STYLES, SwitchNew } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; +import { CONNECTION_NAVIGATOR_VIEW_SETTINGS, isNavigatorViewSettingsEqual } from '@cloudbeaver/core-root'; +import { useStyles } from '@cloudbeaver/core-theming'; import type { IServerConfigurationPageState } from '../IServerConfigurationPageState'; @@ -34,20 +36,18 @@ export const ServerConfigurationNavigatorViewForm: React.FC = observer(fu } }, [configs]); - return ( + return styled(useStyles(BASE_CONTAINERS_STYLES))( <> - - - {translate('administration_configuration_wizard_configuration_navigation_tree_view')} - - + + {translate('administration_configuration_wizard_configuration_navigation_tree_view')} + ); }); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationSecurityForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationSecurityForm.tsx index 1e72f17d08..d93450b76b 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationSecurityForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationSecurityForm.tsx @@ -7,10 +7,12 @@ */ import { observer } from 'mobx-react-lite'; +import styled from 'reshadow'; -import { FormGroup, InputGroup, Switch } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, Group, GroupTitle, SwitchNew } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import type { ServerConfigInput } from '@cloudbeaver/core-sdk'; +import { useStyles } from '@cloudbeaver/core-theming'; interface Props { serverConfig: ServerConfigInput; @@ -20,37 +22,30 @@ export const ServerConfigurationSecurityForm: React.FC = observer(functio serverConfig, }) { const translate = useTranslate(); - - return ( - <> - - {translate('administration_configuration_wizard_configuration_security')} - - - - {translate('administration_configuration_wizard_configuration_security_admin_credentials')} - - - - - {translate('administration_configuration_wizard_configuration_security_public_credentials')} - - - + return styled(useStyles(BASE_CONTAINERS_STYLES))( + + {translate('administration_configuration_wizard_configuration_security')} + + {translate('administration_configuration_wizard_configuration_security_admin_credentials')} + + + {translate('administration_configuration_wizard_configuration_security_public_credentials')} + + ); }); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx index 4bec4e3c2a..9a966da4df 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPage.tsx @@ -7,16 +7,15 @@ */ import { observer } from 'mobx-react-lite'; -import styled, { use, css } from 'reshadow'; +import styled, { css } from 'reshadow'; -import { AdministrationTools, ADMINISTRATION_TOOLS_STYLES } from '@cloudbeaver/core-administration'; -import { FormBox, FormBoxElement, IconButton, Loader, SubmittingForm, useFocus } from '@cloudbeaver/core-blocks'; +import { AdministrationItemContentComponent, AdministrationTools, ADMINISTRATION_TOOLS_STYLES } from '@cloudbeaver/core-administration'; +import { BASE_CONTAINERS_STYLES, ColoredContainer, Container, Group, GroupItem, GroupTitle, IconButton, Loader, Placeholder, SubmittingForm, useFocus, useFormValidator } from '@cloudbeaver/core-blocks'; import { useController, useService } from '@cloudbeaver/core-di'; -import { useFormValidator } from '@cloudbeaver/core-executor'; import { useTranslate } from '@cloudbeaver/core-localization'; -import { useStyles, composes } from '@cloudbeaver/core-theming'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; +import { useStyles } from '@cloudbeaver/core-theming'; -import { ServerConfigurationAdminForm } from './Form/ServerConfigurationAdminForm'; import { ServerConfigurationConfigurationForm } from './Form/ServerConfigurationConfigurationForm'; import { ServerConfigurationInfoForm } from './Form/ServerConfigurationInfoForm'; import { ServerConfigurationNavigatorViewForm } from './Form/ServerConfigurationNavigatorViewForm'; @@ -24,106 +23,73 @@ import { ServerConfigurationSecurityForm } from './Form/ServerConfigurationSecur import { ServerConfigurationPageController } from './ServerConfigurationPageController'; import { ServerConfigurationService } from './ServerConfigurationService'; -const styles = composes( - css` - layout-grid-cell { - composes: theme-background-surface theme-text-on-surface theme-border-color-background from global; +const styles = css` + SubmittingForm { + flex: 1; + display: flex; + overflow: auto; + flex-direction: column; } - layout-grid-cell, message-box { - composes: theme-border-color-background from global; + p { + white-space: pre-wrap; + line-height: 2; } - `, - css` - layout-grid { - width: 100%; - flex: 1; - } +`; - layout-grid-inner { - min-height: 100%; - } - - layout-grid-cell { - position: relative; - display: flex; - flex-direction: column; - border: solid 1px; - } - - message-box { - border-bottom: solid 1px; - } - - message-box, SubmittingForm { - padding: 16px 24px; - } - - SubmittingForm { - flex: 1; - display: flex; - overflow: auto; - flex-direction: column; - } - - FormBox { - flex: 0; - } - - p { - line-height: 2; - } - ` -); - -export const ServerConfigurationPage = observer(function ServerConfigurationPage() { +export const ServerConfigurationPage: AdministrationItemContentComponent = observer(function ServerConfigurationPage({ + configurationWizard, +}) { const translate = useTranslate(); - const style = useStyles(styles, ADMINISTRATION_TOOLS_STYLES); + const style = useStyles(styles, ADMINISTRATION_TOOLS_STYLES, BASE_CONTAINERS_STYLES); const [focusedRef] = useFocus({ focusFirstChild: true }); + const serverConfigResource = useService(ServerConfigResource); const service = useService(ServerConfigurationService); const controller = useController(ServerConfigurationPageController); + const changed = serverConfigResource.isChanged() + || serverConfigResource.isNavigatorSettingsChanged(service.state.navigatorConfig); useFormValidator(service.validationTask, focusedRef); return styled(style)( - - - - {!controller.editing ? ( - + + {controller.editing && ( + + + + + )} + + {!controller.editing && ( + +

{translate('administration_configuration_wizard_configuration_title')}

+
+

{translate('administration_configuration_wizard_configuration_message')}

-
- ) : ( - - - - + + + )} + + + {() => styled(style)( + + + + {translate('administration_configuration_wizard_configuration_plugins')} + + + + + + + )} - - {service.loading ? ( - - ) : ( - <> - - - - {!controller.editing && ( - - )} - - - - - - - - - - - )} - -
-
-
+
+ + ); }); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPageController.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPageController.ts index 354a3f2ea5..f491f1e154 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPageController.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationPageController.ts @@ -36,9 +36,7 @@ export class ServerConfigurationPageController { }; change = (): void => { - if (!this.state.serverConfig.authenticationEnabled) { - this.state.serverConfig.anonymousAccessEnabled = true; - } + this.serverConfigurationService.changed(); if (!this.state.serverConfig.adminCredentialsSaveEnabled) { this.state.serverConfig.publicCredentialsSaveEnabled = false; @@ -62,7 +60,7 @@ export class ServerConfigurationPageController { if (result === DialogueStateResult.Rejected) { return; } - await this.serverConfigurationService.save(); + await this.serverConfigurationService.saveConfiguration(true); } private async finishConfiguration(): Promise { diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts index e752b6f2ec..6de9745314 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationService.ts @@ -9,164 +9,227 @@ import { observable, makeObservable } from 'mobx'; import { AdministrationScreenService } from '@cloudbeaver/core-administration'; -import { UsersResource } from '@cloudbeaver/core-authentication'; +import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; import { DEFAULT_NAVIGATOR_VIEW_SETTINGS } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; -import { IExecutor, Executor } from '@cloudbeaver/core-executor'; +import { IExecutor, Executor, IExecutorHandler, ExecutorInterrupter } from '@cloudbeaver/core-executor'; import { ServerConfigResource } from '@cloudbeaver/core-root'; -import { GraphQLService, ServerConfig } from '@cloudbeaver/core-sdk'; +import { GraphQLService } from '@cloudbeaver/core-sdk'; import type { IServerConfigurationPageState } from './IServerConfigurationPageState'; -export interface IValidationStatusContext { - getState: () => boolean; - invalidate: () => void; +export interface IConfigurationPlaceholderProps { + configurationWizard: boolean; + state: IServerConfigurationPageState; +} + +export interface IServerConfigSaveData { + state: IServerConfigurationPageState; + configurationWizard: boolean; + finish: boolean; +} + +export interface ILoadConfigData { + state: IServerConfigurationPageState; + reload: boolean; } @injectable() export class ServerConfigurationService { state: IServerConfigurationPageState; loading: boolean; - readonly validationTask: IExecutor; + + readonly loadConfigTask: IExecutor; + readonly prepareConfigTask: IExecutor; + readonly saveTask: IExecutor; + readonly validationTask: IExecutor; + readonly configurationContainer: PlaceholderContainer; + readonly pluginsContainer: PlaceholderContainer; + + private done: boolean; constructor( private readonly administrationScreenService: AdministrationScreenService, private readonly serverConfigResource: ServerConfigResource, private readonly graphQLService: GraphQLService, private readonly notificationService: NotificationService, - private readonly usersResource: UsersResource ) { - makeObservable(this, { + makeObservable(this, { state: observable, loading: observable, + done: observable, }); + this.done = false; this.loading = true; - this.state = this.getConfig(); + this.state = serverConfigStateContext(); + this.loadConfigTask = new Executor(); + this.prepareConfigTask = new Executor(); + this.saveTask = new Executor(); this.validationTask = new Executor(); + this.configurationContainer = new PlaceholderContainer(); + this.pluginsContainer = new PlaceholderContainer(); + + this.loadConfigTask + .next(this.validationTask, () => this.getSaveData(false)) + .addHandler(() => { this.loading = true; }) + .addHandler(this.loadServerConfig) + .addPostHandler(() => { this.loading = false; }); + + this.saveTask + .before(this.validationTask) + .before(this.prepareConfigTask) + .addPostHandler(this.save); + + this.validationTask + .addHandler(this.validateForm) + .addPostHandler(this.ensureValidation); + } + + changed(): void { + this.done = false; } async loadConfig(): Promise { - this.loading = true; try { - const config = await this.serverConfigResource.load(); + let reload = false; + this.state = this.administrationScreenService.getItemState( + 'server-configuration', + () => { + reload = true; + return this.state; + }, + !this.administrationScreenService.isConfigurationMode + ); + this.serverConfigResource.setDataUpdate(this.state.serverConfig); - this.state = this.administrationScreenService - .getItemState( - 'server-configuration', - () => this.getConfig(config), - !config?.configurationMode - ); + await this.loadConfigTask.execute({ + state: this.state, + reload, + }); } catch (exception) { this.notificationService.logException(exception, 'Can\'t load server configuration'); - } finally { - this.loading = false; } } isDone(): boolean { - return this.isFormFilled(); + return this.done; } - validationStatusContext = (): IValidationStatusContext => { - let state = this.isFormFilled(); + async saveConfiguration(finish: boolean): Promise { + const contexts = await this.saveTask.execute(this.getSaveData(finish)); - const invalidate = () => { - state = false; - }; - const getState = () => state; + const validation = contexts.getContext(serverConfigValidationContext); - return { - getState, - invalidate, - }; - }; + return validation.getState(); + } - async save(): Promise { - if (!this.state) { - throw new Error('No state available'); - } - - if (!(await this.validate())) { + private loadServerConfig: IExecutorHandler = async (data, contexts) => { + if (!data.reload) { return; } try { - await this.graphQLService.sdk.setDefaultNavigatorSettings({ settings: this.state.navigatorConfig }); - await this.graphQLService.sdk.configureServer({ - configuration: { - ...this.state.serverConfig, - sessionExpireTime: (this.state.serverConfig.sessionExpireTime ?? 30) * 1000 * 60, - }, - }); - await this.serverConfigResource.update(); - this.usersResource.refreshAllLazy(); + const config = await this.serverConfigResource.load(); + + if (!config) { + return; + } + + data.state.serverConfig.serverName = config.name || config.productInfo.name; + data.state.serverConfig.sessionExpireTime = config.sessionExpireTime; + + data.state.serverConfig.adminCredentialsSaveEnabled = config.adminCredentialsSaveEnabled; + data.state.serverConfig.publicCredentialsSaveEnabled = config.publicCredentialsSaveEnabled; + data.state.serverConfig.customConnectionsEnabled = config.supportsCustomConnections; + + data.state.navigatorConfig = { ...config.defaultNavigatorSettings }; + } catch (exception) { + ExecutorInterrupter.interrupt(contexts); + this.notificationService.logException(exception, 'Can\'t load server configuration'); + } + }; + + private getSaveData(finish: boolean): IServerConfigSaveData { + return { + state: this.state, + finish, + configurationWizard: this.administrationScreenService.isConfigurationMode, + }; + } + + private save: IExecutorHandler = async (data, contexts) => { + const validation = contexts.getContext(serverConfigValidationContext); + + if (!validation.getState()) { + return; + } + + try { + await this.serverConfigResource.setDefaultNavigatorSettings(data.state.navigatorConfig); + if (!data.configurationWizard || data.finish) { + await this.serverConfigResource.save(); + } } catch (exception) { this.notificationService.logException(exception, 'Can\'t save server configuration'); throw exception; } - } + }; - async handleConfigurationFinish(): Promise { - await this.save(); - } + private ensureValidation: IExecutorHandler = (data, contexts) => { + const validation = contexts.getContext(serverConfigValidationContext); - async validate(): Promise { - const context = await this.validationTask.execute(true); - const state = await context.getContext(this.validationStatusContext); + if (!validation.getState()) { + ExecutorInterrupter.interrupt(contexts); + this.done = false; + } else { + this.done = true; + } + }; - return state.getState(); - } + private validateForm: IExecutorHandler = (data, contexts) => { + const validation = contexts.getContext(serverConfigValidationContext); - private isFormFilled() { - if (!this.state?.serverConfig.serverName) { + if (!this.isFormFilled(data.state)) { + validation.invalidate(); + } + }; + + private isFormFilled(state: IServerConfigurationPageState) { + if (!state.serverConfig.serverName) { return false; } - if ((this.state.serverConfig.sessionExpireTime ?? 0) < 1) { + if ((state.serverConfig.sessionExpireTime ?? 0) < 1) { return false; } - if (this.administrationScreenService.isConfigurationMode) { - if (!this.state.serverConfig.adminName - || this.state.serverConfig.adminName.length < 6 - || !this.state.serverConfig.adminPassword - ) { - return false; - } - } return true; } +} - private getConfig(config?: ServerConfig | null): IServerConfigurationPageState { - if (!config || config?.configurationMode) { - return { - serverConfig: { - serverName: 'CloudBeaver', - adminName: 'cbadmin', - adminPassword: '', - anonymousAccessEnabled: true, - authenticationEnabled: true, - adminCredentialsSaveEnabled: true, - publicCredentialsSaveEnabled: true, - customConnectionsEnabled: true, - sessionExpireTime: 30, - }, - navigatorConfig: { ...DEFAULT_NAVIGATOR_VIEW_SETTINGS }, - }; - } +export interface IValidationStatusContext { + getState: () => boolean; + invalidate: () => void; +} - return { - serverConfig: { - serverName: config.name, - anonymousAccessEnabled: config.anonymousAccessEnabled, - authenticationEnabled: config.authenticationEnabled, - adminCredentialsSaveEnabled: config.adminCredentialsSaveEnabled, - publicCredentialsSaveEnabled: config.publicCredentialsSaveEnabled, - customConnectionsEnabled: config.supportsCustomConnections, - sessionExpireTime: (config.sessionExpireTime ?? 1800000) / 1000 / 60, - }, - navigatorConfig: { ...config.defaultNavigatorSettings }, - }; - } +export function serverConfigValidationContext(): IValidationStatusContext { + let state = true; + + const invalidate = () => { + state = false; + }; + const getState = () => state; + + return { + getState, + invalidate, + }; +} + +export function serverConfigStateContext(): IServerConfigurationPageState { + return { + navigatorConfig: { ...DEFAULT_NAVIGATOR_VIEW_SETTINGS }, + serverConfig: {}, + }; } diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/Welcome/WelcomePage.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/Welcome/WelcomePage.tsx index 1062d56acc..c56ada831c 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/Welcome/WelcomePage.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/Welcome/WelcomePage.tsx @@ -8,7 +8,7 @@ import styled, { css } from 'reshadow'; -import { BASE_CONTAINERS_STYLES, ColoredContainer, Container, Group, GroupItem } from '@cloudbeaver/core-blocks'; +import { BASE_CONTAINERS_STYLES, ColoredContainer, Group, GroupItem } from '@cloudbeaver/core-blocks'; import { Translate } from '@cloudbeaver/core-localization'; import { useStyles } from '@cloudbeaver/core-theming'; @@ -25,20 +25,14 @@ const styles = css` export const WelcomePage: React.FC = function WelcomePage() { return styled(useStyles(styles, BASE_CONTAINERS_STYLES))( - - - - - -

-
- -

- -
-
-
-
+ + +

+ +

+ +
+
); }; diff --git a/webapp/packages/plugin-administration/src/index.ts b/webapp/packages/plugin-administration/src/index.ts index 7128d5d867..939b406c75 100644 --- a/webapp/packages/plugin-administration/src/index.ts +++ b/webapp/packages/plugin-administration/src/index.ts @@ -2,8 +2,9 @@ import { manifest } from './manifest'; export default manifest; -export * from './ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationAdminForm'; export * from './ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationConfigurationForm'; export * from './ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationInfoForm'; export * from './ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationNavigatorViewForm'; export * from './ConfigurationWizard/ServerConfiguration/Form/ServerConfigurationSecurityForm'; +export * from './ConfigurationWizard/ServerConfiguration/IServerConfigurationPageState'; +export * from './ConfigurationWizard/ServerConfiguration/ServerConfigurationService'; diff --git a/webapp/packages/plugin-administration/src/locales/en.ts b/webapp/packages/plugin-administration/src/locales/en.ts index 05401d53bf..65eac90e24 100644 --- a/webapp/packages/plugin-administration/src/locales/en.ts +++ b/webapp/packages/plugin-administration/src/locales/en.ts @@ -10,26 +10,24 @@ export default [ ['administration_configuration_wizard_configuration_step_description', 'Main server configuration'], ['administration_configuration_wizard_configuration_title', 'You can configure the main server parameters here.'], ['administration_configuration_wizard_configuration_message', 'Administrator is a super user who can configure server, set databases connections, manage other users and much more. Please, remember the entered password. It is not possible to recover administrator password automatically.'], - ['administration_configuration_wizard_configuration_plugins', 'Configuration'], - ['administration_configuration_wizard_configuration_server_info', 'Server Info'], + + ['administration_configuration_wizard_configuration_server_info', 'Server Information'], ['administration_configuration_wizard_configuration_server_name', 'Server Name'], ['administration_configuration_wizard_configuration_server_session_lifetime', 'Session lifetime'], - ['administration_configuration_wizard_configuration_admin', 'Administrator'], - ['administration_configuration_wizard_configuration_admin_name', 'Admin user name'], - ['administration_configuration_wizard_configuration_admin_password', 'Admin password'], - ['administration_configuration_wizard_configuration_anonymous_access', 'Allow anonymous access'], - ['administration_configuration_wizard_configuration_anonymous_access_description', 'Allows to work with CloudBeaver without user authentication'], - ['administration_configuration_wizard_configuration_authentication', 'Enable users authentication'], - ['administration_configuration_wizard_configuration_authentication_description', 'Allows users to authenticate. Otherwise only anonymous access is enabled'], + ['administration_configuration_wizard_configuration_server_session_lifetime_description', 'Here you may specify the number of minutes that you wish the session to be allowed to remain idle before it expires'], + + ['administration_configuration_wizard_configuration_plugins', 'Configuration'], ['administration_configuration_wizard_configuration_custom_connections', 'Enable custom connections'], ['administration_configuration_wizard_configuration_custom_connections_description', 'Allows users to create custom connections. Otherwise, all new connections can be created from the administration part only'], ['administration_configuration_wizard_configuration_navigation_tree_view', 'Navigator simple view'], ['administration_configuration_wizard_configuration_navigation_tree_view_description', "By default, all user's new connections will contain only basic information in navigation tree"], + ['administration_configuration_wizard_configuration_security', 'Security'], - ['administration_configuration_wizard_configuration_security_admin_credentials', 'Credentials saving'], - ['administration_configuration_wizard_configuration_security_public_credentials', 'Users credentials saving'], - ['administration_configuration_wizard_configuration_security_admin_credentials_description', 'Allow to save credentials'], - ['administration_configuration_wizard_configuration_security_public_credentials_description', 'Allow to save credentials for regular users'], + ['administration_configuration_wizard_configuration_security_admin_credentials', 'Save credentials'], + ['administration_configuration_wizard_configuration_security_admin_credentials_description', 'Allow to save credentials for pre-configured database'], + ['administration_configuration_wizard_configuration_security_public_credentials', 'Save users credentials'], + ['administration_configuration_wizard_configuration_security_public_credentials_description', 'Allow to save credentials for non-admin users'], + ['administration_configuration_wizard_configuration_navigator', 'Navigator'], ['administration_configuration_wizard_configuration_navigator_hide_folders', 'Hide Folders'], ['administration_configuration_wizard_configuration_navigator_hide_schemas', 'Hide Schemas'], @@ -38,6 +36,7 @@ export default [ ['administration_configuration_wizard_configuration_navigator_show_only_entities', 'Only Entities'], ['administration_configuration_wizard_configuration_navigator_show_system_objects', 'System Objects'], ['administration_configuration_wizard_configuration_navigator_show_utility_objects', 'Utility Objects'], + ['administration_configuration_wizard_finish', 'Confirmation'], ['administration_configuration_wizard_finish_step_description', 'Confirmation'], ['administration_configuration_wizard_finish_title', 'That is almost it.'], diff --git a/webapp/packages/plugin-administration/src/locales/ru.ts b/webapp/packages/plugin-administration/src/locales/ru.ts index db2831ea6e..e996b1e63b 100644 --- a/webapp/packages/plugin-administration/src/locales/ru.ts +++ b/webapp/packages/plugin-administration/src/locales/ru.ts @@ -1,6 +1,7 @@ export default [ ['administration_configuration_wizard_configuration', 'Настройки сервера'], ['administration_configuration_wizard_configuration_server_session_lifetime', 'Время сессии'], + ['administration_configuration_wizard_configuration_server_session_lifetime_description', 'В случае бездействия пользователя на протяжении указанного количества минут, его сессия будет закрыта.'], ['administration_configuration_wizard_configuration_custom_connections', 'Разрешить пользовательские подключения'], ['administration_configuration_wizard_configuration_custom_connections_description', 'Позволяет пользователям создавать собственные подключения. Иначе, любые подключения могут быть добавлены только со страницы администрирования'], ['administration_configuration_wizard_configuration_navigation_tree_view', 'Упрощенная версия дерева навигации'], diff --git a/webapp/packages/plugin-authentication/package.json b/webapp/packages/plugin-authentication/package.json index 848559426b..d13d88460e 100644 --- a/webapp/packages/plugin-authentication/package.json +++ b/webapp/packages/plugin-authentication/package.json @@ -32,6 +32,7 @@ "@cloudbeaver/core-settings": "^0.1.0", "@cloudbeaver/core-theming": "^0.1.0", "@cloudbeaver/core-utils": "^0.1.0", + "@cloudbeaver/plugin-administration": "^0.1.0", "react": "^16.x.x", "rxjs": "^6.x.x", "mobx": "^5.x.x", diff --git a/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/AuthenticationProviders.tsx b/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/AuthenticationProviders.tsx new file mode 100644 index 0000000000..11a8d790f0 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/AuthenticationProviders.tsx @@ -0,0 +1,112 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { useContext } from 'react'; +import styled from 'reshadow'; + +import { AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; +import { BASE_CONTAINERS_STYLES, Container, FormContext, Group, GroupTitle, Loader, PlaceholderComponent, SwitchNew, useExecutor, useMapResource } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { useStyles } from '@cloudbeaver/core-theming'; +import type { IConfigurationPlaceholderProps } from '@cloudbeaver/plugin-administration'; + +import { ServerConfigurationAdminForm } from './ServerConfigurationAdminForm'; + +export const AuthenticationProviders: PlaceholderComponent = observer(function AuthenticationProviders({ + state: { serverConfig }, + configurationWizard, +}) { + const providers = useMapResource(AuthProvidersResource, AuthProvidersResource.keyAll); + const translate = useTranslate(); + const styles = useStyles(BASE_CONTAINERS_STYLES); + const formContext = useContext(FormContext); + + if (formContext === null) { + throw new Error('Form state should be provided'); + } + + const localExists = providers.resource.has(AUTH_PROVIDER_LOCAL_ID); + const externalAuthentication = providers.data.length === 1 && !localExists; + const providersSelectable = providers.data.length > 1; + + useExecutor({ + executor: formContext.changeExecutor, + handlers: [function switchControls() { + if (externalAuthentication) { + serverConfig.enabledAuthProviders = [...providers.resource.keys]; + serverConfig.authenticationEnabled = true; + } + + if (serverConfig.enabledAuthProviders?.length === 0) { + serverConfig.authenticationEnabled = false; + } + + if (!serverConfig.authenticationEnabled) { + serverConfig.anonymousAccessEnabled = true; + } + }], + }); + + return styled(styles)( + + + {translate('administration_configuration_wizard_configuration_authentication_group')} + + {translate('administration_configuration_wizard_configuration_anonymous_access')} + + + {translate('administration_configuration_wizard_configuration_authentication')} + + + {() => providersSelectable && styled(styles)( + <> + {translate('administration_configuration_wizard_configuration_authentication_provider')} + {providers.data.map(provider => provider && ( + + {provider.label} + + ))} + + )} + + + {configurationWizard && localExists ? ( + + ) : ( + + )} + + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/ServerConfigurationAdminForm.tsx b/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/ServerConfigurationAdminForm.tsx new file mode 100644 index 0000000000..8f6694920e --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/ServerConfigurationAdminForm.tsx @@ -0,0 +1,55 @@ + +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 styled from 'reshadow'; + +import { BASE_CONTAINERS_STYLES, Group, GroupTitle, InputFieldNew } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import type { ServerConfigInput } from '@cloudbeaver/core-sdk'; +import { useStyles } from '@cloudbeaver/core-theming'; + +interface Props { + serverConfig: ServerConfigInput; +} + +export const ServerConfigurationAdminForm: React.FC = observer(function ServerConfigurationAdminForm({ + serverConfig, +}) { + const translate = useTranslate(); + const style = useStyles(BASE_CONTAINERS_STYLES); + + return styled(style)( + + {translate('administration_configuration_wizard_configuration_admin')} + + {translate('administration_configuration_wizard_configuration_admin_name')} + + + {translate('administration_configuration_wizard_configuration_admin_password')} + + + ); +}); diff --git a/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/ServerConfigurationAuthenticationBootstrap.ts b/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/ServerConfigurationAuthenticationBootstrap.ts new file mode 100644 index 0000000000..dea87f8953 --- /dev/null +++ b/webapp/packages/plugin-authentication/src/Administration/ServerConfiguration/ServerConfigurationAuthenticationBootstrap.ts @@ -0,0 +1,83 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor'; +import { ServerConfigResource } from '@cloudbeaver/core-root'; +import { ILoadConfigData, IServerConfigSaveData, ServerConfigurationService, serverConfigValidationContext } from '@cloudbeaver/plugin-administration'; + +@injectable() +export class ServerConfigurationAuthenticationBootstrap extends Bootstrap { + constructor( + private readonly serverConfigurationService: ServerConfigurationService, + private readonly authProvidersResource: AuthProvidersResource, + private readonly serverConfigResource: ServerConfigResource, + private readonly notificationService: NotificationService, + ) { + super(); + } + + register(): void { + this.serverConfigurationService.validationTask.addHandler(this.validateForm); + this.serverConfigurationService.loadConfigTask.addHandler(this.loadServerConfig); + } + + load(): void { } + + private loadServerConfig: IExecutorHandler = async (data, contexts) => { + if (!data.reload) { + return; + } + + try { + await this.authProvidersResource.loadAll(); + const config = await this.serverConfigResource.load(); + + if (!config) { + return; + } + + if (config.configurationMode) { + if (this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID)) { + data.state.serverConfig.adminName = 'cbadmin'; + data.state.serverConfig.adminPassword = ''; + } + } else { + data.state.serverConfig.adminName = undefined; + data.state.serverConfig.adminPassword = undefined; + } + + data.state.serverConfig.anonymousAccessEnabled = config.anonymousAccessEnabled; + data.state.serverConfig.authenticationEnabled = config.authenticationEnabled; + data.state.serverConfig.enabledAuthProviders = [...config.enabledAuthProviders]; + } catch (exception) { + ExecutorInterrupter.interrupt(contexts); + this.notificationService.logException(exception, 'Can\'t load server configuration'); + } + }; + + private validateForm: IExecutorHandler = async (data, contexts) => { + await this.authProvidersResource.loadAll(); + const administratorPresented = data.configurationWizard && this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID); + + if (!administratorPresented) { + return; + } + + const validation = contexts.getContext(serverConfigValidationContext); + + if (!data.state.serverConfig.adminName + || data.state.serverConfig.adminName.length < 6 + || !data.state.serverConfig.adminPassword + ) { + validation.invalidate(); + } + }; +} diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserFormBaseBootstrap.ts b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserFormBaseBootstrap.ts index 573ba90f63..8473951812 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserFormBaseBootstrap.ts +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserFormBaseBootstrap.ts @@ -6,6 +6,7 @@ * you may not use this file except in compliance with the License. */ +import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; import { injectable, Bootstrap } from '@cloudbeaver/core-di'; import { ConnectionAccess } from './ConnectionAccess'; @@ -32,7 +33,7 @@ export class UserFormBaseBootstrap extends Bootstrap { this.userFormService.tabsContainer.add({ key: 'origin', order: 2, - isHidden: (tabId, props) => props?.user.origin.type === 'local', + isHidden: (tabId, props) => props?.user.origin.type === AUTH_PROVIDER_LOCAL_ID, panel: () => OriginInfo, tab: () => OriginInfoTab, }); diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserInfo.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserInfo.tsx index 61b575f16e..f6247dbba0 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserInfo.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UserForm/UserInfo.tsx @@ -100,7 +100,7 @@ export const UserInfo: TabContainerPanelComponent = observer(fun handleRoleChange(role.roleId, checked)} diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts index 1ec14cb0a8..9b5b04f7aa 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationController.ts @@ -8,7 +8,7 @@ import { observable, computed, makeObservable } from 'mobx'; -import { AdminUser, AuthProvidersResource, UsersResource } from '@cloudbeaver/core-authentication'; +import { AdminUser, AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID, UsersResource } from '@cloudbeaver/core-authentication'; import { injectable, IInitializableController } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -40,7 +40,7 @@ export class UsersAdministrationController implements IInitializableController { } get isLocalProviderAvailable(): boolean { - return this.authProvidersResource.data.some(({ id }) => id === 'local'); + return this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID); } get isLoading(): boolean { @@ -65,7 +65,7 @@ export class UsersAdministrationController implements IInitializableController { } init(): void { - this.authProvidersResource.load(); + this.authProvidersResource.loadAll(); } update = async () => { diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationService.ts b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationService.ts index a1dbaccc4b..7fc54f79cd 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationService.ts +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersAdministrationService.ts @@ -18,9 +18,13 @@ import { UsersAdministrationNavigationService } from './UsersAdministrationNavig import { UsersDrawerItem } from './UsersDrawerItem'; import { Origin } from './UsersTable/UserDetailsInfo/Origin'; +export interface IUserDetailsInfoProps { + user: AdminUser; +} + @injectable() export class UsersAdministrationService extends Bootstrap { - readonly userDetailsInfoPlaceholder = new PlaceholderContainer(); + readonly userDetailsInfoPlaceholder = new PlaceholderContainer(); constructor( private administrationItemService: AdministrationItemService, diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx index dc3b43b611..a6213ae26c 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/User.tsx @@ -46,7 +46,7 @@ export const User: React.FC = observer(function User({ user, selectable } {user.userId} {user.grantedRoles.join(', ')} - + ); diff --git a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserDetailsInfo/Origin.tsx b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserDetailsInfo/Origin.tsx index 9cce93b18d..883fc7ca67 100644 --- a/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserDetailsInfo/Origin.tsx +++ b/webapp/packages/plugin-authentication/src/Administration/Users/UsersTable/UserDetailsInfo/Origin.tsx @@ -10,12 +10,10 @@ import { observer } from 'mobx-react-lite'; import styled from 'reshadow'; import { css } from 'reshadow'; -import type { AdminUser } from '@cloudbeaver/core-authentication'; -import { StaticImage } from '@cloudbeaver/core-blocks'; +import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; +import { PlaceholderComponent, StaticImage } from '@cloudbeaver/core-blocks'; -interface Props { - context: AdminUser; -} +import type { IUserDetailsInfoProps } from '../../UsersAdministrationService'; const USER_DETAILS_STYLES = css` StaticImage { @@ -24,10 +22,10 @@ const USER_DETAILS_STYLES = css` } `; -export const Origin: React.FC = observer(function Origin({ context }) { - const isLocal = context.origin.type === 'local'; - const icon = isLocal ? '/icons/local_connection.svg' : context.origin.icon; - const title = isLocal ? 'Local user' : context.origin.displayName; +export const Origin: PlaceholderComponent = observer(function Origin({ user }) { + const isLocal = user.origin.type === AUTH_PROVIDER_LOCAL_ID; + const icon = isLocal ? '/icons/local_connection.svg' : user.origin.icon; + const title = isLocal ? 'Local user' : user.origin.displayName; return styled(USER_DETAILS_STYLES)( diff --git a/webapp/packages/plugin-authentication/src/AuthenticationService.ts b/webapp/packages/plugin-authentication/src/AuthenticationService.ts index 8e991dcb5e..234609abcb 100644 --- a/webapp/packages/plugin-authentication/src/AuthenticationService.ts +++ b/webapp/packages/plugin-authentication/src/AuthenticationService.ts @@ -8,10 +8,13 @@ import { AdministrationScreenService } from '@cloudbeaver/core-administration'; import { AppScreenService } from '@cloudbeaver/core-app'; -import { AppAuthService, UserInfoResource } from '@cloudbeaver/core-authentication'; +import { AppAuthService, AuthProviderContext, AuthProviderService, AuthProvidersResource, AUTH_PROVIDER_LOCAL_ID, UserInfoResource } from '@cloudbeaver/core-authentication'; import { injectable, Bootstrap } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; +import type { IExecutorHandler } from '@cloudbeaver/core-executor'; +import { SessionDataResource } from '@cloudbeaver/core-root'; import { ScreenService } from '@cloudbeaver/core-routing'; +import type { ObjectOrigin } from '@cloudbeaver/core-sdk'; import { AuthDialogService } from './Dialog/AuthDialogService'; @@ -25,14 +28,17 @@ export class AuthenticationService extends Bootstrap { private authDialogService: AuthDialogService, private userInfoResource: UserInfoResource, private notificationService: NotificationService, - private readonly administrationScreenService: AdministrationScreenService + private readonly administrationScreenService: AdministrationScreenService, + private readonly authProviderService: AuthProviderService, + private readonly authProvidersResource: AuthProvidersResource, + private readonly sessionDataResource: SessionDataResource, ) { super(); this.authPromise = null; } - async authUser(provider: string | null = null): Promise { - await this.auth(false, provider); + async authUser(provider: string | null = null, link?: boolean): Promise { + await this.auth(false, provider, link); } async logout(): Promise { @@ -47,11 +53,11 @@ export class AuthenticationService extends Bootstrap { } } - private async auth(persistent: boolean, provider: string | null = null) { + private async auth(persistent: boolean, provider: string | null = null, link?: boolean) { if (this.authPromise) { return this.authPromise; } - this.authPromise = this.authDialogService.showLoginForm(persistent, provider); + this.authPromise = this.authDialogService.showLoginForm(persistent, provider, link); try { await this.authPromise; } finally { @@ -69,11 +75,8 @@ export class AuthenticationService extends Bootstrap { } register(): void { - this.appAuthService.auth.addPostHandler(state => { - if (!state) { - this.requireAuthentication(); - } - }); + this.sessionDataResource.beforeLoad.addPostHandler(() => { this.requireAuthentication(); }); + this.appScreenService.activation.addHandler(() => this.requireAuthentication()); this.administrationScreenService.ensurePermissions.addHandler(async () => { const userInfo = await this.userInfoResource.load(); @@ -83,7 +86,32 @@ export class AuthenticationService extends Bootstrap { await this.auth(false); }); + this.authProviderService.requestAuthProvider.addHandler(this.requestAuthProviderHandler); } load(): void { } + + private requestAuthProviderHandler: IExecutorHandler = async (data, contexts) => { + if (data.type === AUTH_PROVIDER_LOCAL_ID) { + const provider = contexts.getContext(AuthProviderContext); + provider.auth(); + return; + } + + await this.authProvidersResource.loadAll(); + await this.userInfoResource.load(); + + if (!this.authProvidersResource.has(data.subType ?? data.type)) { + return; + } + + if (!this.userInfoResource.hasToken(data.type, data.subType)) { + await this.auth(false, data.subType ?? data.type); + } + + if (this.userInfoResource.hasToken(data.type, data.subType)) { + const provider = contexts.getContext(AuthProviderContext); + provider.auth(); + } + }; } diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx index 08c85f1331..72f309eb4d 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialog.tsx @@ -9,10 +9,11 @@ import { observer } from 'mobx-react-lite'; import styled, { css } from 'reshadow'; +import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { SubmittingForm, ErrorMessage, TabsState, TabList, Tab, TabTitle, Loader } from '@cloudbeaver/core-blocks'; -import { useController } from '@cloudbeaver/core-di'; +import { useController, useService } from '@cloudbeaver/core-di'; import { CommonDialogWrapper, DialogComponent } from '@cloudbeaver/core-dialogs'; import { useTranslate } from '@cloudbeaver/core-localization'; import { composes, useStyles } from '@cloudbeaver/core-theming'; @@ -77,39 +78,58 @@ const styles = composes( bottom: 0; padding: 8px 24px; } + auth-token-info-message { + composes: theme-typography--caption from global; + padding: 8px 24px; + } ` ); -export const AuthDialog: DialogComponent = observer(function AuthDialog({ - payload, +interface IAuthPayload { + provider: string | null; + link?: boolean; +} + +export const AuthDialog: DialogComponent = observer(function AuthDialog({ + payload: { provider, link }, options, rejectDialog, }) { - const controller = useController(AuthDialogController, rejectDialog); + const userInfo = useService(UserInfoResource); + const controller = useController(AuthDialogController, link || false, rejectDialog); const translate = useTranslate(); - if (payload) { - controller.selectProvider(payload); + if (provider) { + controller.selectProvider(provider); } - const showTabs = !payload && controller.providers.length > 1; + const showTabs = !provider && controller.providers.length > 1; + + const additional = userInfo.data !== null && controller.provider?.id && !userInfo.hasToken(controller.provider?.id); return styled(useStyles(styles))( - {controller.providers.map(provider => ( - controller.selectProvider(provider.id)} - > - {provider.label} - - ))} - + icon={controller.provider?.icon} + header={( + <> + {additional && {translate('authentication_request_token')}} + {showTabs && ( + + {controller.providers.map(provider => ( + controller.selectProvider(provider.id)} + > + {provider.label} + + ))} + + )} + )} footer={( void; constructor( @@ -50,7 +50,8 @@ export class AuthDialogController implements IInitializableController, IDestruct }); } - init(onClose: () => void) { + init(link: boolean, onClose: () => void) { + this.link = link; this.close = onClose; this.loadProviders(); } @@ -66,8 +67,7 @@ export class AuthDialogController implements IInitializableController, IDestruct this.isAuthenticating = true; try { - await this.authInfoService.logout(); - await this.authInfoService.login(this.provider.id, this.credentials); + await this.authInfoService.login(this.provider.id, this.credentials, this.link); this.close(); } catch (exception) { if (!this.error.catch(exception) || this.isDistructed) { @@ -82,8 +82,7 @@ export class AuthDialogController implements IInitializableController, IDestruct if (providerId === this.provider?.id) { return; } - this.provider = this.authProvidersResource - .data.find(provider => provider.id === providerId) || null; + this.provider = this.authProvidersResource.get(providerId) || null; this.credentials = {}; }; @@ -95,7 +94,7 @@ export class AuthDialogController implements IInitializableController, IDestruct private async loadProviders() { try { - await this.authProvidersResource.load(); + await this.authProvidersResource.loadAll(); if (this.providers.length > 0) { this.provider = this.providers[0]; } diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx index c867915674..ba585b160e 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogFooter.tsx @@ -28,24 +28,23 @@ export interface Props { onLogin: () => void; } -export const AuthDialogFooter = observer( - function AuthDialogFooter({ - isAuthenticating, - onLogin, - }: Props) { - const translate = useTranslate(); +export const AuthDialogFooter = observer(function AuthDialogFooter({ + isAuthenticating, + onLogin, +}: Props) { + const translate = useTranslate(); - return styled(styles)( - - - - ); - } + return styled(styles)( + + + + ); +} ); diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts index 018b1cfcff..d86acbe9c2 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthDialogService.ts @@ -17,7 +17,7 @@ export class AuthDialogService { private commonDialogService: CommonDialogService ) { } - async showLoginForm(persistent = false, provider: string | null = null): Promise { - await this.commonDialogService.open(AuthDialog, provider, { persistent }); + async showLoginForm(persistent = false, provider: string | null = null, link?: boolean): Promise { + await this.commonDialogService.open(AuthDialog, { provider, link }, { persistent }); } } diff --git a/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/AuthProviderForm.tsx b/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/AuthProviderForm.tsx index 6852779d20..9d99542743 100644 --- a/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/AuthProviderForm.tsx +++ b/webapp/packages/plugin-authentication/src/Dialog/AuthProviderForm/AuthProviderForm.tsx @@ -37,6 +37,7 @@ export const AuthProviderForm = observer(function AuthProviderForm({ {provider.credentialParameters.map(parameter => parameter.user && ( { } diff --git a/webapp/packages/plugin-authentication/src/locales/en.ts b/webapp/packages/plugin-authentication/src/locales/en.ts index e274042d15..b2ca22123f 100644 --- a/webapp/packages/plugin-authentication/src/locales/en.ts +++ b/webapp/packages/plugin-authentication/src/locales/en.ts @@ -2,6 +2,7 @@ export default [ ['authentication_login_dialog_title', 'Authentication'], ['authentication_login', 'Login'], ['authentication_logout', 'Logout'], + ['authentication_request_token', 'Additional authentication is required'], ['authentication_administration_user_connections_user_add', 'User Creation'], ['authentication_administration_user_connections_user_new', 'New user'], ['authentication_administration_user_connections_access_load_fail', 'User\'s granted connections loading failed'], @@ -28,4 +29,13 @@ export default [ ['authentication_user_passwords_not_match', "Passwords don't match"], ['authentication_user_login_already_exists', 'The user with this name already exists'], ['authentication_user_login_cant_be_used', 'Sorry, that name cannot be used'], + ['administration_configuration_wizard_configuration_admin', 'Administrator Credentials'], + ['administration_configuration_wizard_configuration_admin_name', 'Login'], + ['administration_configuration_wizard_configuration_admin_password', 'Password'], + ['administration_configuration_wizard_configuration_anonymous_access', 'Allow anonymous access'], + ['administration_configuration_wizard_configuration_anonymous_access_description', 'Allows to work with CloudBeaver without user authentication'], + ['administration_configuration_wizard_configuration_authentication_group', 'Authentication settings'], + ['administration_configuration_wizard_configuration_authentication_provider', 'Authentication Source'], + ['administration_configuration_wizard_configuration_authentication', 'Enable users authentication'], + ['administration_configuration_wizard_configuration_authentication_description', 'Allows users to authenticate. Otherwise only anonymous access is enabled'], ]; diff --git a/webapp/packages/plugin-authentication/src/manifest.ts b/webapp/packages/plugin-authentication/src/manifest.ts index c5c62fdaf0..311dff607d 100644 --- a/webapp/packages/plugin-authentication/src/manifest.ts +++ b/webapp/packages/plugin-authentication/src/manifest.ts @@ -8,6 +8,7 @@ import type { PluginManifest } from '@cloudbeaver/core-di'; +import { ServerConfigurationAuthenticationBootstrap } from './Administration/ServerConfiguration/ServerConfigurationAuthenticationBootstrap'; import { CreateUserService } from './Administration/Users/CreateUserService'; import { UserFormBaseBootstrap } from './Administration/Users/UserForm/UserFormBaseBootstrap'; import { UserFormService } from './Administration/Users/UserForm/UserFormService'; @@ -15,8 +16,8 @@ import { UsersAdministrationNavigationService } from './Administration/Users/Use import { UsersAdministrationService } from './Administration/Users/UsersAdministrationService'; import { AuthenticationLocaleService } from './AuthenticationLocaleService'; import { AuthenticationService } from './AuthenticationService'; -import { AuthMenuService } from './AuthMenuService'; import { AuthDialogService } from './Dialog/AuthDialogService'; +import { PluginBootstrap } from './PluginBootstrap'; export const manifest: PluginManifest = { info: { @@ -26,11 +27,12 @@ export const manifest: PluginManifest = { providers: [ AuthenticationService, AuthDialogService, - AuthMenuService, + PluginBootstrap, UsersAdministrationService, AuthenticationLocaleService, CreateUserService, UsersAdministrationNavigationService, + ServerConfigurationAuthenticationBootstrap, UserFormService, UserFormBaseBootstrap, ], diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionController.ts b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionController.ts index aa4d40c714..29956bda46 100644 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionController.ts +++ b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionController.ts @@ -9,7 +9,7 @@ import { observable, makeObservable } from 'mobx'; import { - DBDriverResource, Connection, DatabaseAuthModelsResource, ConnectionInfoResource, DBDriver, ConnectionInitConfig + DBDriverResource, Connection, DatabaseAuthModelsResource, ConnectionInfoResource, DBDriver, ConnectionInitConfig, getUniqueConnectionName } from '@cloudbeaver/core-connections'; import type { IFormInitConfig } from '@cloudbeaver/core-connections'; import { injectable, IInitializableController, IDestructibleController } from '@cloudbeaver/core-di'; @@ -34,7 +34,7 @@ export interface IConnectionController { @injectable() export class ConnectionController -implements IInitializableController, IDestructibleController, IConnectionController { + implements IInitializableController, IDestructibleController, IConnectionController { step = ConnectionStep.ConnectionTemplateSelect; isLoading = true; isConnecting = false; @@ -114,12 +114,14 @@ implements IInitializableController, IDestructibleController, IConnectionControl this.isConnecting = true; this.clearError(); try { - const connection = await this.connectionInfoResource.createFromTemplate(this.template.id); + const connectionNames = this.connectionInfoResource.values.map(connection => connection.name); + const uniqueConnectionName = getUniqueConnectionName(this.template.name || 'Template connection', connectionNames); + const connection = await this.connectionInfoResource.createFromTemplate(this.template.id, uniqueConnectionName); try { await this.connectionInfoResource.init(this.getConfig(connection.id)); - this.notificationService.logSuccess({ title: `Connection ${connection.name} established` }); + this.notificationService.logSuccess({ title: 'Connection is established', message: connection.name }); this.onClose(); } catch (exception) { this.showError(exception, 'Failed to establish connection'); diff --git a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx index b1161b584d..fe77281104 100644 --- a/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx +++ b/webapp/packages/plugin-connection-template/src/ConnectionDialog/ConnectionDialog.tsx @@ -11,7 +11,15 @@ import styled, { css } from 'reshadow'; import { useAdministrationSettings } from '@cloudbeaver/core-administration'; import { - ErrorMessage, SubmittingForm, Loader, useFocus, ObjectPropertyInfoForm, FormBox, FormBoxElement, FormGroup, FieldCheckbox + ErrorMessage, + SubmittingForm, + Loader, + useFocus, + ObjectPropertyInfoForm, + FormBox, + FormBoxElement, + FormGroup, + FieldCheckbox } from '@cloudbeaver/core-blocks'; import { SSH_TUNNEL_ID, SSHAuthForm } from '@cloudbeaver/core-connections'; import { useController } from '@cloudbeaver/core-di'; @@ -112,7 +120,7 @@ export const ConnectionDialog = observer(function ConnectionDialog({ diff --git a/webapp/packages/plugin-connection-template/src/TemplateConnectionsResource.ts b/webapp/packages/plugin-connection-template/src/TemplateConnectionsResource.ts index a7faf70e95..a0823e9d72 100644 --- a/webapp/packages/plugin-connection-template/src/TemplateConnectionsResource.ts +++ b/webapp/packages/plugin-connection-template/src/TemplateConnectionsResource.ts @@ -6,10 +6,9 @@ * you may not use this file except in compliance with the License. */ -import { AppAuthService } from '@cloudbeaver/core-authentication'; import { Connection, ConnectionsResource } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; -import { EPermission, PermissionsService } from '@cloudbeaver/core-root'; +import { EPermission, PermissionsService, SessionDataResource } from '@cloudbeaver/core-root'; import { GraphQLService, CachedDataResource } from '@cloudbeaver/core-sdk'; @injectable() @@ -18,11 +17,11 @@ export class TemplateConnectionsResource extends CachedDataResource this.markOutdated()); - appAuthService.auth.addHandler(() => this.markOutdated()); + sessionDataResource.onDataOutdated.addHandler(() => this.markOutdated()); } isLoaded(): boolean { diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx index 9e798355a6..2542100ab2 100644 --- a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionForm.tsx @@ -65,7 +65,7 @@ export const PublicConnectionForm: React.FC = observer(function PublicConnection const save = useCallback(() => service.close(true), []); return styled(styles)( - + {() => ( > = observer(function const selectionContext = useContext(DataGridSelectionContext); const editingContext = useContext(EditingContext); const editor = dataGridContext?.model.source.getEditor(dataGridContext.resultIndex); + const dataGridApi = dataGridContext?.getDataGridApi(); const classes: string[] = []; const { rowIdx, column } = props; @@ -41,6 +42,10 @@ export const CellRenderer: React.FC> = observer(function } const handleMouseDown = useCallback((event: React.MouseEvent) => { + if (dataGridApi) { + dataGridApi.selectCell({ idx: column.idx, rowIdx }); + } + selectionContext?.select( { colIdx: column.idx, diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts index 1df1047c3a..53017b49a8 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/DataGridContext.ts @@ -7,6 +7,7 @@ */ import { createContext } from 'react'; +import type { DataGridHandle } from 'react-data-grid'; import type { IExecutor } from '@cloudbeaver/core-executor'; import type { IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; @@ -21,6 +22,7 @@ export interface IDataGridContext { resultIndex: number; columnResize: IExecutor; getEditorPortal: () => HTMLDivElement | null; + getDataGridApi: () => DataGridHandle | null; } export const DataGridContext = createContext(null); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx index 4299f3ea6b..edfaf42ad6 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/TableColumnHeader/TableColumnHeader.tsx @@ -13,6 +13,7 @@ import styled, { css, use } from 'reshadow'; import { StaticImage, Icon } from '@cloudbeaver/core-blocks'; import type { SqlResultSet } from '@cloudbeaver/core-sdk'; +import { composes, useStyles } from '@cloudbeaver/core-theming'; import type { SortMode } from '@cloudbeaver/plugin-data-viewer'; import { DataGridContext } from '../DataGridContext'; @@ -45,7 +46,7 @@ const headerStyles = css` flex-grow: 1; } - sort-icon { + sort-icons { margin-left: 4px; display: flex; padding: 2px 4px; @@ -57,25 +58,41 @@ const headerStyles = css` cursor: pointer; } - sort-icon > Icon { + sort-icons > SortIcon { width: 8px; - fill: #cbcbcb; + fill: currentColor !important; } - sort-icon > Icon:last-child { + sort-icons > SortIcon:last-child { transform: scaleY(-1); } - sort-icon > Icon[|active] { - fill: #338ECC; - } - sort-icon:hover > Icon { + sort-icons:hover > SortIcon { width: 9px; } - sort-icon[|disabled] { + sort-icons[|disabled] { opacity: 0.7; cursor: default; } `; +const activeSortIcon = composes( + css` + Icon { + composes: theme-text-primary from global; + } + ` +); + +interface ISortIconProps { + active: boolean; + className?: string; +} + +const SortIcon: React.FC = function SortIcon({ active, className }) { + return styled(useStyles(active && activeSortIcon))( + + ); +}; + function getColumn(colIdx: number, source: SqlResultSet) { return source.columns?.[colIdx]; } @@ -134,10 +151,10 @@ export const TableColumnHeader: React.FC> = observer(fu {columnName} {sortable && ( - - - - + + + + )} ); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss b/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss index 8659d68489..52afbbd717 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss +++ b/webapp/packages/plugin-data-spreadsheet-new/src/styles/themes/_base-react-grid.scss @@ -13,7 +13,7 @@ $edited-color: #ff99006b; @mixin base-react-grid() { :global { .cb-react-grid-container:focus-within { - .rdg-cell-custom-selected { + .rdg-cell-custom-selected:not(.rdg-cell-custom-editing) { background-color: rgba(0, 145, 234, 0.3); } .rdg-cell-selected { diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataAction.ts new file mode 100644 index 0000000000..307b3524f8 --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/IDatabaseDataAction.ts @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { IDatabaseDataAction } from '../IDatabaseDataAction'; +import type { IDatabaseDataResult } from '../IDatabaseDataResult'; +import type { IResultSetElementKey } from './ResultSet/IResultSetElementKey'; + +export interface IDatabaseResultDataAction + extends IDatabaseDataAction { + getCellValue: (cell: IResultSetElementKey) => any; +} diff --git a/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts new file mode 100644 index 0000000000..e0fb9f986a --- /dev/null +++ b/webapp/packages/plugin-data-viewer/src/DatabaseDataModel/Actions/ResultSet/ResultSetDataAction.ts @@ -0,0 +1,29 @@ +import { ResultDataFormat } from '@cloudbeaver/core-sdk'; + +import type { IDatabaseResultSet } from '../../IDatabaseResultSet'; +import { databaseDataAction } from '../DatabaseDataActionDecorator'; +import type { IDatabaseResultDataAction } from '../IDatabaseDataAction'; +import type { IResultSetElementKey } from './IResultSetElementKey'; + +@databaseDataAction() +export class ResultSetDataAction implements IDatabaseResultDataAction { + static dataFormat = ResultDataFormat.Resultset; + result: IDatabaseResultSet; + + constructor( + result: IDatabaseResultSet) { + this.result = result; + } + + getCellValue(cell: IResultSetElementKey): any { + if (cell.row === undefined || cell.column === undefined || !this.result.data?.rows) { + return undefined; + } + + return this.result.data.rows[cell.row][cell.column]; + } + + updateResult(result: IDatabaseResultSet) { + this.result = result; + } +} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx index 487a696065..c9a3301961 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeader.tsx @@ -35,7 +35,7 @@ export const TableHeader = observer(function TableHeader({ return styled(styles)( - + ); }); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts index 370b5a529b..498b05d48e 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableHeaderService.ts @@ -12,9 +12,13 @@ import { injectable, Bootstrap } from '@cloudbeaver/core-di'; import type { DataModelWrapper } from '../DataModelWrapper'; import { TableWhereFilter } from './TableWhereFilter'; +export interface ITableHeaderPlaceholderProps { + model: DataModelWrapper; +} + @injectable() export class TableHeaderService extends Bootstrap { - readonly tableHeaderPlaceholder = new PlaceholderContainer(); + readonly tableHeaderPlaceholder = new PlaceholderContainer(); register(): void { this.tableHeaderPlaceholder.add(TableWhereFilter, 1); diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx index 0878ac2340..258c61309b 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableHeader/TableWhereFilter.tsx @@ -11,10 +11,11 @@ import { useCallback, useState } from 'react'; import styled, { css } from 'reshadow'; import { InlineEditor } from '@cloudbeaver/core-app'; +import type { PlaceholderComponent } from '@cloudbeaver/core-blocks'; import { useTranslate } from '@cloudbeaver/core-localization'; import { composes, useStyles } from '@cloudbeaver/core-theming'; -import type { DataModelWrapper } from '../DataModelWrapper'; +import type { ITableHeaderPlaceholderProps } from './TableHeaderService'; const styles = composes( css` @@ -30,37 +31,33 @@ const styles = composes( ` ); -interface Props { - context: DataModelWrapper; -} - -export const TableWhereFilter = observer(function TableWhereFilter({ - context, -}: Props) { +export const TableWhereFilter: PlaceholderComponent = observer(function TableWhereFilter({ + model, +}) { const translate = useTranslate(); - const [filterValue, setValue] = useState(() => context.source.options?.whereFilter || ''); + const [filterValue, setValue] = useState(() => model.source.options?.whereFilter || ''); const handleApply = useCallback(() => { - if (context.isLoading()) { + if (model.isLoading()) { return; } - context.source.options!.whereFilter = filterValue; - context.refresh(); - }, [context, filterValue]); + model.source.options!.whereFilter = filterValue; + model.refresh(); + }, [model, filterValue]); const resetFilter = useCallback(() => { - if (context.isLoading()) { + if (model.isLoading()) { return; } - const applyNeeded = context.source.options?.whereFilter === filterValue; + const applyNeeded = model.source.options?.whereFilter === filterValue; setValue(''); if (applyNeeded) { - context.source.options!.whereFilter = ''; - context.refresh(); + model.source.options!.whereFilter = ''; + model.refresh(); } - }, [context, filterValue]); + }, [model, filterValue]); return styled(useStyles(styles))( 1} + disabled={model.isLoading() || model.source.results.length > 1} simple onSave={handleApply} onUndo={resetFilter} diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx index 2ad7df332d..c2617d677b 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/ValuePanel/ValuePanel.tsx @@ -7,6 +7,7 @@ */ import { observer } from 'mobx-react-lite'; +import { useRef, useState } from 'react'; import styled, { css } from 'reshadow'; import { TabList, TabPanelList, TabsState, UNDERLINE_TAB_STYLES } from '@cloudbeaver/core-blocks'; @@ -69,9 +70,29 @@ export const ValuePanel: DataPresentationComponent = ob resultIndex, }) { const service = useService(DataValuePanelService); + const [currentTabId, setCurrentTabId] = useState(''); + const lastTabId = useRef(''); + + const displayed = service.getDisplayed({ dataFormat, model, resultIndex }); + + if (displayed.length > 0) { + const firstTabId = displayed[0].key; + if (firstTabId !== lastTabId.current) { + setCurrentTabId(firstTabId); + lastTabId.current = firstTabId; + } + } return styled(useStyles(styles, UNDERLINE_TAB_STYLES))( - + setCurrentTabId(tab.tabId)} + > diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts index 376b349a8c..f1e60a69dd 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/TextValue/TextValuePresentationBootstrap.ts @@ -23,9 +23,10 @@ export class TextValuePresentationBootstrap extends Bootstrap { key: 'text-presentation', options: { dataFormat: [ResultDataFormat.Resultset] }, name: 'data_viewer_presentation_value_text_title', + order: Number.MAX_SAFE_INTEGER, panel: () => TextValuePresentation, }); } - load(): void {} + load(): void { } } diff --git a/webapp/packages/plugin-data-viewer/src/index.ts b/webapp/packages/plugin-data-viewer/src/index.ts index f2fa42de8c..c63d044d45 100644 --- a/webapp/packages/plugin-data-viewer/src/index.ts +++ b/webapp/packages/plugin-data-viewer/src/index.ts @@ -5,6 +5,7 @@ export default manifest; export * from './DatabaseDataModel/Actions/ResultSet/IResultSetElementKey'; export * from './DatabaseDataModel/Actions/ResultSet/ResultSetFormatAction'; export * from './DatabaseDataModel/Actions/ResultSet/ResultSetSelectAction'; +export * from './DatabaseDataModel/Actions/ResultSet/ResultSetDataAction'; export * from './DatabaseDataModel/Actions/DatabaseDataActionDecorator'; export * from './DatabaseDataModel/Actions/IDatabaseDataFormatAction'; export * from './DatabaseDataModel/Actions/IDatabaseDataSelectAction'; @@ -23,6 +24,7 @@ export * from './DatabaseDataModel/IDatabaseResultSet'; // All Services and Components that is provided by this plugin should be exported here export * from './TableViewer/TableViewerStorageService'; +export * from './TableViewer/ValuePanel/DataValuePanelService'; export * from './TableViewer/TableViewer'; export * from './TableViewer/TableFooter/TableFooterMenu/TableFooterMenuService'; diff --git a/webapp/packages/plugin-ddl-viewer/src/DdlViewerBootstrap.ts b/webapp/packages/plugin-ddl-viewer/src/DdlViewerBootstrap.ts index 6b398bd808..df357133db 100644 --- a/webapp/packages/plugin-ddl-viewer/src/DdlViewerBootstrap.ts +++ b/webapp/packages/plugin-ddl-viewer/src/DdlViewerBootstrap.ts @@ -6,22 +6,27 @@ * you may not use this file except in compliance with the License. */ -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ITabBuilder, ObjectFoldersService } from '@cloudbeaver/plugin-object-viewer'; import { DdlViewerTabService } from './DdlViewerTabService'; @injectable() -export class DdlViewerBootstrap { - constructor(private objectFoldersService: ObjectFoldersService, - private ddlViewerTabService: DdlViewerTabService) { +export class DdlViewerBootstrap extends Bootstrap { + constructor( + private objectFoldersService: ObjectFoldersService, + private ddlViewerTabService: DdlViewerTabService + ) { + super(); } - bootstrap() { + register(): void { const tabBuilder: ITabBuilder = { build: nodeId => this.ddlViewerTabService.buildTab(nodeId), }; this.objectFoldersService.registerTabConstructor(tabBuilder); } + + load(): void {} } diff --git a/webapp/packages/plugin-ddl-viewer/src/manifest.ts b/webapp/packages/plugin-ddl-viewer/src/manifest.ts index df6505eae5..8c024f01e5 100644 --- a/webapp/packages/plugin-ddl-viewer/src/manifest.ts +++ b/webapp/packages/plugin-ddl-viewer/src/manifest.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import type { IServiceInjector, PluginManifest } from '@cloudbeaver/core-di'; +import type { PluginManifest } from '@cloudbeaver/core-di'; import { DdlViewerBootstrap } from './DdlViewerBootstrap'; import { DdlViewerService } from './DdlViewerService'; @@ -18,12 +18,8 @@ export const manifest: PluginManifest = { }, providers: [ + DdlViewerBootstrap, DdlViewerTabService, DdlViewerService, ], - - initialize(services: IServiceInjector): void { - const service = services.resolveServiceByClass(DdlViewerBootstrap); - service.bootstrap(); - }, }; diff --git a/webapp/packages/plugin-gis-viewer/.gitignore b/webapp/packages/plugin-gis-viewer/.gitignore new file mode 100644 index 0000000000..15bc16c7c3 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/.gitignore @@ -0,0 +1,17 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/plugin-gis-viewer/package.json b/webapp/packages/plugin-gis-viewer/package.json new file mode 100644 index 0000000000..7e3b684632 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/package.json @@ -0,0 +1,49 @@ +{ + "name": "@cloudbeaver/plugin-gis-viewer", + "sideEffects": false, + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "main": "lib/index.js", + "module": "lib/index.js", + "types": "src/index.ts", + "scripts": { + "prebuild": "rimraf lib/*", + "build": "webpack --config ../../configs/webpack.plugin.config.js", + "postbuild": "tsc", + "lint": "eslint ./src/ --ext .ts,.tsx", + "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix" + }, + "peerDependencies": { + "@cloudbeaver/core-app": "^0.1.0", + "@cloudbeaver/core-blocks": "^0.1.0", + "@cloudbeaver/core-di": "^0.1.0", + "@cloudbeaver/core-dialogs": "^0.1.0", + "@cloudbeaver/core-events": "^0.1.0", + "@cloudbeaver/core-extensions": "^0.1.0", + "@cloudbeaver/core-localization": "^0.1.0", + "@cloudbeaver/core-plugin": "^0.1.0", + "@cloudbeaver/core-product": "^0.1.0", + "@cloudbeaver/core-root": "^0.1.0", + "@cloudbeaver/core-sdk": "^0.1.0", + "@cloudbeaver/core-settings": "^0.1.0", + "@cloudbeaver/core-theming": "^0.1.0", + "@cloudbeaver/core-utils": "^0.1.0", + "@cloudbeaver/core-view": "^0.1.0", + "@cloudbeaver/plugin-data-viewer": "^0.1.0", + "mobx": "^5.x.x", + "mobx-react-lite": "^3.x.x", + "react": "^16.x.x", + "reshadow": "^0.x.x", + "rxjs": "^6.x.x" + }, + "dependencies": { + "leaflet": "^1.7.1", + "react-leaflet": "^3.1.0", + "wellknown": "^0.5.0" + }, + "devDependencies": { + "@types/react-leaflet": "^2.8.1", + "@types/wellknown": "^0.5.1" + } +} diff --git a/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx b/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx new file mode 100644 index 0000000000..396fdc104c --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/GISValuePresentation.tsx @@ -0,0 +1,96 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { useCallback, useMemo } from 'react'; +import wellknown from 'wellknown'; + +import { TextPlaceholder } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { IDatabaseResultSet, ResultSetSelectAction, IResultSetElementKey, IDatabaseDataModel } from '@cloudbeaver/plugin-data-viewer'; + +import { IGeoJSONFeature, IAssociatedValue, LeafletMap } from './LeafletMap'; +import { ResultSetGISAction } from './ResultSetGISAction'; + +interface Props { + model: IDatabaseDataModel; + resultIndex: number; +} + +export const GISValuePresentation: React.FC = observer(function GISValuePresentation({ + model, + resultIndex, +}) { + const translate = useTranslate(); + + const modelResultData = model.getResult(resultIndex); + const selection = model.source.getAction(resultIndex, ResultSetSelectAction); + const gis = model.source.getAction(resultIndex, ResultSetGISAction); + + const focusedCell = selection.getFocusedElement() as Required | null; + const selectedCells = selection.getSelectedElements(); + + if (selectedCells.length === 0 && focusedCell) { + selectedCells.push(focusedCell); + } + + const parsedGISData = useMemo(() => { + const result: IGeoJSONFeature[] = []; + + for (const cell of selectedCells) { + const cellValue = gis.getCellValue(cell); + + if (!cellValue) { + continue; + } + + try { + const parsedCellValue = wellknown.parse(cellValue.mapText || cellValue.text); + if (!parsedCellValue) { + continue; + } + + result.push({ type: 'Feature', geometry: parsedCellValue, properties: { associatedCell: cell, srid: cellValue.srid } }); + } catch (exception) { + console.error(`Failed to parse "${cellValue.mapText || cellValue.text}" value.`); + console.error(exception); + } + } + + return result; + }, [selectedCells, gis]); + + const getAssociatedValues = useCallback((cell: Required): IAssociatedValue[] => { + if (!modelResultData?.data?.columns || !modelResultData?.data?.rows) { + return []; + } + + const { column: columnIndex, row: rowIndex } = cell; + + return modelResultData.data.columns.reduce((result: IAssociatedValue[], column, i) => { + if (i !== columnIndex) { + result.push({ + key: column.name!, + value: model.source + .getEditor(resultIndex) + .getCell(rowIndex, i), + }); + } + + return result; + }, []); + }, [modelResultData, model, resultIndex]); + + if (!parsedGISData.length) { + return {translate('gis_presentation_placeholder')}; + } + + return ( + + ); +}); diff --git a/webapp/packages/plugin-gis-viewer/src/GISViewer.tsx b/webapp/packages/plugin-gis-viewer/src/GISViewer.tsx new file mode 100644 index 0000000000..515112d065 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/GISViewer.tsx @@ -0,0 +1,30 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { TabContainerPanelComponent } from '@cloudbeaver/core-blocks'; +import { ComplexLoader, Loader } from '@cloudbeaver/core-blocks'; +import type { IDataValuePanelProps, IDatabaseResultSet } from '@cloudbeaver/plugin-data-viewer'; + +async function loader() { + const { GISValuePresentation } = await import('./GISValuePresentation'); + return { GISValuePresentation }; +} + +export const GISViewer: TabContainerPanelComponent> = function GISViewer({ model, resultIndex }) { + return ( + } + > + {({ GISValuePresentation }) => ( + + )} + + ); +} +; diff --git a/webapp/packages/plugin-gis-viewer/src/GISViewerBootstrap.ts b/webapp/packages/plugin-gis-viewer/src/GISViewerBootstrap.ts new file mode 100644 index 0000000000..7bead27928 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/GISViewerBootstrap.ts @@ -0,0 +1,57 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import { DataValuePanelService, ResultSetSelectAction } from '@cloudbeaver/plugin-data-viewer'; + +import { GISViewer } from './GISViewer'; +import { ResultSetGISAction } from './ResultSetGISAction'; + +@injectable() +export class GISViewerBootstrap extends Bootstrap { + constructor(private readonly dataValuePanelService: DataValuePanelService) { + super(); + } + + register(): void | Promise { + this.dataValuePanelService.add({ + key: 'gis-presentation', + options: { dataFormat: [ResultDataFormat.Resultset] }, + name: 'gis_presentation_title', + order: 10, + panel: () => GISViewer, + isHidden: (_, context) => { + if (!context || !context.model.source.hasResult(context.resultIndex)) { + return true; + } + + const selection = context.model.source.getAction(context.resultIndex, ResultSetSelectAction); + const gis = context.model.source.getAction(context.resultIndex, ResultSetGISAction); + + const focusedElement = selection.getFocusedElement(); + + if (focusedElement) { + return !gis.isGISFormat(focusedElement); + } + + const selectedCells = selection.getSelectedElements(); + + if (selectedCells.length > 0) { + const firstSelectedCell = selectedCells[0]; + + return !gis.isGISFormat(firstSelectedCell); + } + + return true; + }, + }); + } + + load(): void { } +} diff --git a/webapp/packages/plugin-gis-viewer/src/IDatabaseDataGISAction.ts b/webapp/packages/plugin-gis-viewer/src/IDatabaseDataGISAction.ts new file mode 100644 index 0000000000..f3a471968a --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/IDatabaseDataGISAction.ts @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { IDatabaseDataResult, IDatabaseDataAction, IResultSetElementKey } from '@cloudbeaver/plugin-data-viewer'; + +import type { IGISType } from './ResultSetGISAction'; + +export interface IDatabaseDataGISAction + extends IDatabaseDataAction { + getGISDataFor: (selectedCells: Array>) => Array>; + getCellValue: (cell: IResultSetElementKey) => IGISType | undefined; + isGISFormat: (cell: IResultSetElementKey) => boolean; +} diff --git a/webapp/packages/plugin-gis-viewer/src/LeafletMap.tsx b/webapp/packages/plugin-gis-viewer/src/LeafletMap.tsx new file mode 100644 index 0000000000..f7beaf47d7 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/LeafletMap.tsx @@ -0,0 +1,215 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +// eslint-disable-next-line @typescript-eslint/triple-slash-reference +/// + +import 'leaflet/dist/leaflet.css'; + +import type geojson from 'geojson'; +import leaflet from 'leaflet'; +import { useCallback, useEffect, useState } from 'react'; +import { MapContainer, GeoJSON, LayersControl, TileLayer } from 'react-leaflet'; +import type { TileLayerProps } from 'react-leaflet'; +import styled, { css } from 'reshadow'; + +import { useSplit } from '@cloudbeaver/core-blocks'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import type { IResultSetElementKey } from '@cloudbeaver/plugin-data-viewer'; + +export interface IAssociatedValue { + key: string; + value: any; +} + +interface IFeatureProperties { + srid: number; + associatedCell: Required; +} + +export interface IGeoJSONFeature extends GeoJSON.Feature { + type: 'Feature'; + bbox?: geojson.BBox; +} + +interface IBaseTile extends TileLayerProps { + name: string; + checked?: boolean; +} + +interface Props { + geoJSON: IGeoJSONFeature[]; + getAssociatedValues: (cell: Required) => IAssociatedValue[]; +} + +const baseTiles: Record<'street' | 'topography', IBaseTile> = { + street: { + name: 'gis_presentation_base_tile_street_name', + checked: true, + attribution: '© OpenStreetMap contributors', + url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + maxZoom: 18, + id: 'osm.streets', + }, + topography: { + name: 'gis_presentation_base_tile_topography_name', + attribution: '© OpenStreetMap,' + + ' © SRTM,' + + ' © OpenTopoMap', + url: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', + maxZoom: 17, + }, +}; + +const geojsonMarkerOptions: leaflet.CircleMarkerOptions = { + radius: 4, + weight: 3, +}; + +const popupOption: leaflet.PopupOptions = { + closeButton: true, + minWidth: 260, + maxWidth: 800, + maxHeight: 500, +}; + +function polyStyle() { + const objColor = 'blue'; + + return { + weight: 2, + color: objColor, + }; +} + +function getCRS(feature?: IGeoJSONFeature): leaflet.CRS { + switch (feature?.properties?.srid) { + case 0: + return leaflet.CRS.Simple; + case 3857: + return leaflet.CRS.EPSG3857; + case 4326: + return leaflet.CRS.EPSG4326; + case 3395: + return leaflet.CRS.EPSG3395; + case 900913: + return leaflet.CRS.EPSG900913; + default: + return leaflet.CRS.EPSG3857; + } +} + +const styles = css` + MapContainer { + width: 100%; + height: 100%; + } +`; + +export const LeafletMap: React.FC = function LeafletMap({ geoJSON, getAssociatedValues }) { + const splitContext = useSplit(); + const translate = useTranslate(); + + const [mapRef, setMapRef] = useState(null); + const [geoJSONLayerRef, setGeoJSONLayerRef] = useState(null); + + const crs = getCRS(geoJSON[0]); + + const onEachFeature = useCallback((feature: IGeoJSONFeature, layer: leaflet.Layer) => { + const associatedValues = getAssociatedValues(feature.properties.associatedCell); + if (associatedValues.length > 0) { + let popupContent = ''; + + popupContent += ''; + for (let i = 0; i < associatedValues.length; i++) { + const { key, value } = associatedValues[i]; + + if (value === null || value === undefined) { + continue; + } + + popupContent += ''; + } + popupContent += '
' + key + '' + value + '
'; + layer.bindPopup(popupContent, popupOption); + } + }, [getAssociatedValues]); + + useEffect(() => { + if (geoJSONLayerRef && mapRef) { + geoJSONLayerRef.clearLayers(); + + for (let i = 0; i < geoJSON.length; i++) { + geoJSONLayerRef.addData(geoJSON[i]); + } + + const bounds = geoJSONLayerRef.getBounds(); + + if (Object.keys(bounds).length === 0) { + return; + } + + if (crs === leaflet.CRS.Simple) { + const maxDimension = Math.max(bounds.getNorth() - bounds.getSouth(), bounds.getEast() - bounds.getWest()); + mapRef.setMinZoom(-5); + if (maxDimension > 0) { + mapRef.fitBounds(bounds); + } else { + mapRef.setView(bounds.getCenter(), mapRef.getZoom()); + } + } else { + mapRef.fitBounds(bounds); + } + } + }, [geoJSON, geoJSONLayerRef, crs, mapRef]); + + useEffect(() => { + if (mapRef) { + mapRef.invalidateSize(); + } + }, [splitContext.isResizing, splitContext.mode, mapRef]); + + return styled(styles)( + + leaflet.circleMarker(latlng, geojsonMarkerOptions)} + onEachFeature={onEachFeature} + /> + + + + + + + + + + + ); +} +; diff --git a/webapp/packages/plugin-gis-viewer/src/LocaleService.ts b/webapp/packages/plugin-gis-viewer/src/LocaleService.ts new file mode 100644 index 0000000000..efc6d123bd --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/LocaleService.ts @@ -0,0 +1,32 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +@injectable() +export class LocaleService extends Bootstrap { + constructor(private localizationService: LocalizationService) { + super(); + } + + register(): void | Promise { + this.localizationService.addProvider(this.provider.bind(this)); + } + + load(): void | Promise { } + + private async provider(locale: string) { + switch (locale) { + case 'ru': + return (await import('./locales/ru')).default; + default: + return (await import('./locales/en')).default; + } + } +} diff --git a/webapp/packages/plugin-gis-viewer/src/ResultSetGISAction.ts b/webapp/packages/plugin-gis-viewer/src/ResultSetGISAction.ts new file mode 100644 index 0000000000..45136cb524 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/ResultSetGISAction.ts @@ -0,0 +1,72 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 { ResultDataFormat } from '@cloudbeaver/core-sdk'; +import { databaseDataAction, IResultSetElementKey, IDatabaseResultSet } from '@cloudbeaver/plugin-data-viewer'; + +import type { IDatabaseDataGISAction } from './IDatabaseDataGISAction'; + +export interface IGISType { + $type: string; + srid: number; + text: string; + mapText: string | null; + properties: Record | null; +} +@databaseDataAction() +export class ResultSetGISAction implements IDatabaseDataGISAction { + private readonly GISValueType = 'geometry'; + + static dataFormat = ResultDataFormat.Resultset; + result: IDatabaseResultSet; + + constructor( + result: IDatabaseResultSet) { + this.result = result; + } + + isGISFormat(cell: IResultSetElementKey): boolean { + if (cell.row === undefined || cell.column === undefined) { + return false; + } + + const value = this.result?.data?.rows?.[cell.row][cell.column]; + + if (value !== null && typeof value === 'object' && '$type' in value) { + return value.$type === this.GISValueType; + } + + return false; + } + + getGISDataFor(cells: Array>): Array> { + const result: Array> = []; + if (!this.result.data?.rows) { + return result; + } + + for (const cell of cells) { + if (this.isGISFormat(cell)) { + result.push(cell); + } + } + return result; + } + + getCellValue(cell: IResultSetElementKey): IGISType | undefined { + if (cell.row === undefined || cell.column === undefined || !this.result.data?.rows || !this.isGISFormat(cell)) { + return undefined; + } + + return this.result.data.rows[cell.row][cell.column]; + } + + updateResult(result: IDatabaseResultSet) { + this.result = result; + } +} diff --git a/webapp/packages/plugin-gis-viewer/src/index.ts b/webapp/packages/plugin-gis-viewer/src/index.ts new file mode 100644 index 0000000000..899d1e1836 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/index.ts @@ -0,0 +1,5 @@ +import { manifest } from './manifest'; + +export default manifest; + +export * from './ResultSetGISAction'; diff --git a/webapp/packages/plugin-gis-viewer/src/locales/en.ts b/webapp/packages/plugin-gis-viewer/src/locales/en.ts new file mode 100644 index 0000000000..ee6ba63543 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/locales/en.ts @@ -0,0 +1,6 @@ +export default [ + ['gis_presentation_title', 'Map'], + ['gis_presentation_placeholder', 'No data available'], + ['gis_presentation_base_tile_street_name', 'Street'], + ['gis_presentation_base_tile_topography_name', 'Topography'], +]; diff --git a/webapp/packages/plugin-gis-viewer/src/locales/ru.ts b/webapp/packages/plugin-gis-viewer/src/locales/ru.ts new file mode 100644 index 0000000000..0a57aee1c2 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/locales/ru.ts @@ -0,0 +1,6 @@ +export default [ + ['gis_presentation_title', 'Карта'], + ['gis_presentation_placeholder', 'Нет доступных данных'], + ['gis_presentation_base_tile_street_name', 'Схема'], + ['gis_presentation_base_tile_topography_name', 'Топографическая'], +]; diff --git a/webapp/packages/plugin-gis-viewer/src/manifest.ts b/webapp/packages/plugin-gis-viewer/src/manifest.ts new file mode 100644 index 0000000000..a0a8113b5b --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/manifest.ts @@ -0,0 +1,14 @@ +import type { PluginManifest } from '@cloudbeaver/core-di'; + +import { GISViewerBootstrap } from './GISViewerBootstrap'; +import { LocaleService } from './LocaleService'; + +export const manifest: PluginManifest = { + info: { + name: 'GIS Viewer plugin', + }, + providers: [ + GISViewerBootstrap, + LocaleService, + ], +}; diff --git a/webapp/packages/plugin-gis-viewer/src/react-leaflet.d.ts b/webapp/packages/plugin-gis-viewer/src/react-leaflet.d.ts new file mode 100644 index 0000000000..1c7349fb82 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/src/react-leaflet.d.ts @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2021 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 geojson from 'geojson'; +import * as ReactLeaflet from 'react-leaflet'; + +declare module 'react-leaflet' { + interface GeoJSONProps { + data?: geojson.GeoJsonObject; + } +} diff --git a/webapp/packages/plugin-gis-viewer/tsconfig.json b/webapp/packages/plugin-gis-viewer/tsconfig.json new file mode 100644 index 0000000000..50f9f59516 --- /dev/null +++ b/webapp/packages/plugin-gis-viewer/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "lib" + }, + "include": [ + "src" + ], + "exclude": [ + "**/node_modules", + "**/.*/" + ], +} \ No newline at end of file diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx index db9f208f17..7a267970b9 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperties.tsx @@ -39,9 +39,9 @@ export const ObjectProperties = observer(function ObjectProperties({ return styled(styles)( - + {dbObject.properties.map(v => ( - + ))} diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperty.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperty.tsx index 559ff4f3b4..4d5a16280b 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperty.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectInfoTab/ObjectProperty.tsx @@ -27,6 +27,7 @@ export const ObjectProperty = observer(function ObjectProperty({ ? ( , - data: INodeNavigationData - ): Promise => { + objectViewerTabContext: IAsyncContextLoader = async ( + contexts, + data + ) => { const tabInfo = contexts.getContext(this.navigationTabsService.navigationTabContext); const nodeInfo = await contexts.getContext(this.navNodeManagerService.navigationNavNodeContext); @@ -354,6 +354,9 @@ export class ObjectViewerTabService { data: INodeNavigationData, contexts: IExecutionContextProvider ) { + if (!contexts.hasContext(this.objectViewerTabContext)) { + return; + } try { const { switchPage } = await contexts.getContext(this.objectViewerTabContext); diff --git a/webapp/packages/plugin-object-viewer/src/manifest.ts b/webapp/packages/plugin-object-viewer/src/manifest.ts index 56dbca742a..a2073f1e7b 100644 --- a/webapp/packages/plugin-object-viewer/src/manifest.ts +++ b/webapp/packages/plugin-object-viewer/src/manifest.ts @@ -22,6 +22,7 @@ export const manifest: PluginManifest = { info: { name: 'Object Viewer Plugin' }, providers: [ + ObjectViewerBootstrap, ObjectPropertiesPageService, ObjectFoldersService, ObjectViewerTabService, @@ -31,11 +32,4 @@ export const manifest: PluginManifest = { DBObjectPageService, LocaleService, ], - - async initialize(injector: IServiceInjector) { - injector - .resolveServiceByClass(ObjectViewerBootstrap) - .bootstrap(); - }, - }; diff --git a/webapp/packages/plugin-product/src/ProductInfoDialog.tsx b/webapp/packages/plugin-product/src/ProductInfoDialog.tsx index 53f5c4f422..c533b81b88 100644 --- a/webapp/packages/plugin-product/src/ProductInfoDialog.tsx +++ b/webapp/packages/plugin-product/src/ProductInfoDialog.tsx @@ -15,15 +15,9 @@ import { useService } from '@cloudbeaver/core-di'; import { CommonDialogWrapper, DialogComponentProps } from '@cloudbeaver/core-dialogs'; import { useTranslate } from '@cloudbeaver/core-localization'; import { ServerConfigResource } from '@cloudbeaver/core-root'; -import { composes, useStyles } from '@cloudbeaver/core-theming'; +import { ThemeService, useStyles } from '@cloudbeaver/core-theming'; -const productInfoDialogStyles = composes( - css` - IconOrImage { - composes: theme-background-primary from global; - } - `, - css` +const productInfoDialogStyles = css` CommonDialogWrapper { min-width: 600px; } @@ -38,20 +32,27 @@ const productInfoDialogStyles = composes( display: flex; white-space: pre-wrap; } + FormFieldDescriptionNew { + white-space: pre-wrap; + } IconOrImage { - padding: 10px; + max-width: 154px; + height: 32px; border-radius: 2px; } -`); +`; export const ProductInfoDialog: React.FC> = observer( function ProductInfoDialog(props) { const translate = useTranslate(); const styles = useStyles(BASE_CONTAINERS_STYLES, productInfoDialogStyles); const serverConfigResource = useService(ServerConfigResource); + const themeService = useService(ThemeService); + const version = useAppVersion(); const productInfo = serverConfigResource.data?.productInfo; + const logoIcon = themeService.currentThemeId === 'light' ? '/icons/product-logo_light.svg' : '/icons/product-logo_dark.svg'; return styled(styles)( > = observer( footer={( )} onReject={props.rejectDialog} > - + {!productInfo ? ( {translate('app_product_info_placeholder')} ) : ( <> - + {productInfo.name} diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts index ecc07d7396..18432bfd20 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts @@ -15,7 +15,7 @@ import { isObjectCatalogProvider, isObjectSchemaProvider } from '@cloudbeaver/core-app'; import { isConnectionProvider } from '@cloudbeaver/core-connections'; -import { injectable } from '@cloudbeaver/core-di'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ContextMenuService, IMenuContext } from '@cloudbeaver/core-dialogs'; import { ExtensionUtils } from '@cloudbeaver/core-extensions'; import { ActiveViewService } from '@cloudbeaver/core-view'; @@ -23,16 +23,18 @@ import { ActiveViewService } from '@cloudbeaver/core-view'; import { SqlEditorNavigatorService } from './SqlEditorNavigatorService'; @injectable() -export class SqlEditorBootstrap { +export class SqlEditorBootstrap extends Bootstrap { constructor( private mainMenuService: MainMenuService, private contextMenuService: ContextMenuService, private sqlEditorNavigatorService: SqlEditorNavigatorService, private connectionSchemaManagerService: ConnectionSchemaManagerService, private activeViewService: ActiveViewService - ) { } + ) { + super(); + } - async bootstrap() { + register(): void { this.mainMenuService.registerRootItem( { id: 'sql-editor', @@ -58,6 +60,8 @@ export class SqlEditorBootstrap { }); } + load(): void {} + private isSQLEntryDisabled() { const activeView = this.activeViewService.view; if (activeView) { diff --git a/webapp/packages/plugin-sql-editor/src/manifest.ts b/webapp/packages/plugin-sql-editor/src/manifest.ts index 4a851b7d97..f23d088ece 100644 --- a/webapp/packages/plugin-sql-editor/src/manifest.ts +++ b/webapp/packages/plugin-sql-editor/src/manifest.ts @@ -23,6 +23,7 @@ export const sqlEditorPluginManifest: PluginManifest = { }, providers: [ + SqlEditorBootstrap, SqlDialectInfoService, SqlEditorTabService, SqlResultTabsService, @@ -31,8 +32,4 @@ export const sqlEditorPluginManifest: PluginManifest = { SqlEditorNavigatorService, LocaleService, ], - - initialize(services): void { - services.resolveServiceByClass(SqlEditorBootstrap).bootstrap(); - }, }; diff --git a/webapp/packages/product-default/package.json b/webapp/packages/product-default/package.json index 8752269a3e..bc6f8be396 100644 --- a/webapp/packages/product-default/package.json +++ b/webapp/packages/product-default/package.json @@ -1,7 +1,7 @@ { "name": "@cloudbeaver/product-default", "sideEffects": false, - "version": "20.0.5", + "version": "20.0.6", "description": "", "license": "Apache-2.0", "main": "lib/index.js", diff --git a/webapp/packages/product-default/src/index.ts b/webapp/packages/product-default/src/index.ts index a931237728..56577486d8 100644 --- a/webapp/packages/product-default/src/index.ts +++ b/webapp/packages/product-default/src/index.ts @@ -10,6 +10,7 @@ import dataExport from '@cloudbeaver/plugin-data-export'; import spreadsheetNew from '@cloudbeaver/plugin-data-spreadsheet-new'; import dataViewer from '@cloudbeaver/plugin-data-viewer'; import ddlViewer from '@cloudbeaver/plugin-ddl-viewer'; +import gisViewer from '@cloudbeaver/plugin-gis-viewer'; import objectViewer from '@cloudbeaver/plugin-object-viewer'; import productPlugin from '@cloudbeaver/plugin-product'; import sqlEditor from '@cloudbeaver/plugin-sql-editor'; @@ -25,6 +26,7 @@ const PLUGINS = [ connectionTemplate, dataExport, dataViewer, + gisViewer, ddlViewer, objectViewer, sqlEditor, diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 38cd8301c4..2261a998c1 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -2557,6 +2557,11 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.6.0.tgz#f022195afdfc942e088ee2101285a1d31c7d727f" integrity sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw== +"@react-leaflet/core@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@react-leaflet/core/-/core-1.0.2.tgz#39c6a73f61c666d5dcf673741cea2672fa4bbae1" + integrity sha512-QbleYZTMcgujAEyWGki8Lx6cXQqWkNtQlqf5c7NImlIp8bKW66bFpez/6EVatW7+p9WKBOEOVci/9W7WW70EZg== + "@reshadow/babel@^0.0.1-alpha.76", "@reshadow/babel@^0.0.1-issue-119.0": version "0.0.1-issue-119.0" resolved "https://registry.yarnpkg.com/@reshadow/babel/-/babel-0.0.1-issue-119.0.tgz#ef0e9228e06f821e3d2287f01569fb076153dbb5" @@ -2745,6 +2750,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.45.tgz#e9387572998e5ecdac221950dab3e8c3b16af884" integrity sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g== +"@types/geojson@*": + version "7946.0.7" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad" + integrity sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ== + "@types/glob@^7.1.1": version "7.1.3" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" @@ -2792,6 +2802,13 @@ dependencies: "@types/node" "*" +"@types/leaflet@*": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.7.0.tgz#3700fb5d29a8214fbd496565b08ec28e40cee808" + integrity sha512-ltv5jR+VjKSMtoDkxH61Rsbo0zLU7iqyOXpVPkAX4F+79fg2eymC7t0msWsfNaEZO1FGTIQATCCCQe+ijWoicg== + dependencies: + "@types/geojson" "*" + "@types/md5@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@types/md5/-/md5-2.2.1.tgz#0e2d65d8f6cc91aafbc9be5a4c08fee9250406ce" @@ -2841,6 +2858,23 @@ dependencies: "@types/react" "^16" +"@types/react-leaflet@^2.8.1": + version "2.8.1" + resolved "https://registry.yarnpkg.com/@types/react-leaflet/-/react-leaflet-2.8.1.tgz#acda592cf1b7dfe20ac81071c21c1e5521fe71a4" + integrity sha512-BbFyH9R/MecfXaz1Yt29oGBG8AxIKILgAniO61/kv9/sRe5/qj9yDxf3PW7M+nDl5Wn3V7Mp9inKF0LflV+KkQ== + dependencies: + "@types/leaflet" "*" + "@types/react" "*" + +"@types/react@*": + version "17.0.3" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79" + integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/react@^16", "@types/react@^16.9.49": version "16.14.2" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.14.2.tgz#85dcc0947d0645349923c04ccef6018a1ab7538c" @@ -2849,6 +2883,11 @@ "@types/prop-types" "*" csstype "^3.0.2" +"@types/scheduler@*": + version "0.16.1" + resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" + integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== + "@types/tern@*": version "0.23.3" resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.3.tgz#4b54538f04a88c9ff79de1f6f94f575a7f339460" @@ -2868,6 +2907,11 @@ dependencies: "@types/node" "*" +"@types/wellknown@^0.5.1": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@types/wellknown/-/wellknown-0.5.1.tgz#4fdf9a9a21e78b640e7c60b7b6857165153d6a9d" + integrity sha512-Q654r/2cjmh4XdFcZbRUKrY9Z3KmJJhXGQ4snwjWJCNrDNsNCJRaKQZRd/KPJaNGC0gZUxcmAu7184JpDX2Sjw== + "@typescript-eslint/eslint-plugin@^4.13.0": version "4.13.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.13.0.tgz#5f580ea520fa46442deb82c038460c3dd3524bb6" @@ -4509,6 +4553,15 @@ concat-stream@^2.0.0: readable-stream "^3.0.2" typedarray "^0.0.6" +concat-stream@~1.5.0: + version "1.5.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.2.tgz#708978624d856af41a5a741defdd261da752c266" + integrity sha1-cIl4Yk2FavQaWnQd790mHadSwmY= + dependencies: + inherits "~2.0.1" + readable-stream "~2.0.0" + typedarray "~0.0.5" + config-chain@^1.1.11: version "1.1.12" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" @@ -7448,7 +7501,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -8247,6 +8300,11 @@ latest-version@5.1.0: dependencies: package-json "^6.3.0" +leaflet@^1.7.1: + version "1.7.1" + resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.7.1.tgz#10d684916edfe1bf41d688a3b97127c0322a2a19" + integrity sha512-/xwPEBidtg69Q3HlqPdU3DnrXQOvQU/CCHA1tcDQVzOwm91YMYaILjNp7L4Eaw5Z4sOYdbBz6koWyibppd8Zqw== + lerna@^3.22.1: version "3.22.1" resolved "https://registry.yarnpkg.com/lerna/-/lerna-3.22.1.tgz#82027ac3da9c627fd8bf02ccfeff806a98e65b62" @@ -8921,7 +8979,7 @@ minimist-options@^3.0.1: arrify "^1.0.1" is-plain-obj "^1.1.0" -minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5: +minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@~1.2.0: version "1.2.5" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== @@ -10873,6 +10931,11 @@ pretty-format@^3.8.0: resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385" integrity sha1-v77VbV6ad2ZF9LH/eqGjrE+jw4U= +process-nextick-args@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + integrity sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M= + process-nextick-args@~2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" @@ -11102,6 +11165,13 @@ react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-leaflet@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/react-leaflet/-/react-leaflet-3.1.0.tgz#42deb5e454518016eff8bc85511ae58812f910f5" + integrity sha512-kdZS8NYbYFPmkQr7zSDR2gkKGFeWvkxqoqcmZEckzHL4d5c85dJ2gbbqhaPDpmWWgaRw9O29uA/77qpKmK4mTQ== + dependencies: + "@react-leaflet/core" "^1.0.2" + react-popper@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.4.tgz#d2ad3d2474ac9f1abf93df3099d408e5aa6a2e22" @@ -11264,6 +11334,18 @@ read@1, read@~1.0.1: string_decoder "^1.1.1" util-deprecate "^1.0.1" +readable-stream@~2.0.0: + version "2.0.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" + integrity sha1-j5A0HmilPMySh4jaz80Rs265t44= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "~1.0.0" + process-nextick-args "~1.0.6" + string_decoder "~0.10.x" + util-deprecate "~1.0.1" + readdir-scoped-modules@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz#8d45407b4f870a0dcaebc0e28670d18e74514309" @@ -12503,6 +12585,11 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -13074,7 +13161,7 @@ type-is@~1.6.17, type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typedarray@^0.0.6: +typedarray@^0.0.6, typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= @@ -13249,6 +13336,15 @@ urix@^0.1.0: resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= +url-loader@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2" + integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA== + dependencies: + loader-utils "^2.0.0" + mime-types "^2.1.27" + schema-utils "^3.0.0" + url-parse-lax@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-3.0.0.tgz#16b5cafc07dbe3676c1b1999177823d6503acb0c" @@ -13571,6 +13667,14 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== +wellknown@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/wellknown/-/wellknown-0.5.0.tgz#09ae9871fa826cf0a6ec1537ef00c379d78d7101" + integrity sha1-Ca6YcfqCbPCm7BU37wDDedeNcQE= + dependencies: + concat-stream "~1.5.0" + minimist "~1.2.0" + whatwg-fetch@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f"