diff --git a/README.md b/README.md index 33e30bb77e..edd6fee183 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,16 @@ You can see live demo of CloudBeaver here: https://demo.cloudbeaver.io ## Changelog +### 24.0.5. 2024-05-20 +- The process of application update was improved - you can track the application update process now; +- All popup dialogs became available for screen readers, including JAWS, to improve the experience for users with disabilities; +- Data Editor: + - Large text values (more than 100 Kb) are now automatically opened in the Value panel; +- DuckDB: + - Spatial data visualization support was added; + - The driver has been updated to version 0.10.2; +- Different bug fixes and enhancements have been made. + ### 24.0.4. 2024-05-06 - Added the ability to stop the process of file upload in the table; - Row count calculation in the grid can be cancelled for Data Editor and SQL Editor; diff --git a/deploy/docker/Dockerfile b/deploy/docker/base-java/Dockerfile similarity index 88% rename from deploy/docker/Dockerfile rename to deploy/docker/base-java/Dockerfile index 0a110689fa..3d6522ffb1 100644 --- a/deploy/docker/Dockerfile +++ b/deploy/docker/base-java/Dockerfile @@ -2,9 +2,11 @@ FROM ubuntu:23.10 MAINTAINER DBeaver Corp, devops@dbeaver.com +ENV DEBIAN_FRONTEND=noninteractive + RUN set -eux; \ apt-get update; \ - DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + apt-get install -y --no-install-recommends \ # curl required for historical reasons, see https://github.com/adoptium/containers/issues/255 curl \ wget \ @@ -43,10 +45,3 @@ RUN set -eux; \ ### Patch java security COPY java.security* ${JAVA_HOME}/conf/security/java.security - -COPY cloudbeaver /opt/cloudbeaver - -EXPOSE 8978 -RUN find /opt/cloudbeaver -type d -exec chmod 775 {} \; -WORKDIR /opt/cloudbeaver/ -ENTRYPOINT ["./run-server.sh"] diff --git a/deploy/docker/cloudbeaver-ce/Dockerfile b/deploy/docker/cloudbeaver-ce/Dockerfile new file mode 100644 index 0000000000..361df92c0a --- /dev/null +++ b/deploy/docker/cloudbeaver-ce/Dockerfile @@ -0,0 +1,10 @@ +FROM dbeaver/base-java + +MAINTAINER DBeaver Corp, devops@dbeaver.com + +COPY cloudbeaver /opt/cloudbeaver + +EXPOSE 8978 +RUN find /opt/cloudbeaver -type d -exec chmod 775 {} \; +WORKDIR /opt/cloudbeaver/ +ENTRYPOINT ["./run-server.sh"] diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/cloudbeaver-ce/docker-compose.yml similarity index 100% rename from deploy/docker/docker-compose.yml rename to deploy/docker/cloudbeaver-ce/docker-compose.yml diff --git a/deploy/docker/make-docker-container.sh b/deploy/docker/make-docker-container.sh index 17f869b3e4..f38c98d2a1 100755 --- a/deploy/docker/make-docker-container.sh +++ b/deploy/docker/make-docker-container.sh @@ -1,3 +1,3 @@ cd .. -docker build -t dbeaver/cloudbeaver:dev . --file ./docker/Dockerfile +docker build -t dbeaver/cloudbeaver:dev . --file ./docker/cloudbeaver-ce/Dockerfile diff --git a/deploy/supervisor/cloudbeaver.conf b/deploy/supervisor/cloudbeaver.conf deleted file mode 100644 index 01d38086f8..0000000000 --- a/deploy/supervisor/cloudbeaver.conf +++ /dev/null @@ -1,10 +0,0 @@ -[program:cloudbeaver] -directory=/opt/cloudbeaver/ -command=/opt/cloudbeaver/run-server.sh -stopasgroup=true -killasgroup=true -stopsignal=INT -autostart=true -autorestart=true -redirect_stderr=true -stdout_logfile=/var/log/cloudbeaver-server.log diff --git a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF index fa743ca1e8..b1ef7fd2b3 100644 --- a/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF +++ b/server/bundles/io.cloudbeaver.model/META-INF/MANIFEST.MF @@ -22,6 +22,7 @@ Require-Bundle: org.jkiss.dbeaver.data.gis;visibility:=reexport, Export-Package: io.cloudbeaver, io.cloudbeaver.auth, io.cloudbeaver.auth.provider, + io.cloudbeaver.auth.provider.fa, io.cloudbeaver.auth.provider.local, io.cloudbeaver.auth.provisioning, io.cloudbeaver.websocket, diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/fa/AbstractSessionExternal.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/fa/AbstractSessionExternal.java new file mode 100644 index 0000000000..f0ac26016a --- /dev/null +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/auth/provider/fa/AbstractSessionExternal.java @@ -0,0 +1,79 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudbeaver.auth.provider.fa; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.auth.*; + +import java.time.LocalDateTime; +import java.util.Map; + +public abstract class AbstractSessionExternal implements SMSessionExternal { + + @NotNull + protected final Map authParameters; + @NotNull + protected final SMSession parentSession; + @NotNull + protected final SMAuthSpace space; + + protected AbstractSessionExternal( + @NotNull SMSession parentSession, + @NotNull SMAuthSpace space, + @NotNull Map authParameters + ) { + this.parentSession = parentSession; + this.space = space; + this.authParameters = authParameters; + } + + @NotNull + @Override + public SMAuthSpace getSessionSpace() { + return space; + } + + @NotNull + @Override + public SMSessionContext getSessionContext() { + return this.parentSession.getSessionContext(); + } + + @Nullable + @Override + public SMSessionPrincipal getSessionPrincipal() { + return parentSession.getSessionPrincipal(); + } + + @NotNull + @Override + public LocalDateTime getSessionStart() { + return parentSession.getSessionStart(); + } + + @Override + public void close() { + // do nothing + } + + @Override + public Map getAuthParameters() { + return authParameters; + } +} diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java index 30295d47d7..3ebb0593ab 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/app/BaseWebApplication.java @@ -35,6 +35,7 @@ import org.jkiss.dbeaver.model.auth.SMCredentialsProvider; import org.jkiss.dbeaver.model.auth.SMSessionContext; import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.impl.app.ApplicationRegistry; import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.secret.DBSSecretController; @@ -248,8 +249,13 @@ public synchronized String getApplicationInstanceId() throws DBException { if (instanceId == null) { try { byte[] macAddress = RuntimeUtils.getLocalMacAddress(); - // workspace id from is read from property file - instanceId = BaseWorkspaceImpl.readWorkspaceIdProperty() + "_" + CommonUtils.toHexString(macAddress); + instanceId = String.join( + "_", + ApplicationRegistry.getInstance().getApplication().getId(), + BaseWorkspaceImpl.readWorkspaceIdProperty(), // workspace id is read from property file + CommonUtils.toHexString(macAddress), + CommonUtils.toString(getServerPort()) + ); } catch (Exception e) { throw new DBException("Error during generation instance id generation", e); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserEventHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserEventHandler.java index 3c22b3ae9e..37d6ed7bae 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserEventHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSUserEventHandler.java @@ -45,7 +45,7 @@ public void handleEvent(@NotNull EVENT event) { break; case USER_DELETED: if (event instanceof WSUserDeletedEvent userDeletedEvent) { - sessionManager.closeUserSession(userDeletedEvent.getUserId()); + sessionManager.closeUserSession(userDeletedEvent.getDeletedUserId()); } break; default: diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.ldap.auth/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..983d13ae7d --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/META-INF/MANIFEST.MF @@ -0,0 +1,14 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Vendor: Cloudbeaver LDAP +Bundle-Vendor: DBeaver Corp +Bundle-SymbolicName: io.cloudbeaver.service.ldap.auth;singleton:=true +Bundle-Version: 1.0.0.qualifier +Bundle-Release-Date: 20240506 +Bundle-RequiredExecutionEnvironment: JavaSE-17 +Bundle-ActivationPolicy: lazy +Bundle-ClassPath: . +Require-Bundle: org.jkiss.dbeaver.model;visibility:=reexport, + org.jkiss.dbeaver.registry;visibility:=reexport, + io.cloudbeaver.model +Automatic-Module-Name: io.cloudbeaver.service.ldap.auth diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/build.properties b/server/bundles/io.cloudbeaver.service.ldap.auth/build.properties new file mode 100644 index 0000000000..95692a59c9 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/build.properties @@ -0,0 +1,5 @@ +source.. = src/ +output.. = target/classes/ +bin.includes = .,\ + META-INF/,\ + plugin.xml diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml b/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml new file mode 100644 index 0000000000..4abd568426 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/plugin.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/pom.xml b/server/bundles/io.cloudbeaver.service.ldap.auth/pom.xml new file mode 100644 index 0000000000..09ebea89c1 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + io.cloudbeaver + bundles + 1.0.0-SNAPSHOT + ../ + + io.cloudbeaver.service.ldap.auth + 1.0.0-SNAPSHOT + eclipse-plugin + + diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java new file mode 100644 index 0000000000..4a86e2fb4e --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapAuthProvider.java @@ -0,0 +1,166 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.ldap.auth; + +import io.cloudbeaver.DBWUserIdentity; +import io.cloudbeaver.auth.SMAuthProviderExternal; +import io.cloudbeaver.auth.SMBruteForceProtected; +import io.cloudbeaver.auth.provider.local.LocalAuthProviderConstants; +import io.cloudbeaver.model.session.WebSession; +import io.cloudbeaver.model.user.WebUser; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.DBPObject; +import org.jkiss.dbeaver.model.auth.SMSession; +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; +import org.jkiss.dbeaver.model.security.SMController; +import org.jkiss.utils.CommonUtils; + +import javax.naming.Context; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class LdapAuthProvider implements SMAuthProviderExternal, SMBruteForceProtected { + public LdapAuthProvider() { + } + + @Override + public Map authExternalUser( + @NotNull DBRProgressMonitor monitor, + @Nullable SMAuthProviderCustomConfiguration providerConfig, + @NotNull Map authParameters + ) throws DBException { + if (providerConfig == null) { + throw new DBException("LDAP provider config is null"); + } + String userName = JSONUtils.getString(authParameters, LdapConstants.CRED_USERNAME); + if (CommonUtils.isEmpty(userName)) { + throw new DBException("LDAP user name is empty"); + } + String password = JSONUtils.getString(authParameters, LdapConstants.CRED_PASSWORD); + if (CommonUtils.isEmpty(password)) { + throw new DBException("LDAP password is empty"); + } + String unit = CommonUtils.nullIfEmpty(JSONUtils.getString(authParameters, LdapConstants.CRED_UNITS)); + + LdapSettings ldapSettings = new LdapSettings(providerConfig); + Hashtable environment = new Hashtable<>(); + environment.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + + var ldapProviderUrl = "ldap://" + ldapSettings.getHost() + ":" + ldapSettings.getPort(); + environment.put(Context.PROVIDER_URL, ldapProviderUrl); + environment.put(Context.SECURITY_AUTHENTICATION, "simple"); + + String cn = "cn=" + userName; + var principal = Stream.of(cn, unit, ldapSettings.getBaseDN()) + .filter(CommonUtils::isNotEmpty) + .collect(Collectors.joining(",")); + + environment.put(Context.SECURITY_PRINCIPAL, principal); + environment.put(Context.SECURITY_CREDENTIALS, password); + try { + DirContext context = new InitialDirContext(environment); + context.close(); + Map userData = new HashMap<>(); + userData.put(LdapConstants.CRED_USERNAME, userName); + userData.put(LdapConstants.CRED_SESSION_ID, UUID.randomUUID()); + return userData; + } catch (Exception e) { + throw new DBException("LDAP authentication failed: " + e.getMessage(), e); + } + } + + @NotNull + @Override + public DBWUserIdentity getUserIdentity( + @NotNull DBRProgressMonitor monitor, + @Nullable SMAuthProviderCustomConfiguration customConfiguration, + @NotNull Map authParameters + ) throws DBException { + String userName = JSONUtils.getString(authParameters, LocalAuthProviderConstants.CRED_USER); + if (CommonUtils.isEmpty(userName)) { + throw new DBException("LDAP user name is empty"); + } + return new DBWUserIdentity(userName, userName); + } + + @Nullable + @Override + public DBPObject getUserDetails( + @NotNull DBRProgressMonitor monitor, + @NotNull WebSession webSession, + @NotNull SMSession session, + @NotNull WebUser user, + boolean selfIdentity + ) throws DBException { + return null; + } + + @NotNull + @Override + public String validateLocalAuth( + @NotNull DBRProgressMonitor monitor, + @NotNull SMController securityController, + @NotNull SMAuthProviderCustomConfiguration providerConfig, + @NotNull Map userCredentials, + @Nullable String activeUserId + ) throws DBException { + String userId = JSONUtils.getString(userCredentials, LdapConstants.CRED_USERNAME); + if (CommonUtils.isEmpty(userId)) { + throw new DBException("LDAP user id not found"); + } + return activeUserId == null ? userId : activeUserId; + } + + @Override + public SMSession openSession( + @NotNull DBRProgressMonitor monitor, + @NotNull SMSession mainSession, + @Nullable SMAuthProviderCustomConfiguration customConfiguration, + @NotNull Map userCredentials + ) throws DBException { + return new LdapSession(mainSession, mainSession.getSessionSpace(), userCredentials); + } + + @Override + public void closeSession(@NotNull SMSession mainSession, SMSession session) throws DBException { + + } + + @Override + public void refreshSession( + @NotNull DBRProgressMonitor monitor, + @NotNull SMSession mainSession, + SMSession session + ) throws DBException { + + } + + @Override + public Object getInputUsername(@NotNull Map cred) { + return cred.get(LdapConstants.CRED_USERNAME); + } +} diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java new file mode 100644 index 0000000000..02fcfbea6d --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapConstants.java @@ -0,0 +1,29 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.ldap.auth; + +public interface LdapConstants { + String PARAM_HOST = "ldap-host"; + String PARAM_PORT = "ldap-port"; + String PARAM_DN = "ldap-dn"; + + + String CRED_USERNAME = "user"; + String CRED_PASSWORD = "password"; + String CRED_UNITS = "units"; + String CRED_SESSION_ID = "session-id"; +} diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSession.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSession.java new file mode 100644 index 0000000000..c4326af395 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSession.java @@ -0,0 +1,41 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.ldap.auth; + +import io.cloudbeaver.auth.provider.fa.AbstractSessionExternal; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.auth.SMAuthSpace; +import org.jkiss.dbeaver.model.auth.SMSession; +import org.jkiss.dbeaver.model.data.json.JSONUtils; + +import java.util.Map; + +public class LdapSession extends AbstractSessionExternal { + protected LdapSession( + @NotNull SMSession parentSession, + @NotNull SMAuthSpace space, + @NotNull Map authParameters + ) { + super(parentSession, space, authParameters); + } + + @NotNull + @Override + public String getSessionId() { + return JSONUtils.getString(authParameters, LdapConstants.CRED_SESSION_ID, "sessionNotFound"); + } +} diff --git a/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSettings.java b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSettings.java new file mode 100644 index 0000000000..9bce30ee14 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.ldap.auth/src/io/cloudbeaver/service/ldap/auth/LdapSettings.java @@ -0,0 +1,54 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.ldap.auth; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.security.SMAuthProviderCustomConfiguration; +import org.jkiss.utils.CommonUtils; + +public class LdapSettings { + @NotNull + private final SMAuthProviderCustomConfiguration providerConfiguration; + @NotNull + private final String host; + @NotNull + private final String baseDN; + private final int port; + + protected LdapSettings(SMAuthProviderCustomConfiguration providerConfiguration) { + this.providerConfiguration = providerConfiguration; + this.host = providerConfiguration.getParameter(LdapConstants.PARAM_HOST); + this.port = CommonUtils.isNotEmpty(providerConfiguration.getParameter(LdapConstants.PARAM_PORT)) ? Integer.parseInt( + providerConfiguration.getParameter(LdapConstants.PARAM_PORT)) : 389; + this.baseDN = providerConfiguration.getParameterOrDefault(LdapConstants.PARAM_DN, ""); + } + + + @NotNull + public String getBaseDN() { + return baseDN; + } + + @NotNull + public String getHost() { + return host; + } + + public int getPort() { + return port; + } +} diff --git a/server/bundles/pom.xml b/server/bundles/pom.xml index 5ce7dcb36f..4cd1b54263 100644 --- a/server/bundles/pom.xml +++ b/server/bundles/pom.xml @@ -24,7 +24,9 @@ io.cloudbeaver.service.rm io.cloudbeaver.service.rm.nio io.cloudbeaver.service.data.transfer + io.cloudbeaver.service.security + io.cloudbeaver.service.ldap.auth io.cloudbeaver.resources.drivers.base diff --git a/server/features/io.cloudbeaver.server.feature/feature.xml b/server/features/io.cloudbeaver.server.feature/feature.xml index 887f69b061..3b96765cfa 100644 --- a/server/features/io.cloudbeaver.server.feature/feature.xml +++ b/server/features/io.cloudbeaver.server.feature/feature.xml @@ -39,7 +39,9 @@ + + diff --git a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/SQLQueryTranslatorTest.java b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/SQLQueryTranslatorTest.java index 4420ead0b2..3c06bbce97 100644 --- a/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/SQLQueryTranslatorTest.java +++ b/server/test/io.cloudbeaver.test.platform/src/io/cloudbeaver/test/platform/SQLQueryTranslatorTest.java @@ -27,6 +27,7 @@ import org.jkiss.dbeaver.model.sql.SQLDialect; import org.jkiss.dbeaver.model.sql.translate.SQLQueryTranslator; import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.dbeaver.utils.GeneralUtils; import org.junit.Assert; import org.junit.Test; @@ -191,10 +192,15 @@ private static void translateAndValidateQueries( ); Assert.assertEquals( entry.getKey().getDialectId() + " has invalid syntax " + translated, - entry.getValue().toLowerCase(), - translated.toLowerCase() + normalizeScript(entry.getValue()), + normalizeScript(translated) ); } } + private static String normalizeScript(String script) { + // Unify for tests + return script.toLowerCase().replace(GeneralUtils.getDefaultLineSeparator(), "\n"); + } + } diff --git a/webapp/packages/core-app/src/Body.tsx b/webapp/packages/core-app/src/Body.tsx index 95add4a0f9..f5a7ec2130 100644 --- a/webapp/packages/core-app/src/Body.tsx +++ b/webapp/packages/core-app/src/Body.tsx @@ -54,10 +54,10 @@ export const Body = observer(function Body() { {Screen && } - + - + diff --git a/webapp/packages/core-blocks/src/ErrorBoundary.tsx b/webapp/packages/core-blocks/src/ErrorBoundary.tsx index bcc05ba8ad..cd6382d863 100644 --- a/webapp/packages/core-blocks/src/ErrorBoundary.tsx +++ b/webapp/packages/core-blocks/src/ErrorBoundary.tsx @@ -16,11 +16,13 @@ import { ErrorContext, IExceptionContext } from './ErrorContext'; import { ExceptionMessage } from './ExceptionMessage'; interface Props { + simple?: boolean; icon?: boolean; root?: boolean; inline?: boolean; remount?: boolean; className?: string; + fallback?: React.ReactElement; onClose?: () => any; onRefresh?: () => any; } @@ -71,6 +73,35 @@ export class ErrorBoundary extends React.Component +

Something went wrong.

+ {onClose && ( +
+ +
+ )} + {this.canRefresh && ( +
+ +
+ )} +
+ {errorData.error.toString()} + {stack &&
} + {stack} +
+ {this.props.fallback} + + ); + } + if (root) { return ( diff --git a/webapp/packages/core-blocks/src/Loader/Loader.tsx b/webapp/packages/core-blocks/src/Loader/Loader.tsx index b8a4adca70..0c54ee52dd 100644 --- a/webapp/packages/core-blocks/src/Loader/Loader.tsx +++ b/webapp/packages/core-blocks/src/Loader/Loader.tsx @@ -227,6 +227,7 @@ export const Loader = observer(function Loader({ small={small} inline={inline} fullSize={fullSize} + overlay={overlay} className={className} inlineException={inlineException} /> diff --git a/webapp/packages/core-blocks/src/Menu/Menu.m.css b/webapp/packages/core-blocks/src/Menu/Menu.m.css index 851b929b69..63a2520c16 100644 --- a/webapp/packages/core-blocks/src/Menu/Menu.m.css +++ b/webapp/packages/core-blocks/src/Menu/Menu.m.css @@ -1,10 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ .menuButton { composes: theme-ripple from global; } .menuButton { background: none; border: none; - outline: none !important; color: inherit; cursor: pointer; diff --git a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts index ff219920c8..2ea9352bfe 100644 --- a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts +++ b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts @@ -40,9 +40,10 @@ export interface ResourceKeyWithIncludes { readonly includes: TIncludes; } -type ResourceData, TKey, TIncludes> = TResource extends CachedDataResource - ? CachedResourceData - : CachedMapResourceLoader, CachedResourceData extends Map ? I : never, TIncludes>; +type ResourceData, TKey, TIncludes> = + TResource extends CachedDataResource + ? CachedResourceData + : CachedMapResourceLoader, CachedResourceData extends Map ? I : never, TIncludes>; interface IActions, TKey, TIncludes> { active?: boolean; @@ -91,11 +92,12 @@ interface IDataResourceResult extends IMapResourceState = TResource extends CachedDataResource - ? IDataResourceResult - : TKey extends ResourceKeyList | ResourceKeyListAlias - ? IMapResourceListResult - : IMapResourceResult; +type TResult = + TResource extends CachedDataResource + ? IDataResourceResult + : TKey extends ResourceKeyList | ResourceKeyListAlias + ? IMapResourceListResult + : IMapResourceResult; /** * Accepts resource class or instance and returns resource state. @@ -244,6 +246,7 @@ export function useResource< } this.exception = null; } catch (exception: any) { + console.error(exception); if (this.loadingPromise !== loadingPromise) { return; } diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx index 6ca250ae7a..49b8c15252 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx @@ -10,9 +10,11 @@ import { useContext } from 'react'; import { EventContext } from '@cloudbeaver/core-events'; +import { Clickable } from '../../Clickable'; import { getComputed } from '../../getComputed'; import { Icon } from '../../Icon'; import { Loader } from '../../Loader/Loader'; +import { useTranslate } from '../../localization/useTranslate'; import { s } from '../../s'; import { useS } from '../../useS'; import { useStateDelay } from '../../useStateDelay'; @@ -29,6 +31,7 @@ interface Props { } export const TreeNodeExpand = observer(function TreeNodeExpand({ leaf, big, filterActive, disabled, className }) { + const translate = useTranslate(); const styles = useS(style); const context = useContext(TreeNodeContext); @@ -85,14 +88,22 @@ export const TreeNodeExpand = observer(function TreeNodeExpand({ leaf, bi } } + const title = translate('ui_expand'); + return ( -
{loading && } {expandable && } -
+ ); }); diff --git a/webapp/packages/core-blocks/src/useS.ts b/webapp/packages/core-blocks/src/useS.ts index 7997747e78..8339adacca 100644 --- a/webapp/packages/core-blocks/src/useS.ts +++ b/webapp/packages/core-blocks/src/useS.ts @@ -46,18 +46,20 @@ export function useS(...componentStyles: [...T]): Ex const stylesRef = useRef([]); const [patch, forceUpdate] = useState(0); const loadedStyles = useRef([]); - const themeService = useService(ThemeService); - const [currentThemeId, setCurrentThemeId] = useState(() => themeService.themeId); - const lastThemeRef = useRef(currentThemeId); - // @ts-ignore - const filteredStyles = themeService.mapStyles(componentStyles.flat(Infinity).filter(Boolean) as Style[], context); + const themeService = useService(ThemeService, true); + const [currentThemeId, setCurrentThemeId] = useState(() => themeService?.themeId); + const lastThemeRef = useRef(currentThemeId); + const filteredStyles = themeService + ? // @ts-ignore + themeService.mapStyles(componentStyles.flat(Infinity).filter(Boolean) as Style[], context) + : (componentStyles.flat(Infinity).filter(Boolean) as Style[]); const trackTheme = filteredStyles.some(style => typeof style === 'function'); useExecutor({ - executor: themeService.onChange, + executor: themeService?.onChange, handlers: [ function updateThemeId(theme) { - if (currentThemeId !== themeService.themeId && trackTheme) { + if (currentThemeId !== themeService?.themeId && trackTheme) { setCurrentThemeId(theme.id); } }, @@ -76,22 +78,24 @@ export function useS(...componentStyles: [...T]): Ex lastThemeRef.current = currentThemeId; for (const style of filteredStyles) { - let data: ClassCollection> | Promise; + let data: ClassCollection> | Promise | undefined; if (typeof style === 'object') { data = style; } else { - if (!stylesCache.get(currentThemeId).has(style)) { - data = style(currentThemeId); - stylesCache.get(currentThemeId).set(style, style(currentThemeId)); - } else { - data = stylesCache.get(currentThemeId).get(style)!; + if (currentThemeId !== undefined) { + if (!stylesCache.get(currentThemeId).has(style)) { + data = style(currentThemeId); + stylesCache.get(currentThemeId).set(style, style(currentThemeId)); + } else { + data = stylesCache.get(currentThemeId).get(style)!; + } } } if (data instanceof Promise) { themedStyles.push(data); - } else { + } else if (data !== undefined) { staticStyles.push(data); } } diff --git a/webapp/packages/core-bootstrap/src/bootstrap.ts b/webapp/packages/core-bootstrap/src/bootstrap.ts index 98db1dec2e..b9180d13ab 100644 --- a/webapp/packages/core-bootstrap/src/bootstrap.ts +++ b/webapp/packages/core-bootstrap/src/bootstrap.ts @@ -39,13 +39,11 @@ export async function bootstrap(plugins: PluginManifest[]): Promise { const exception = context.getContext(executionExceptionContext); if (exception.exception) { - console.error(exception.exception); render.renderError(exception.exception); } }); if (exception) { - console.error(exception); render.renderError(exception); } else { render.renderApp(); diff --git a/webapp/packages/core-bootstrap/src/renderLayout.tsx b/webapp/packages/core-bootstrap/src/renderLayout.tsx index 69b0187e2b..91323d28d2 100644 --- a/webapp/packages/core-bootstrap/src/renderLayout.tsx +++ b/webapp/packages/core-bootstrap/src/renderLayout.tsx @@ -47,22 +47,29 @@ export function renderLayout(serviceInjector: IServiceInjector): IRender { }, renderApp() { this.initRoot().render( - - - }> - - - - - , + } simple> + + } root> + }> + + + + + + , ); }, renderError(exception?: any) { + if (exception) { + console.error(exception); + } this.initRoot().render( - - - - , + } simple> + + + + + , ); }, }; diff --git a/webapp/packages/core-browser/src/ServiceWorkerService.ts b/webapp/packages/core-browser/src/ServiceWorkerService.ts index 3d789dff78..a1d0199e0c 100644 --- a/webapp/packages/core-browser/src/ServiceWorkerService.ts +++ b/webapp/packages/core-browser/src/ServiceWorkerService.ts @@ -44,39 +44,47 @@ export class ServiceWorkerService extends Disposable { return; } - if (process.env.NODE_ENV === 'development') { - const registration = await navigator.serviceWorker.getRegistration(this.workerURL); - registration?.unregister(); - } else { - this.workbox = new Workbox(this.workerURL); - this.registration = (await this.workbox.register()) || null; - // should be after registration - this.registerEventListeners(); - - if (this.registration?.active) { - this.isUpdating = true; + try { + if (process.env.NODE_ENV === 'development') { + const registration = await navigator.serviceWorker.getRegistration(this.workerURL); + registration?.unregister(); + } else { + this.workbox = new Workbox(this.workerURL); + this.registration = (await this.workbox.register()) || null; + // should be after registration + this.registerEventListeners(); + + if (this.registration?.active) { + this.isUpdating = true; + } } + } catch (exception: any) { + console.error(exception); } } async load(): Promise { - if (this.registration?.installing || this.registration?.waiting) { - this.onUpdate.execute({ - type: this.isUpdating ? 'updating' : 'installing', - }); - } + try { + if (this.registration?.installing || this.registration?.waiting) { + this.onUpdate.execute({ + type: this.isUpdating ? 'updating' : 'installing', + }); + } - await this.workbox?.update(); + await this.workbox?.update(); - if (this.registration?.active) { - // wait for update only for active service worker - if (this.registration.installing || this.registration.waiting) { - // handled by refresh at 'controlling' event - await new Promise(() => {}); + if (this.registration?.active) { + // wait for update only for active service worker + if (this.registration.installing || this.registration.waiting) { + // handled by refresh at 'controlling' event + await new Promise(() => {}); + } } - } - this.updateIntervalId = setInterval(() => this.workbox?.update(), 1000 * 60 * 60 * 24); + this.updateIntervalId = setInterval(() => this.workbox?.update(), 1000 * 60 * 60 * 24); + } catch (exception: any) { + console.error(exception); + } } dispose(): void { diff --git a/webapp/packages/core-cli/configs/webpack.config.js b/webapp/packages/core-cli/configs/webpack.config.js index 5a70152855..c9d0e997bf 100644 --- a/webapp/packages/core-cli/configs/webpack.config.js +++ b/webapp/packages/core-cli/configs/webpack.config.js @@ -44,6 +44,10 @@ module.exports = (env, argv) => { /\.tsbuildinfo$/, /\.DS_Store$/, /\.svg$/, + /\.png$/, + /\.jpg$/, + /\.gif$/, + /\.jpeg$/, /.woff2?$/, /.eot$/, /.ttf$/, @@ -124,23 +128,32 @@ module.exports = (env, argv) => { }, }; - return { - context: resolve(__dirname, '../../../../../'), - experiments: { - layers: true, - }, - entry: { + let entry = {}; + + if (devMode) { + entry = { + main: { + import: main, + }, + sso: { + import: sso, + }, + }; + } else { + entry = { main: { import: main, - runtime: 'main-runtime', - layer: 'main', }, sso: { import: sso, runtime: 'sso-runtime', - layer: 'sso', }, - }, + }; + } + + return { + context: resolve(__dirname, '../../../../../'), + entry, output: { filename: 'js/[name]-[contenthash].js', chunkFilename: 'js/[name]-[contenthash].js', @@ -179,7 +192,6 @@ module.exports = (env, argv) => { priority: 20, reuseExistingChunk: true, enforce: true, - layer: 'main', }, packages: { chunks: 'initial', @@ -187,7 +199,6 @@ module.exports = (env, argv) => { name: 'packages', priority: 10, reuseExistingChunk: true, - layer: 'main', }, packagesAsync: { chunks: 'async', @@ -202,7 +213,6 @@ module.exports = (env, argv) => { }, priority: 10, reuseExistingChunk: true, - layer: 'main', }, extendedVendorAsync: { chunks: 'async', @@ -210,7 +220,6 @@ module.exports = (env, argv) => { name: 'extended-vendor-async', priority: -5, reuseExistingChunk: true, - layer: 'main', }, extendedVendor: { chunks: 'initial', @@ -218,7 +227,6 @@ module.exports = (env, argv) => { test: new RegExp(`[\\/]node_modules/(${excludedFromVendor.join('|')}).*?`, ''), priority: -5, reuseExistingChunk: true, - layer: 'main', }, vendorAsync: { chunks: 'async', @@ -226,7 +234,6 @@ module.exports = (env, argv) => { name: 'vendor-async', priority: -10, reuseExistingChunk: true, - layer: 'main', }, vendor: { chunks: 'initial', @@ -234,21 +241,20 @@ module.exports = (env, argv) => { test: new RegExp(`[\\/]node_modules/(?!:${excludedFromVendor.join('|')}).*?`, ''), priority: -10, reuseExistingChunk: true, - layer: 'main', }, asyncCommons: { chunks: 'async', name: 'commons-async', filename: 'js/[name]-[contenthash].js', priority: -15, - layer: 'main', + reuseExistingChunk: true, }, commons: { chunks: 'initial', name: 'commons', filename: 'js/[name]-[contenthash].js', priority: -15, - layer: 'main', + reuseExistingChunk: true, }, defaultAsync: { chunks: 'async', @@ -256,7 +262,6 @@ module.exports = (env, argv) => { filename: '[name]-[contenthash].js', priority: -20, reuseExistingChunk: true, - layer: 'main', }, default: { chunks: 'initial', @@ -264,23 +269,6 @@ module.exports = (env, argv) => { filename: '[name]-[contenthash].js', priority: -20, reuseExistingChunk: true, - layer: 'main', - }, - defaultAsyncSso: { - chunks: 'async', - name: 'bundle-async', - filename: '[name]-[contenthash].js', - priority: -20, - reuseExistingChunk: true, - layer: 'sso', - }, - defaultSso: { - chunks: 'initial', - name: 'bundle', - filename: '[name]-[contenthash].js', - priority: -20, - reuseExistingChunk: true, - layer: 'sso', }, }, }, @@ -338,8 +326,8 @@ module.exports = (env, argv) => { }), new IgnoreNotFoundExportPlugin(), new MiniCssExtractPlugin({ - filename: devMode ? 'styles/[name].css' : 'styles/[name]-[contenthash].css', - chunkFilename: devMode ? 'styles/[name].bundle.css' : 'styles/[name]-[contenthash].css', + filename: 'styles/[name]-[contenthash].css', + chunkFilename: 'styles/[name]-[contenthash].css', ignoreOrder: true, // Enable to remove warnings about conflicting order insert: linkTag => { document.head.appendChild(linkTag); diff --git a/webapp/packages/core-cli/configs/webpack.product.dev.config.js b/webapp/packages/core-cli/configs/webpack.product.dev.config.js index 9002d59605..bc376bde22 100644 --- a/webapp/packages/core-cli/configs/webpack.product.dev.config.js +++ b/webapp/packages/core-cli/configs/webpack.product.dev.config.js @@ -50,17 +50,13 @@ module.exports = (env, argv) => { devtoolModuleFilenameTemplate: 'file:///[absolute-resource-path]', }, watchOptions: { - aggregateTimeout: 3000, + aggregateTimeout: 1000, ignored: ['**/node_modules', '**/packages/*/src/**/*.{ts,tsx}'], }, optimization: { minimize: false, - // moduleIds: 'named', - - // improve performance - removeAvailableModules: false, - removeEmptyChunks: false, - // splitChunks: false, + runtimeChunk: 'single', + moduleIds: 'named', }, infrastructureLogging: { level: 'warn', diff --git a/webapp/packages/core-di/src/App.ts b/webapp/packages/core-di/src/App.ts index 7017182538..623e7e12c0 100644 --- a/webapp/packages/core-di/src/App.ts +++ b/webapp/packages/core-di/src/App.ts @@ -5,6 +5,8 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { makeObservable, observable } from 'mobx'; + import { Executor, IExecutor } from '@cloudbeaver/core-executor'; import { Bootstrap } from './Bootstrap'; @@ -21,12 +23,14 @@ export interface IStartData { export class App { readonly onStart: IExecutor; private readonly plugins: PluginManifest[]; + private readonly loadedServices: Map>>; private readonly diWrapper: IDiWrapper = inversifyWrapper; private isAppServiceBound: boolean; constructor(plugins: PluginManifest[] = []) { this.plugins = plugins; this.onStart = new Executor(); + this.loadedServices = new Map(); this.isAppServiceBound = false; this.onStart.addHandler(async ({ preload }) => { @@ -34,6 +38,10 @@ export class App { await this.initializeServices(preload); await this.loadServices(preload); }); + + makeObservable(this, { + loadedServices: observable.shallow, + }); } async start(): Promise { @@ -55,8 +63,13 @@ export class App { return [...this.plugins]; } - getServices(preload?: boolean): Array<() => Promise>> { - return this.plugins.map(plugin => (preload ? plugin.preload || [] : plugin.providers)).flat(); + getServices(plugin?: PluginManifest): Array> { + if (plugin) { + return [...(this.loadedServices.get(plugin) || [])]; + } + return Array.from(this.loadedServices.values()) + .map(set => [...set]) + .flat(); } registerChildContainer(container: DIContainer): void { @@ -81,17 +94,14 @@ export class App { this.getServiceCollection().addServiceByClass(App, this); this.isAppServiceBound = true; } - const services = await Promise.all(this.getServices(preload).map(serviceLoader => serviceLoader())); - for (const service of services) { + for await (const service of this.getServicesAsync(preload)) { this.diWrapper.collection.addServiceByClass(service); } } private async initializeServices(preload?: boolean): Promise { - const services = await Promise.all(this.getServices(preload).map(serviceLoader => serviceLoader())); - - for (const service of services) { + for await (const service of this.getServicesAsync(preload)) { if (service.prototype instanceof Bootstrap) { const serviceInstance = this.diWrapper.injector.getServiceByClass(service); @@ -105,9 +115,7 @@ export class App { } private async loadServices(preload?: boolean): Promise { - const services = await Promise.all(this.getServices(preload).map(serviceLoader => serviceLoader())); - - for (const service of services) { + for await (const service of this.getServicesAsync(preload)) { if (service.prototype instanceof Bootstrap) { const serviceInstance = this.diWrapper.injector.getServiceByClass(service); @@ -117,4 +125,54 @@ export class App { } } } + + private async *getServicesAsync(preload?: boolean, concurrency = 20): AsyncGenerator> { + let i = 0; + let queue: Array>>> = []; + + for (const plugin of this.plugins) { + let servicesLoaders: Array<() => Promise>> = []; + + if (preload) { + servicesLoaders = plugin.preload || []; + } else { + servicesLoaders = plugin.providers; + } + + const loadedServices = this.loadedServices.get(plugin) || (observable(new Set(), { deep: false }) as Set>); + this.loadedServices.set(plugin, loadedServices); + + queue.push( + (async function loader() { + const services = (await Promise.all(servicesLoaders.map(serviceLoader => serviceLoader()))).flat(); + + for (const service of services) { + loadedServices.add(service); + } + + return services; + })(), + ); + i++; + + if (i >= concurrency) { + const services = (await Promise.all(queue)).flat(); + + for (const service of services) { + yield service; + } + + queue = []; + i = 0; + } + } + + if (queue.length > 0) { + const services = (await Promise.all(queue)).flat(); + + for (const service of services) { + yield service; + } + } + } } diff --git a/webapp/packages/core-di/src/DIContainer.ts b/webapp/packages/core-di/src/DIContainer.ts index 6f314e014f..9545443884 100644 --- a/webapp/packages/core-di/src/DIContainer.ts +++ b/webapp/packages/core-di/src/DIContainer.ts @@ -92,6 +92,10 @@ export class DIContainer implements IServiceInjector, IServiceCollection { return this.parent; } + hasServiceByClass(ctor: IServiceConstructor): boolean { + return this.container.isBound(ctor); + } + getServiceByClass(ctor: IServiceConstructor): T { return this.container.get(ctor); } diff --git a/webapp/packages/core-di/src/IApp.ts b/webapp/packages/core-di/src/IApp.ts index 7b035746ab..8546a64715 100644 --- a/webapp/packages/core-di/src/IApp.ts +++ b/webapp/packages/core-di/src/IApp.ts @@ -27,6 +27,7 @@ export interface IInitializableController { export type IServiceConstructor = ITypedConstructor; export interface IServiceInjector { + hasServiceByClass: (ctor: IServiceConstructor) => boolean; getServiceByClass: (ctor: IServiceConstructor) => T; getServiceByToken: (token: ValueToken) => T; resolveServiceByClass: (ctor: IServiceConstructor) => T; diff --git a/webapp/packages/core-di/src/inversifyWrapper.ts b/webapp/packages/core-di/src/inversifyWrapper.ts index 69506c77ec..cb8f34a18e 100644 --- a/webapp/packages/core-di/src/inversifyWrapper.ts +++ b/webapp/packages/core-di/src/inversifyWrapper.ts @@ -19,6 +19,9 @@ const mainContainer = new DIContainer(); export const inversifyWrapper: IDiWrapper = { injector: { + hasServiceByClass(ctor: IServiceConstructor): boolean { + return mainContainer.hasServiceByClass(ctor); + }, getServiceByClass(ctor: IServiceConstructor): T { return mainContainer.getServiceByClass(ctor); }, diff --git a/webapp/packages/core-di/src/useService.ts b/webapp/packages/core-di/src/useService.ts index 81b23ee262..8eb9ea22b3 100644 --- a/webapp/packages/core-di/src/useService.ts +++ b/webapp/packages/core-di/src/useService.ts @@ -11,8 +11,15 @@ import { appContext } from './AppContext'; import type { IServiceConstructor } from './IApp'; import type { ValueToken } from './InjectionToken'; -export function useService(ctor: IServiceConstructor): T { +export function useService(ctor: IServiceConstructor): T; +export function useService(ctor: IServiceConstructor, optional: true): T | undefined; +export function useService(ctor: IServiceConstructor, optional?: boolean): T | undefined { const app = useContext(appContext); + + if (optional && !app.hasServiceByClass(ctor)) { + return undefined; + } + return app.getServiceByClass(ctor); } diff --git a/webapp/packages/core-theming/src/ThemeService.ts b/webapp/packages/core-theming/src/ThemeService.ts index 71dee4ec58..5506c15c6e 100644 --- a/webapp/packages/core-theming/src/ThemeService.ts +++ b/webapp/packages/core-theming/src/ThemeService.ts @@ -8,7 +8,7 @@ import { computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { NotificationService, UIError } from '@cloudbeaver/core-events'; +import { UIError } from '@cloudbeaver/core-events'; import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import type { Style } from './ComponentStyle'; @@ -23,8 +23,6 @@ import { DEFAULT_THEME_ID, themes } from './themes'; import { ThemeSettingsService } from './ThemeSettingsService'; import type { ClassCollection } from './themeUtils'; -const COMMON_STYLES: any[] = []; - export interface ITheme { name: string; id: string; @@ -63,10 +61,7 @@ export class ThemeService extends Bootstrap { private readonly themeMap: Map = new Map(); private reactionDisposer: IReactionDisposer | null; - constructor( - private readonly notificationService: NotificationService, - private readonly themeSettingsService: ThemeSettingsService, - ) { + constructor(private readonly themeSettingsService: ThemeSettingsService) { super(); this.reactionDisposer = null; @@ -132,16 +127,6 @@ export class ThemeService extends Bootstrap { await this.loadTheme(this.themeId); } - getThemeStyles(themeId: string): ClassCollection[] { - const theme = this.themeMap.get(themeId); - - if (!theme) { - this.notificationService.logError({ title: `Theme ${themeId} not found.` }); - return COMMON_STYLES; - } - return [...COMMON_STYLES, theme.styles!]; - } - async changeTheme(themeId: string): Promise { if (themeId === this.themeId) { return; diff --git a/webapp/packages/plugin-devtools/src/PluginBootstrap.ts b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts index ccb2983e38..b208034b1a 100644 --- a/webapp/packages/plugin-devtools/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts @@ -85,7 +85,7 @@ export class PluginBootstrap extends Bootstrap { if (search) { return [ new SearchResourceMenuItem(), - // ...this.getResources(this.app.services.filter(service => service.name.toLocaleLowerCase().includes(search.toLocaleLowerCase()))), + ...this.getResources(this.app.getServices().filter(service => service.name.toLocaleLowerCase().includes(search.toLocaleLowerCase()))), ]; } @@ -128,7 +128,7 @@ export class PluginBootstrap extends Bootstrap { const item = context.tryGet(DATA_CONTEXT_SUBMENU_ITEM); if (item instanceof PluginSubMenuItem) { - return item.plugin.providers.some(provider => provider.prototype instanceof CachedResource); + return this.app.getServices(item.plugin).some(service => service.prototype instanceof CachedResource); } return false; @@ -152,7 +152,7 @@ export class PluginBootstrap extends Bootstrap { return items; } - return [/*...this.getResources(plugin.providers),*/ ...items]; + return [...this.getResources(this.app.getServices(plugin)), ...items]; }, });