diff --git a/.vscode/launch.json b/.vscode/launch.json index caa67e126d..13ce3d5f43 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,13 @@ "sourceMaps": true, "sourceMapPathOverrides": { "webpack:///*": "${workspaceFolder}/../*" - } + }, + "skipFiles": [ + "/**", + "**/node_modules/**", + "${workspaceFolder}/webapp/**/node_modules/**/*.js", + "${workspaceFolder}/webapp/**/dist/**/*.js" + ] }, { "type": "java", diff --git a/README.md b/README.md index b82b3ae99c..7943c2202d 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,12 @@ You can see live demo of CloudBeaver here: https://demo.cloudbeaver.io ## Changelog +### CloudBeaver 23.2.3 - 2023-10-23 + +- The SSL option is available for establishing a connection in SQL Server; +- Added the ability to edit binary values in a table; +- Different bug fixes and enhancements have been made. + ### CloudBeaver 23.2.2 - 2023-10-09 - The 'Save credentials' checkbox has been removed from a template creating form as credentials are not stored in templates; diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java index c59670bf84..f7d5c46e2d 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/BaseWebProjectImpl.java @@ -62,6 +62,12 @@ public RMController getResourceController() { return resourceController; } + @NotNull + @Override + public RMProject getRMProject() { + return project; + } + @Override public boolean isVirtual() { return true; diff --git a/server/bundles/io.cloudbeaver.server/plugin.xml b/server/bundles/io.cloudbeaver.server/plugin.xml index 522dfe5427..7213197f5a 100644 --- a/server/bundles/io.cloudbeaver.server/plugin.xml +++ b/server/bundles/io.cloudbeaver.server/plugin.xml @@ -47,7 +47,6 @@ - @@ -73,6 +72,9 @@ + + + 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 c903d3b7e1..946ba17e73 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 @@ -325,14 +325,6 @@ protected void startServer() { } - { - try { - initializeSecurityController(); - } catch (Exception e) { - log.error("Error initializing database", e); - return; - } - } try { initializeServer(); } catch (DBException e) { @@ -340,6 +332,14 @@ protected void startServer() { return; } + try { + initializeSecurityController(); + } catch (Exception e) { + log.error("Error initializing database", e); + return; + } + + if (configurationMode) { // Try to configure automatically performAutoConfiguration(configPath.toFile().getParentFile()); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java index 40e44c9e3a..4accdf7fe6 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSDeleteTempFileHandler.java @@ -25,6 +25,7 @@ import org.jkiss.utils.IOUtils; import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; public class WSDeleteTempFileHandler implements WSEventHandler { @@ -36,10 +37,12 @@ public void resetTempFolder(String sessionId) { Path path = CBPlatform.getInstance() .getTempFolder(new VoidProgressMonitor(), TEMP_FILE_FOLDER) .resolve(sessionId); - try { - IOUtils.deleteDirectory(path); - } catch (IOException e) { - log.error("Error deleting temp path", e); + if (Files.exists(path)) { + try { + IOUtils.deleteDirectory(path); + } catch (IOException e) { + log.error("Error deleting temp path", e); + } } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSEventHandlerWorkspaceConfigUpdate.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSEventHandlerWorkspaceConfigUpdate.java new file mode 100644 index 0000000000..fa16b732ea --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/events/WSEventHandlerWorkspaceConfigUpdate.java @@ -0,0 +1,35 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2023 DBeaver Corp + * + * All Rights Reserved. + * + * NOTICE: All information contained herein is, and remains + * the property of DBeaver Corp and its suppliers, if any. + * The intellectual and technical concepts contained + * herein are proprietary to DBeaver Corp and its suppliers + * and may be covered by U.S. and Foreign Patents, + * patents in process, and are protected by trade secret or copyright law. + * Dissemination of this information or reproduction of this material + * is strictly forbidden unless prior written permission is obtained + * from DBeaver Corp. + */ +package io.cloudbeaver.server.events; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.WorkspaceConfigEventManager; +import org.jkiss.dbeaver.model.websocket.event.WSEventType; +import org.jkiss.dbeaver.model.websocket.event.WSWorkspaceConfigurationChangedEvent; + +public class WSEventHandlerWorkspaceConfigUpdate extends WSDefaultEventHandler { + private static final Log log = Log.getLog(WSEventHandlerWorkspaceConfigUpdate.class); + + @Override + public void handleEvent(@NotNull WSWorkspaceConfigurationChangedEvent event) { + String configFileName = event.getConfigFilePath(); + WorkspaceConfigEventManager.fireConfigChangedEvent(configFileName); + super.handleEvent(event); + } + +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java index f05434a483..70e26a8c90 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLFileLoaderServlet.java @@ -53,6 +53,8 @@ public class WebSQLFileLoaderServlet extends WebServiceServletBase { private static final String FILE_ID = "fileId"; + private static final String FORBIDDEN_CHARACTERS_FILE_REGEX = "(?U)[$()@ /]+"; + private static final Gson gson = new GsonBuilder() .serializeNulls() .setPrettyPrinting() @@ -88,7 +90,7 @@ protected void processServiceRequest( String fileId = JSONUtils.getString(variables, FILE_ID); - if (fileId != null) { + if (fileId != null && !fileId.matches(FORBIDDEN_CHARACTERS_FILE_REGEX) && !fileId.startsWith(".")) { Path file = tempFolder.resolve(fileId); try { Files.write(file, request.getPart("fileData").getInputStream().readAllBytes()); @@ -96,6 +98,10 @@ protected void processServiceRequest( log.error(e.getMessage()); throw new DBWebException(e.getMessage()); } + } else { + String illegalCharacters = fileId != null ? + fileId.replaceAll(FORBIDDEN_CHARACTERS_FILE_REGEX, " ").strip() : null; + throw new DBException("Resource path '" + fileId + "' contains illegal characters: " + illegalCharacters); } } } \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.fs/plugin.xml b/server/bundles/io.cloudbeaver.service.fs/plugin.xml index 45540f23a6..09ad0fdeca 100644 --- a/server/bundles/io.cloudbeaver.service.fs/plugin.xml +++ b/server/bundles/io.cloudbeaver.service.fs/plugin.xml @@ -8,5 +8,7 @@ class="io.cloudbeaver.service.fs.WebServiceBindingFS"> - + + + diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystem.java b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystem.java index 122362c65f..871179c58c 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystem.java +++ b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystem.java @@ -16,13 +16,13 @@ */ package io.cloudbeaver.service.rm.fs; -import io.cloudbeaver.model.session.WebSession; import io.cloudbeaver.service.rm.nio.RMNIOFileSystemProvider; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.DBPImage; import org.jkiss.dbeaver.model.fs.DBFVirtualFileSystem; import org.jkiss.dbeaver.model.fs.DBFVirtualFileSystemRoot; +import org.jkiss.dbeaver.model.rm.RMController; import org.jkiss.dbeaver.model.rm.RMProject; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; @@ -35,8 +35,9 @@ public class RMVirtualFileSystem implements DBFVirtualFileSystem { @NotNull private final RMProject rmProject; - public RMVirtualFileSystem(@NotNull WebSession webSession, @NotNull RMProject rmProject) { - this.rmNioFileSystemProvider = new RMNIOFileSystemProvider(webSession.getRmController()); + + public RMVirtualFileSystem(@NotNull RMController rmController, @NotNull RMProject rmProject) { + this.rmNioFileSystemProvider = new RMNIOFileSystemProvider(rmController); this.rmProject = rmProject; } diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystemProvider.java b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystemProvider.java index e5355ee310..33617f913a 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystemProvider.java +++ b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/fs/RMVirtualFileSystemProvider.java @@ -16,13 +16,12 @@ */ package io.cloudbeaver.service.rm.fs; -import io.cloudbeaver.WebProjectImpl; -import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.app.DBPProject; import org.jkiss.dbeaver.model.fs.DBFFileSystemProvider; import org.jkiss.dbeaver.model.fs.DBFVirtualFileSystem; +import org.jkiss.dbeaver.model.rm.RMControllerProvider; import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; public class RMVirtualFileSystemProvider implements DBFFileSystemProvider { @@ -33,16 +32,11 @@ public DBFVirtualFileSystem[] getAvailableFileSystems( @NotNull DBRProgressMonitor monitor, @NotNull DBPProject project ) { - var session = project.getSessionContext().getPrimaryAuthSpace(); - if (!(session instanceof WebSession)) { + if (!(project instanceof RMControllerProvider)) { return new DBFVirtualFileSystem[0]; } - WebSession webSession = (WebSession) session; - WebProjectImpl webProject = webSession.getProjectById(project.getId()); - if (webProject == null) { - log.warn(String.format("Project %s not found in session %s", project.getId(), webSession.getSessionId())); - return new DBFVirtualFileSystem[0]; - } - return new DBFVirtualFileSystem[]{new RMVirtualFileSystem(webSession, webProject.getRmProject())}; + RMControllerProvider rmControllerProvider = (RMControllerProvider) project; + return new DBFVirtualFileSystem[]{new RMVirtualFileSystem(rmControllerProvider.getResourceController(), + rmControllerProvider.getRMProject())}; } } diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMNIOFileSystemProvider.java b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMNIOFileSystemProvider.java index 676b28dddc..d1c367a781 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMNIOFileSystemProvider.java +++ b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMNIOFileSystemProvider.java @@ -29,6 +29,8 @@ import java.io.IOException; import java.io.OutputStream; import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileAttribute; @@ -80,6 +82,7 @@ public Path getPath(URI uri) { } RMNIOFileSystem rmNioFileSystem = new RMNIOFileSystem(projectId, this); String resourcePath = uri.getPath(); + resourcePath = URLDecoder.decode(resourcePath, StandardCharsets.UTF_8); if (CommonUtils.isNotEmpty(resourcePath) && projectId == null) { throw new IllegalArgumentException("Project is not specified in URI"); } diff --git a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMPath.java b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMPath.java index c38642269e..8b5d0eb37d 100644 --- a/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMPath.java +++ b/server/bundles/io.cloudbeaver.service.rm.nio/src/io/cloudbeaver/service/rm/nio/RMPath.java @@ -25,9 +25,14 @@ import java.io.IOException; import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.nio.file.LinkOption; import java.nio.file.Path; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Collectors; public class RMPath extends NIOPath { @NotNull @@ -71,7 +76,7 @@ public Path getFileName() { if (ArrayUtils.isEmpty(parts)) { return this; } - return new RMPath(rmNioFileSystem, parts[parts.length - 1]); + return new RMPath(new RMNIOFileSystem(null, getFileSystem().rmProvider()), parts[parts.length - 1]); } @Override @@ -122,22 +127,31 @@ public Path resolve(String other) { @Override public URI toUri() { var fileSystem = getFileSystem(); - var uriBuilder = new StringBuilder(fileSystem.provider().getScheme()) - .append("://"); - - if (rmProjectId != null) { - uriBuilder.append(rmProjectId); + var uriBuilder = new StringBuilder(); + if (isAbsolute()) { + uriBuilder.append(fileSystem.provider().getScheme()) + .append("://"); } - String rmResourcePath = getResourcePath(); - if (rmResourcePath != null) { - uriBuilder.append(fileSystem.getSeparator()) - .append(rmResourcePath); - } + var paths = new ArrayList(); + paths.add(rmProjectId); + paths.add(getResourcePath()); + + uriBuilder.append( + paths.stream() + .filter(Objects::nonNull) + .map(s -> URLEncoder.encode(s, StandardCharsets.UTF_8)) + .collect(Collectors.joining(fileSystem.getSeparator())) + ); return URI.create(uriBuilder.toString()); } + @Override + public boolean isAbsolute() { + return rmNioFileSystem.getRmProjectId() != null; + } + @Override public Path toAbsolutePath() { if (isAbsolute()) { diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java index 6d33551ad0..0f1622d98f 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/CBEmbeddedSecurityController.java @@ -67,7 +67,8 @@ /** * Server controller */ -public class CBEmbeddedSecurityController implements SMAdminController, SMAuthenticationManager { +public class CBEmbeddedSecurityController + implements SMAdminController, SMAuthenticationManager { private static final Log log = Log.getLog(CBEmbeddedSecurityController.class); @@ -80,14 +81,14 @@ public class CBEmbeddedSecurityController implements SMAdminController, SMAuthen }.getType(); private static final Gson gson = new GsonBuilder().create(); - protected final WebAuthApplication application; + protected final T application; protected final CBDatabase database; protected final SMCredentialsProvider credentialsProvider; protected final SMControllerConfiguration smConfig; public CBEmbeddedSecurityController( - WebAuthApplication application, + T application, CBDatabase database, SMCredentialsProvider credentialsProvider, SMControllerConfiguration smConfig @@ -132,44 +133,69 @@ public void createUser( log.debug("Create user: " + userId); try (Connection dbCon = database.openConnection()) { try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { - createAuthSubject(dbCon, userId, SUBJECT_USER); - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("INSERT INTO {table_prefix}CB_USER" + - "(USER_ID,IS_ACTIVE,CREATE_TIME,DEFAULT_AUTH_ROLE) VALUES(?,?,?,?)")) - ) { - dbStat.setString(1, userId); - dbStat.setString(2, enabled ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE); - dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis())); - if (CommonUtils.isEmpty(defaultAuthRole)) { - dbStat.setNull(4, Types.VARCHAR); - } else { - dbStat.setString(4, defaultAuthRole); - } - dbStat.execute(); + createUser(dbCon, userId, metaParameters, enabled, defaultAuthRole); + String defaultTeamName = application.getAppConfiguration().getDefaultUserTeam(); + if (!CommonUtils.isEmpty(defaultTeamName)) { + setUserTeams(dbCon, userId, new String[]{defaultTeamName}, userId); } - saveSubjectMetas(dbCon, userId, metaParameters); txn.commit(); } - String defaultTeamName = application.getAppConfiguration().getDefaultUserTeam(); - if (!CommonUtils.isEmpty(defaultTeamName)) { - setUserTeams(userId, new String[]{defaultTeamName}, userId); - } } catch (SQLException e) { throw new DBCException("Error saving user in database", e); } } + public void createUser( + @NotNull Connection dbCon, + @NotNull String userId, + @Nullable Map metaParameters, + boolean enabled, + @Nullable String defaultAuthRole + ) throws DBException, SQLException { + createAuthSubject(dbCon, userId, SUBJECT_USER); + try (PreparedStatement dbStat = dbCon.prepareStatement( + database.normalizeTableNames("INSERT INTO {table_prefix}CB_USER" + + "(USER_ID,IS_ACTIVE,CREATE_TIME,DEFAULT_AUTH_ROLE) VALUES(?,?,?,?)")) + ) { + dbStat.setString(1, userId); + dbStat.setString(2, enabled ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE); + dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis())); + if (CommonUtils.isEmpty(defaultAuthRole)) { + dbStat.setNull(4, Types.VARCHAR); + } else { + dbStat.setString(4, defaultAuthRole); + } + dbStat.execute(); + } + saveSubjectMetas(dbCon, userId, metaParameters); + + } + @Override public void importUsers(@NotNull SMUserImportList userImportList) throws DBException { for (SMUserProvisioning user : userImportList.getUsers()) { if (isSubjectExists(user.getUserId())) { log.info("Skip already exist user: " + user.getUserId()); + setUserAuthRole(user.getUserId(), userImportList.getAuthRole()); continue; } createUser(user.getUserId(), user.getMetaParameters(), true, userImportList.getAuthRole()); } } + protected void importUsers(@NotNull Connection connection, @NotNull SMUserImportList userImportList) + throws DBException, SQLException { + for (SMUserProvisioning user : userImportList.getUsers()) { + if (isSubjectExists(user.getUserId())) { + log.info("User already exist : " + user.getUserId()); + setUserAuthRole(connection, user.getUserId(), userImportList.getAuthRole()); + enableUser(connection, user.getUserId(), true); + continue; + } + createUser(connection, user.getUserId(), user.getMetaParameters(), true, userImportList.getAuthRole()); + } + } + @Override public void deleteUser(String userId) throws DBCException { invalidateAllUserTokens(userId); @@ -192,25 +218,7 @@ public void deleteUser(String userId) throws DBCException { public void setUserTeams(String userId, String[] teamIds, String grantorId) throws DBCException { try (Connection dbCon = database.openConnection()) { try (JDBCTransaction txn = new JDBCTransaction(dbCon)) { - JDBCUtils.executeStatement( - dbCon, - database.normalizeTableNames("DELETE FROM {table_prefix}CB_USER_TEAM WHERE USER_ID=?"), - userId - ); - if (!ArrayUtils.isEmpty(teamIds)) { - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("INSERT INTO {table_prefix}CB_USER_TEAM" + - "(USER_ID,TEAM_ID,GRANT_TIME,GRANTED_BY) VALUES(?,?,?,?)")) - ) { - for (String teamId : teamIds) { - dbStat.setString(1, userId); - dbStat.setString(2, teamId); - dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis())); - dbStat.setString(4, grantorId); - dbStat.execute(); - } - } - } + setUserTeams(dbCon, userId, teamIds, grantorId); txn.commit(); } } catch (SQLException e) { @@ -219,6 +227,28 @@ public void setUserTeams(String userId, String[] teamIds, String grantorId) thro addSubjectPermissionsUpdateEvent(userId, SMSubjectType.user); } + public void setUserTeams(@NotNull Connection dbCon, String userId, String[] teamIds, String grantorId) + throws SQLException { + JDBCUtils.executeStatement( + dbCon, + database.normalizeTableNames("DELETE FROM {table_prefix}CB_USER_TEAM WHERE USER_ID=?"), + userId + ); + if (!ArrayUtils.isEmpty(teamIds)) { + try (PreparedStatement dbStat = dbCon.prepareStatement( + database.normalizeTableNames("INSERT INTO {table_prefix}CB_USER_TEAM" + + "(USER_ID,TEAM_ID,GRANT_TIME,GRANTED_BY) VALUES(?,?,?,?)")) + ) { + for (String teamId : teamIds) { + dbStat.setString(1, userId); + dbStat.setString(2, teamId); + dbStat.setTimestamp(3, new Timestamp(System.currentTimeMillis())); + dbStat.setString(4, grantorId); + dbStat.execute(); + } + } + } + } @NotNull @@ -563,17 +593,21 @@ public void setCurrentUserParameter(String name, Object value) throws DBExceptio public void enableUser(String userId, boolean enabled) throws DBException { try (Connection dbCon = database.openConnection()) { - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("UPDATE {table_prefix}CB_USER SET IS_ACTIVE=? WHERE USER_ID=?"))) { - dbStat.setString(1, enabled ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE); - dbStat.setString(2, userId); - dbStat.executeUpdate(); - } + enableUser(dbCon, userId, enabled); } catch (SQLException e) { throw new DBCException("Error while updating user configuration", e); } } + public void enableUser(Connection dbCon, String userId, boolean enabled) throws SQLException { + try (PreparedStatement dbStat = dbCon.prepareStatement(database.normalizeTableNames( + "UPDATE {table_prefix}CB_USER SET IS_ACTIVE=? WHERE USER_ID=?"))) { + dbStat.setString(1, enabled ? CHAR_BOOL_TRUE : CHAR_BOOL_FALSE); + dbStat.setString(2, userId); + dbStat.executeUpdate(); + } + } + @Override public void setUserAuthRole(@NotNull String userId, @Nullable String authRole) throws DBException { if (credentialsProvider.getActiveUserCredentials() != null @@ -582,20 +616,28 @@ public void setUserAuthRole(@NotNull String userId, @Nullable String authRole) t throw new SMException("User cannot change his own role"); } try (Connection dbCon = database.openConnection()) { - try (PreparedStatement dbStat = dbCon.prepareStatement( - database.normalizeTableNames("UPDATE {table_prefix}CB_USER SET DEFAULT_AUTH_ROLE=? WHERE USER_ID=?"))) { - dbStat.setString(1, authRole); - dbStat.setString(2, userId); - if (dbStat.executeUpdate() <= 0) { - throw new SMException("User not found"); - } - } + setUserAuthRole(dbCon, userId, authRole); } catch (SQLException e) { throw new DBCException("Error while updating user authentication role", e); } addSubjectPermissionsUpdateEvent(userId, SMSubjectType.user); } + public void setUserAuthRole(@NotNull Connection dbCon, @NotNull String userId, @Nullable String authRole) + throws DBException, SQLException { + try (PreparedStatement dbStat = dbCon.prepareStatement( + database.normalizeTableNames("UPDATE {table_prefix}CB_USER SET DEFAULT_AUTH_ROLE=? WHERE USER_ID=?"))) { + if (authRole == null) { + dbStat.setNull(1, Types.VARCHAR); + } else { + dbStat.setString(1, authRole); + } + dbStat.setString(2, userId); + if (dbStat.executeUpdate() <= 0) { + throw new SMException("User not found"); + } + } + } /////////////////////////////////////////// diff --git a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java index 855cbc8c56..075bf04b46 100644 --- a/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java +++ b/server/bundles/io.cloudbeaver.service.security/src/io/cloudbeaver/service/security/EmbeddedSecurityControllerFactory.java @@ -31,7 +31,7 @@ /** * Embedded Security Controller Factory */ -public class EmbeddedSecurityControllerFactory { +public class EmbeddedSecurityControllerFactory { private static volatile CBDatabase DB_INSTANCE; public static CBDatabase getDbInstance() { @@ -42,7 +42,7 @@ public static CBDatabase getDbInstance() { * Create new security controller instance with custom configuration */ public CBEmbeddedSecurityController createSecurityService( - WebAuthApplication application, + T application, Map databaseConfig, SMCredentialsProvider credentialsProvider, SMControllerConfiguration smConfig @@ -86,7 +86,7 @@ private synchronized void initDatabase(WebAuthApplication application, Map = observer(function ActionIconButton(props) { + const styles = useS(style); + + return ; +}); diff --git a/webapp/packages/core-blocks/src/FormControls/Field.tsx b/webapp/packages/core-blocks/src/FormControls/Field.tsx index f6ba8ca247..de95a34810 100644 --- a/webapp/packages/core-blocks/src/FormControls/Field.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Field.tsx @@ -1,7 +1,7 @@ import { observer } from 'mobx-react-lite'; import type { HTMLAttributes, PropsWithChildren } from 'react'; -import { getLayoutProps } from '../Containers/filterLayoutFakeProps'; +import { filterLayoutFakeProps, getLayoutProps } from '../Containers/filterLayoutFakeProps'; import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps'; import elementsSizeStyles from '../Containers/shared/ElementsSize.m.css'; import { s } from '../s'; @@ -14,8 +14,8 @@ type Props = ILayoutSizeProps & }; export const Field: React.FC> = observer(function Field({ children, className, ...rest }) { const styles = useS(fieldStyles, elementsSizeStyles); - const layoutProps = getLayoutProps(rest); + rest = filterLayoutFakeProps(rest); return (
diff --git a/webapp/packages/core-blocks/src/IconButton.tsx b/webapp/packages/core-blocks/src/IconButton.tsx index 10f8b5f333..1158f6d740 100644 --- a/webapp/packages/core-blocks/src/IconButton.tsx +++ b/webapp/packages/core-blocks/src/IconButton.tsx @@ -5,17 +5,19 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { observer } from 'mobx-react-lite'; +import type React from 'react'; import { Button, ButtonProps } from 'reakit/Button'; import styled from 'reshadow'; import type { ComponentStyle } from '@cloudbeaver/core-theming'; import { Icon } from './Icon'; +import IconButtonStyles from './IconButton.m.css'; import { s } from './s'; import { StaticImage } from './StaticImage'; import { useS } from './useS'; import { useStyles } from './useStyles'; -import IconButtonStyles from './IconButton.m.css'; interface Props { name: string; @@ -24,7 +26,9 @@ interface Props { style?: ComponentStyle; } -export function IconButton({ name, img, viewBox, style, className, ...rest }: Props & ButtonProps) { +export type IconButtonProps = Props & ButtonProps; + +export const IconButton: React.FC = observer(function IconButton({ name, img, viewBox, style, className, ...rest }) { const styles = useS(IconButtonStyles); return styled(useStyles(style))( @@ -33,4 +37,4 @@ export function IconButton({ name, img, viewBox, style, className, ...rest }: Pr {!img && } , ); -} +}); diff --git a/webapp/packages/core-blocks/src/Menu/ACTION_ICON_BUTTON_STYLES.ts b/webapp/packages/core-blocks/src/Menu/ACTION_ICON_BUTTON_STYLES.ts deleted file mode 100644 index 05180fdf80..0000000000 --- a/webapp/packages/core-blocks/src/Menu/ACTION_ICON_BUTTON_STYLES.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2023 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 { css } from 'reshadow'; - -export const ACTION_ICON_BUTTON_STYLES = css` - IconButton { - composes: theme-form-element-radius theme-ripple from global; - - padding: 4px !important; - margin: 2px !important; - width: 24px !important; - height: 24px !important; - overflow: hidden; - flex-shrink: 0; - } -`; diff --git a/webapp/packages/core-blocks/src/Placeholder/Placeholder.tsx b/webapp/packages/core-blocks/src/Placeholder/Placeholder.tsx index c9fe9f93aa..4b5220227e 100644 --- a/webapp/packages/core-blocks/src/Placeholder/Placeholder.tsx +++ b/webapp/packages/core-blocks/src/Placeholder/Placeholder.tsx @@ -7,6 +7,9 @@ */ import { observer } from 'mobx-react-lite'; +import { isDefined } from '@cloudbeaver/core-utils'; + +import { useAutoLoad } from '../Loader/useAutoLoad'; import type { PlaceholderContainer, PlaceholderElement } from './PlaceholderContainer'; type Props> = T & { @@ -27,6 +30,14 @@ export const Placeholder = observer(function Placeholder element.getLoaders?.(rest as unknown as T)) + .flat() + .filter(isDefined), + ); + elements = elements.filter(placeholder => !placeholder.isHidden?.(rest as unknown as T)); return ( diff --git a/webapp/packages/core-blocks/src/Placeholder/PlaceholderContainer.ts b/webapp/packages/core-blocks/src/Placeholder/PlaceholderContainer.ts index 8c3a93ab24..967c1c30aa 100644 --- a/webapp/packages/core-blocks/src/Placeholder/PlaceholderContainer.ts +++ b/webapp/packages/core-blocks/src/Placeholder/PlaceholderContainer.ts @@ -7,7 +7,7 @@ */ import { observable } from 'mobx'; -import { uuid } from '@cloudbeaver/core-utils'; +import { ILoadableState, uuid } from '@cloudbeaver/core-utils'; export type PlaceholderComponent = Record> = React.FunctionComponent; @@ -16,6 +16,7 @@ export interface PlaceholderElement = Record; isHidden?: (props: T) => boolean; order?: number; + getLoaders?: (props: T) => ILoadableState[]; } export class PlaceholderContainer = Record> { @@ -29,12 +30,13 @@ export class PlaceholderContainer = Record !placeholder.isHidden?.(props)); } - add(component: PlaceholderComponent, order?: number, isHidden?: (props: T) => boolean): void { + add(component: PlaceholderComponent, order?: number, isHidden?: (props: T) => boolean, getLoaders?: (props: T) => ILoadableState[]): void { const placeholder: PlaceholderElement = { id: uuid(), component, order, isHidden, + getLoaders, }; if (order === undefined) { diff --git a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts index e98f215baa..b2baf0282c 100644 --- a/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts +++ b/webapp/packages/core-blocks/src/ResourcesHooks/useResource.ts @@ -17,12 +17,13 @@ import { CachedMapResourceListGetter, CachedMapResourceLoader, CachedMapResourceValue, - CachedResource, CachedResourceContext, CachedResourceData, CachedResourceKey, + IResource, isResourceKeyList, isResourceKeyListAlias, + Resource, ResourceKey, ResourceKeyList, ResourceKeyListAlias, @@ -39,16 +40,11 @@ export interface ResourceKeyWithIncludes { readonly includes: TIncludes; } -type ResourceData, TKey, TIncludes> = TResource extends CachedDataResource< - any, - any, - any, - any -> +type ResourceData, TKey, TIncludes> = TResource extends CachedDataResource ? CachedResourceData : CachedMapResourceLoader, CachedResourceData extends Map ? I : never, TIncludes>; -interface IActions, TKey, TIncludes> { +interface IActions, TKey, TIncludes> { active?: boolean; forceSuspense?: boolean; silent?: boolean; @@ -111,7 +107,7 @@ type TResult = TResource extends CachedDataResource< * @param actions */ export function useResource< - TResource extends CachedResource, + TResource extends IResource, TKeyArg extends ResourceKey>, TIncludes extends Readonly>, >( @@ -122,7 +118,7 @@ export function useResource< ): TResult; export function useResource< - TResource extends CachedResource, + TResource extends Resource, TKeyArg extends ResourceKey>, TIncludes extends CachedResourceContext, >( @@ -132,7 +128,7 @@ export function useResource< actions?: TResource extends any ? IActions : never, ): IMapResourceResult | IMapResourceListResult | IDataResourceResult { // eslint-disable-next-line react-hooks/rules-of-hooks - const resource = ctor instanceof CachedResource ? ctor : useService(ctor); + const resource = ctor instanceof Resource ? ctor : useService(ctor); const errorContext = useContext(ErrorContext); let key: ResourceKey | null = keyObj as ResourceKey; let includes: TIncludes = [] as unknown as TIncludes; @@ -233,10 +229,10 @@ export function useResource< const { key, includes, resource } = propertiesRef; if (refresh) { - resource.markOutdated(key); + await resource.refresh(key, includes as any); + } else { + await resource.load(key, includes as any); } - - await resource.load(key, includes as any); }, async load(refresh?: boolean): Promise { if (propertiesRef.key === null) { @@ -332,7 +328,7 @@ export function useResource< if (!this.isLoaded()) { if (this.loading) { - throw this.resource.waitLoad(); + throw refObj.load(); } if (this.canLoad) { diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index 9918ccd680..f7f567b2b9 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -35,8 +35,6 @@ export * from './localization/useTranslate'; export * from './ConnectionImageWithMask/ConnectionImageWithMask'; export { default as ConnectionImageWithMaskSvgStyles } from './ConnectionImageWithMask/ConnectionImageWithMaskSvg.m.css'; -export * from './Menu/ACTION_ICON_BUTTON_STYLES'; -export { default as ActionIconButtonStyles } from './ActionIconButton.m.css'; export * from './Menu/Menu'; export { default as MenuStyles } from './Menu/Menu.m.css'; export * from './Menu/MenuBarSmallItem'; @@ -182,7 +180,9 @@ export * from './ExceptionMessage'; export { default as ExceptionMessageStyles } from './ExceptionMessage.m.css'; export * from './getComputed'; export * from './IconButton'; +export * from './ActionIconButton'; export { default as IconButtonStyles } from './IconButton.m.css'; +export { default as ActionIconButtonStyles } from './ActionIconButton.m.css'; export * from './IconOrImage'; export * from './s'; export * from './SContext'; diff --git a/webapp/packages/core-resource/src/Resource/CachedMapResource.ts b/webapp/packages/core-resource/src/Resource/CachedMapResource.ts index 72f4d31048..befc5a3bac 100644 --- a/webapp/packages/core-resource/src/Resource/CachedMapResource.ts +++ b/webapp/packages/core-resource/src/Resource/CachedMapResource.ts @@ -5,14 +5,15 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, computed, makeObservable } from 'mobx'; +import { action, computed, entries, keys, makeObservable, values } from 'mobx'; import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import { ILoadableState, isArraysEqual, isContainsException } from '@cloudbeaver/core-utils'; -import { CachedResource, CachedResourceKey } from './CachedResource'; +import { CachedResource } from './CachedResource'; import type { CachedResourceIncludeArgs, CachedResourceValueIncludes } from './CachedResourceIncludes'; import type { ICachedResourceMetadata } from './ICachedResourceMetadata'; +import type { CachedResourceKey } from './IResource'; import type { ResourceKey, ResourceKeySimple } from './ResourceKey'; import type { ResourceKeyAlias } from './ResourceKeyAlias'; import { isResourceKeyList, resourceKeyList, ResourceKeyList } from './ResourceKeyList'; @@ -46,15 +47,15 @@ export abstract class CachedMapResource< readonly onItemDelete: ISyncExecutor>; get entries(): [TKey, TValue][] { - return Array.from(this.data.entries()); + return entries(this.data) as [TKey, TValue][]; } get values(): TValue[] { - return Array.from(this.data.values()); + return values(this.data) as TValue[]; } get keys(): TKey[] { - return Array.from(this.data.keys()); + return keys(this.data) as TKey[]; } constructor(defaultValue?: () => Map, defaultIncludes?: CachedResourceIncludeArgs) { diff --git a/webapp/packages/core-resource/src/Resource/CachedResource.ts b/webapp/packages/core-resource/src/Resource/CachedResource.ts index 978b9765e7..531782251e 100644 --- a/webapp/packages/core-resource/src/Resource/CachedResource.ts +++ b/webapp/packages/core-resource/src/Resource/CachedResource.ts @@ -5,9 +5,8 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, makeObservable, observable, toJS } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; -import { Dependency } from '@cloudbeaver/core-di'; import { ExecutionContext, Executor, @@ -19,7 +18,6 @@ import { SyncExecutor, TaskScheduler, } from '@cloudbeaver/core-executor'; -import { isPrimitive, MetadataMap } from '@cloudbeaver/core-utils'; import { CachedResourceOffsetPageKey, @@ -29,30 +27,21 @@ import { isOffsetPageOutdated, } from './CachedResourceOffsetPageKeys'; import type { ICachedResourceMetadata } from './ICachedResourceMetadata'; +import type { IResource } from './IResource'; +import { Resource } from './Resource'; import { isResourceAlias } from './ResourceAlias'; -import { ResourceAliases } from './ResourceAliases'; import { ResourceError } from './ResourceError'; import type { ResourceKey, ResourceKeyFlat } from './ResourceKey'; import { resourceKeyAlias } from './ResourceKeyAlias'; -import { isResourceKeyList, resourceKeyList, ResourceKeyList } from './ResourceKeyList'; +import { resourceKeyList } from './ResourceKeyList'; import { resourceKeyListAlias } from './ResourceKeyListAlias'; -import { ResourceKeyUtils } from './ResourceKeyUtils'; -import { ResourceLogger } from './ResourceLogger'; -import { ResourceMetadata } from './ResourceMetadata'; import { ResourceOffsetPagination } from './ResourceOffsetPagination'; -import { ResourceUseTracker } from './ResourceUseTracker'; export interface IDataError { param: ResourceKey; exception: Error; } -export type CachedResourceData = TResource extends CachedResource ? T : never; -export type CachedResourceValue = TResource extends CachedResource ? T : never; -export type CachedResourceKey = TResource extends CachedResource ? T : never; -export type CachedResourceContext = TResource extends CachedResource ? T : void; -export type CachedResourceMetadata = TResource extends CachedResource ? T : void; - export const CachedResourceParamKey = resourceKeyAlias('@cached-resource/param-default'); export const CachedResourceListEmptyKey = resourceKeyListAlias('@cached-resource/empty'); @@ -65,48 +54,33 @@ export abstract class CachedResource< TKey, TInclude extends ReadonlyArray, TMetadata extends ICachedResourceMetadata = ICachedResourceMetadata, -> extends Dependency { - data: TData; - +> extends Resource { readonly onClear: ISyncExecutor; readonly onDataOutdated: ISyncExecutor>; readonly onDataUpdate: ISyncExecutor>; readonly onDataError: ISyncExecutor>>; readonly beforeLoad: IExecutor>; - readonly useTracker: ResourceUseTracker; readonly offsetPagination: ResourceOffsetPagination; - readonly aliases: ResourceAliases; - protected defaultIncludes: TInclude; protected get loading(): boolean { return this.scheduler.executing; } protected outdateWaitList: ResourceKey[]; protected readonly scheduler: TaskScheduler>; - protected readonly logger: ResourceLogger; - protected readonly metadata: ResourceMetadata; /** Need to infer value type */ private readonly typescriptHack: TValue; - constructor(defaultKey: ResourceKey, private readonly defaultValue: () => TData, defaultIncludes: TInclude = [] as any) { - super(); + constructor(defaultKey: ResourceKey, defaultValue: () => TData, defaultIncludes: TInclude = [] as any) { + super(defaultValue, defaultIncludes); - this.logger = new ResourceLogger(this.getName()); - this.aliases = new ResourceAliases(this.logger, this.validateKey.bind(this)); - this.metadata = new ResourceMetadata(this.aliases, this.getDefaultMetadata.bind(this), this.isKeyEqual.bind(this), this.getKeyRef.bind(this)); this.offsetPagination = new ResourceOffsetPagination(this.metadata); - this.useTracker = new ResourceUseTracker(this.logger, this.aliases, this.metadata); - this.isKeyEqual = this.isKeyEqual.bind(this); - this.isIntersect = this.isIntersect.bind(this); this.loadingTask = this.loadingTask.bind(this); this.typescriptHack = null as any; - this.defaultIncludes = defaultIncludes; this.outdateWaitList = []; this.scheduler = new TaskScheduler(this.isIntersect); - this.data = defaultValue(); this.beforeLoad = new Executor(null, this.isIntersect); this.onClear = new SyncExecutor(); this.onDataOutdated = new SyncExecutor>(null); @@ -124,7 +98,6 @@ export abstract class CachedResource< this.logger.spy(this.onDataError, 'onDataError'); makeObservable(this, { - data: observable, loader: action, markLoading: action, markLoaded: action, @@ -146,15 +119,11 @@ export abstract class CachedResource< }, 5 * 60 * 1000); } - getName(): string { - return this.constructor.name; - } - /** * Mark resource as in use when {@link resource} is in use * @param resource resource to depend on */ - connect(resource: CachedResource): void { + connect(resource: IResource): void { let subscription: string | null = null; const subscriptionHandler = () => { @@ -305,18 +274,7 @@ export abstract class CachedResource< } } - return this.metadata.every(param, metadata => metadata.loaded) && (!includes || this.isIncludes(param, includes)); - } - - /** - * Return true if resource is outdated or not loaded - * @param param - Resource key - */ - isLoadable(param?: ResourceKey, context?: TInclude): boolean { - if (param === undefined) { - param = CachedResourceParamKey; - } - return !this.isLoaded(param, context) || this.isOutdated(param); + return this.metadata.every(param, metadata => metadata.loaded && (!includes || includes.every(include => metadata.includes.includes(include)))); } /** @@ -327,34 +285,6 @@ export abstract class CachedResource< return this.scheduler.wait(); } - isLoading(key?: ResourceKey): boolean { - if (key === undefined) { - key = CachedResourceParamKey; - } - - return this.metadata.some(key, metadata => metadata.loading); - } - - /** - * Return true if specified {@link includes} is loaded for specified {@link key} - * @param key - Resource key - * @param includes - Includes - */ - isIncludes(key: ResourceKey, includes: TInclude): boolean { - return this.metadata.every(key, metadata => includes.every(include => metadata.includes.includes(include))); - } - - getException(param: ResourceKeyFlat): Error | null; - getException(param: ResourceKeyList): Error[] | null; - getException(param: ResourceKey): Error[] | Error | null; - getException(param: ResourceKey): Error[] | Error | null { - if (isResourceKeyList(param)) { - return this.metadata.map(param, metadata => metadata?.exception || null).filter((exception): exception is Error => exception !== null); - } - - return this.metadata.map(param, metadata => metadata?.exception || null); - } - isOutdated(param?: ResourceKey): boolean { if (param === undefined) { param = CachedResourceParamKey; @@ -564,55 +494,6 @@ export abstract class CachedResource< }, {}); } - /** - * Can be overridden to provide equality check for complicated keys - */ - isKeyEqual(param: TKey, second: TKey): boolean { - return param === second; - } - - /** - * Check if key is a part of nextKey - * @param nextKey - Resource key - * @param key - Resource key - * @returns {boolean} Returns true if key can be represented by nextKey - */ - isIntersect(key: ResourceKey, nextKey: ResourceKey): boolean { - if (key === nextKey) { - return true; - } - - if (isResourceAlias(key) && isResourceAlias(nextKey)) { - key = this.aliases.transformToAlias(key); - nextKey = this.aliases.transformToAlias(nextKey); - - return key.isEqual(nextKey) && this.isIntersect(key.target, nextKey.target); - } else if (isResourceAlias(key) || isResourceAlias(nextKey)) { - return true; - } - - if (isResourceKeyList(key) || isResourceKeyList(nextKey)) { - return ResourceKeyUtils.isIntersect(key, nextKey, this.isKeyEqual); - } - - return ResourceKeyUtils.isIntersect(key, nextKey, this.isKeyEqual); - } - - /** - * Can be overridden to provide static link to complicated keys - */ - protected getKeyRef(key: TKey): TKey { - if (isPrimitive(key)) { - return key; - } - return Object.freeze(toJS(key)); - } - - /** - * Check if key is valid. Can be overridden to provide custom validation. - */ - protected abstract validateKey(key: TKey): boolean; - protected resetIncludes(): void { this.metadata.update(metadata => { metadata.includes = observable([...this.defaultIncludes]); @@ -670,21 +551,6 @@ export abstract class CachedResource< this.onDataOutdated.execute(key); } - /** - * Use to extend metadata - * @returns {Record} Object Map - */ - protected getDefaultMetadata(key: TKey, metadata: MetadataMap): TMetadata { - return { - loaded: false, - outdated: true, - loading: false, - exception: null, - includes: observable([...this.defaultIncludes]), - dependencies: observable([]), - } as ICachedResourceMetadata as TMetadata; - } - protected async preLoadData( param: ResourceKey, contexts: IExecutionContextProvider>, diff --git a/webapp/packages/core-resource/src/Resource/IResource.ts b/webapp/packages/core-resource/src/Resource/IResource.ts new file mode 100644 index 0000000000..b2ccf69be1 --- /dev/null +++ b/webapp/packages/core-resource/src/Resource/IResource.ts @@ -0,0 +1,45 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { ICachedResourceMetadata } from './ICachedResourceMetadata'; +import type { ResourceAliases } from './ResourceAliases'; +import type { ResourceKey, ResourceKeyFlat } from './ResourceKey'; +import type { ResourceKeyList } from './ResourceKeyList'; +import type { ResourceUseTracker } from './ResourceUseTracker'; + +export type CachedResourceData = TResource extends IResource ? T : never; +export type CachedResourceKey = TResource extends IResource ? T : never; +export type CachedResourceContext = TResource extends IResource ? T : void; +export type CachedResourceValue = TResource extends IResource ? T : never; +export type CachedResourceMetadata = TResource extends IResource ? T : void; + +export interface IResource< + TData, + TKey, + TInclude extends ReadonlyArray, + TValue = TData, + TMetadata extends ICachedResourceMetadata = ICachedResourceMetadata, +> { + data: TData; + readonly aliases: ResourceAliases; + readonly useTracker: ResourceUseTracker; + getName(): string; + + getException(param: ResourceKeyFlat): Error | null; + getException(param: ResourceKeyList): Error[] | null; + getException(param: ResourceKey): Error[] | Error | null; + getException(param: ResourceKey): Error[] | Error | null; + + isLoadable(param?: ResourceKey, context?: TInclude): boolean; + isLoaded(param?: ResourceKey, includes?: TInclude): boolean; + isLoading(key?: ResourceKey): boolean; + isOutdated(param?: ResourceKey): boolean; + isIntersect(key: ResourceKey, nextKey: ResourceKey): boolean; + + load(key?: ResourceKey, context?: TInclude): Promise; + refresh(key?: ResourceKey, context?: TInclude): Promise; +} diff --git a/webapp/packages/core-resource/src/Resource/Resource.ts b/webapp/packages/core-resource/src/Resource/Resource.ts new file mode 100644 index 0000000000..40357c32a5 --- /dev/null +++ b/webapp/packages/core-resource/src/Resource/Resource.ts @@ -0,0 +1,168 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { makeObservable, observable, toJS } from 'mobx'; + +import { Dependency } from '@cloudbeaver/core-di'; +import { isContainsException, isPrimitive, MetadataMap } from '@cloudbeaver/core-utils'; + +import { CachedResourceParamKey } from './CachedResource'; +import type { ICachedResourceMetadata } from './ICachedResourceMetadata'; +import type { IResource } from './IResource'; +import { isResourceAlias } from './ResourceAlias'; +import { ResourceAliases } from './ResourceAliases'; +import type { ResourceKey, ResourceKeyFlat } from './ResourceKey'; +import { isResourceKeyList, type ResourceKeyList } from './ResourceKeyList'; +import { ResourceKeyUtils } from './ResourceKeyUtils'; +import { ResourceLogger } from './ResourceLogger'; +import { ResourceMetadata } from './ResourceMetadata'; +import { ResourceUseTracker } from './ResourceUseTracker'; + +export abstract class Resource< + TData, + TKey, + TInclude extends ReadonlyArray, + TValue = TData, + TMetadata extends ICachedResourceMetadata = ICachedResourceMetadata, + > + extends Dependency + implements IResource +{ + data: TData; + + readonly aliases: ResourceAliases; + readonly useTracker: ResourceUseTracker; + + protected readonly logger: ResourceLogger; + protected readonly metadata: ResourceMetadata; + + constructor(protected readonly defaultValue: () => TData, protected defaultIncludes: TInclude = [] as any) { + super(); + this.isKeyEqual = this.isKeyEqual.bind(this); + this.isIntersect = this.isIntersect.bind(this); + + this.logger = new ResourceLogger(this.getName()); + this.aliases = new ResourceAliases(this.logger, this.validateKey.bind(this)); + this.metadata = new ResourceMetadata(this.aliases, this.getDefaultMetadata.bind(this), this.isKeyEqual, this.getKeyRef.bind(this)); + this.useTracker = new ResourceUseTracker(this.logger, this.aliases, this.metadata); + + this.data = this.defaultValue(); + + makeObservable(this, { + data: observable, + }); + } + + abstract isLoaded(param?: ResourceKey | undefined, includes?: TInclude | undefined): boolean; + abstract isOutdated(param?: ResourceKey | undefined): boolean; + + isLoadable(param?: ResourceKey | undefined, context?: TInclude | undefined): boolean { + if (param === undefined) { + param = CachedResourceParamKey; + } + + if (isContainsException(this.getException(param))) { + return false; + } + + return !this.isLoaded(param, context) || this.isOutdated(param); + } + + isLoading(key?: ResourceKey): boolean { + if (key === undefined) { + key = CachedResourceParamKey; + } + + return this.metadata.some(key, metadata => metadata.loading); + } + + /** + * Check if key is a part of nextKey + * @param nextKey - Resource key + * @param key - Resource key + * @returns {boolean} Returns true if key can be represented by nextKey + */ + isIntersect(key: ResourceKey, nextKey: ResourceKey): boolean { + if (key === nextKey) { + return true; + } + + if (isResourceAlias(key) && isResourceAlias(nextKey)) { + key = this.aliases.transformToAlias(key); + nextKey = this.aliases.transformToAlias(nextKey); + + return key.isEqual(nextKey) && this.isIntersect(key.target, nextKey.target); + } else if (isResourceAlias(key) || isResourceAlias(nextKey)) { + return true; + } + + if (isResourceKeyList(key) || isResourceKeyList(nextKey)) { + return ResourceKeyUtils.isIntersect(key, nextKey, this.isKeyEqual); + } + + return ResourceKeyUtils.isIntersect(key, nextKey, this.isKeyEqual); + } + + /** + * Can be overridden to provide equality check for complicated keys + */ + isKeyEqual(param: TKey, second: TKey): boolean { + return param === second; + } + + getName(): string { + return this.constructor.name; + } + + getException(param: ResourceKeyFlat): Error | null; + getException(param: ResourceKeyList): Error[] | null; + getException(param: ResourceKey): Error[] | Error | null; + getException(param: ResourceKey): Error[] | Error | null { + if (param === undefined) { + param = CachedResourceParamKey; + } + + if (isResourceKeyList(param)) { + return this.metadata.map(param, metadata => metadata?.exception || null).filter((exception): exception is Error => exception !== null); + } + + return this.metadata.map(param, metadata => metadata?.exception || null); + } + + abstract load(key?: ResourceKey | undefined, context?: TInclude | undefined): Promise; + abstract refresh(key?: ResourceKey | undefined, context?: TInclude | undefined): Promise; + + /** + * Can be overridden to provide static link to complicated keys + */ + protected getKeyRef(key: TKey): TKey { + if (isPrimitive(key)) { + return key; + } + return Object.freeze(toJS(key)); + } + + /** + * Check if key is valid. Can be overridden to provide custom validation. + */ + protected abstract validateKey(key: TKey): boolean; + + /** + * Use to extend metadata + * @returns {Record} Object Map + */ + protected getDefaultMetadata(key: TKey, metadata: MetadataMap): TMetadata { + return { + loaded: false, + outdated: true, + loading: false, + exception: null, + includes: observable([...this.defaultIncludes]), + dependencies: observable([]), + } as ICachedResourceMetadata as TMetadata; + } +} diff --git a/webapp/packages/core-resource/src/Resource/ResourceError.ts b/webapp/packages/core-resource/src/Resource/ResourceError.ts index 5f6894a5be..5dd282765d 100644 --- a/webapp/packages/core-resource/src/Resource/ResourceError.ts +++ b/webapp/packages/core-resource/src/Resource/ResourceError.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 { action } from 'mobx'; + import { LoadingError } from '@cloudbeaver/core-utils'; import type { CachedResource } from './CachedResource'; @@ -18,12 +20,12 @@ export class ResourceError extends LoadingError { options?: ErrorOptions, ) { super( - () => { + action(() => { // @TODO extract clean error logic to the CachedResource. // For now when the ResourceError is thrown and refresh fn is called, the error is not cleaned in the resource this.resource.cleanError(this.key); this.resource.markOutdated(this.key); - }, + }), message, options, ); diff --git a/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts b/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts index cb3bc2225f..250a93db2b 100644 --- a/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts +++ b/webapp/packages/core-resource/src/Resource/ResourceMetadata.ts @@ -50,16 +50,31 @@ export class ResourceMetadata { return this.metadata.has(this.getMetadataKeyRef(key)); } - every(param: ResourceKey, predicate: (metadata: TMetadata) => boolean): boolean { + every(predicate: (metadata: TMetadata) => boolean): boolean; + every(param: ResourceKey, predicate: (metadata: TMetadata) => boolean): boolean; + every(param: ResourceKey | ((metadata: TMetadata) => boolean), predicate?: (metadata: TMetadata) => boolean): boolean { + if (!predicate) { + predicate = param as (metadata: TMetadata) => boolean; + for (const metadata of this.values()) { + if (!predicate(metadata)) { + return false; + } + } + return true; + } + + param = param as ResourceKey; + predicate = predicate as MetadataCallback; + if (!this.has(param)) { return false; } - return !this.some(param, key => !predicate(key)); + return !this.some(param, key => !predicate!(key)); } + some(predicate: MetadataCallback): boolean; some(param: ResourceKey, predicate: MetadataCallback): boolean; - some(param: ResourceKey | MetadataCallback, predicate?: MetadataCallback): boolean { if (!predicate) { predicate = param as (metadata: TMetadata) => boolean; diff --git a/webapp/packages/core-resource/src/index.ts b/webapp/packages/core-resource/src/index.ts index cff8c3ac8e..9e3b099a41 100644 --- a/webapp/packages/core-resource/src/index.ts +++ b/webapp/packages/core-resource/src/index.ts @@ -12,6 +12,8 @@ export { } from './Resource/CachedResourceOffsetPageKeys'; export * from './Resource/CachedTreeResource/CachedTreeResource'; export * from './Resource/ICachedResourceMetadata'; +export * from './Resource/IResource'; +export * from './Resource/Resource'; export * from './Resource/ResourceAlias'; export * from './Resource/ResourceError'; export * from './Resource/ResourceKey'; diff --git a/webapp/packages/core-root/src/ServerEventEmitter/TopicEventHandler.ts b/webapp/packages/core-root/src/ServerEventEmitter/TopicEventHandler.ts index e5d72aca6e..5052c14914 100644 --- a/webapp/packages/core-root/src/ServerEventEmitter/TopicEventHandler.ts +++ b/webapp/packages/core-root/src/ServerEventEmitter/TopicEventHandler.ts @@ -8,7 +8,7 @@ import { Connectable, connectable, filter, map, merge, Observable, Subject } from 'rxjs'; import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; -import type { CachedResource } from '@cloudbeaver/core-resource'; +import type { IResource } from '@cloudbeaver/core-resource'; import { compose } from '@cloudbeaver/core-utils'; import type { IBaseServerEvent, IServerEventCallback, IServerEventEmitter, Subscription } from './IServerEventEmitter'; @@ -29,8 +29,8 @@ export abstract class TopicEventHandler< readonly eventsSubject: Connectable; private subscription: Subscription | null; - private readonly activeResources: Array>; - private readonly subscribedResources: Map, ISubscribedResourceInfo>; + private readonly activeResources: Array>; + private readonly subscribedResources: Map, ISubscribedResourceInfo>; private readonly serverSubject?: Observable; private readonly subject: Subject; constructor(private readonly topic: string, private readonly emitter: IServerEventEmitter) { @@ -56,7 +56,7 @@ export abstract class TopicEventHandler< id: TEventID, callback: IServerEventCallback, mapTo: (event: TEvent) => T = event => event as unknown as T, - resource?: CachedResource, + resource?: IResource, ): Subscription { if (resource) { this.registerResource(resource); @@ -82,7 +82,7 @@ export abstract class TopicEventHandler< callback: IServerEventCallback, mapTo: (param: TEvent) => T = event => event as unknown as T, filterFn: (param: TEvent) => boolean = () => true, - resource?: CachedResource, + resource?: IResource, ): Subscription { if (resource) { this.registerResource(resource); @@ -104,7 +104,7 @@ export abstract class TopicEventHandler< return this; } - private resourceUseHandler(resource: CachedResource) { + private resourceUseHandler(resource: IResource) { const index = this.activeResources.indexOf(resource); if (index !== -1) { @@ -124,7 +124,7 @@ export abstract class TopicEventHandler< } } - private removeActiveResource(resource: CachedResource) { + private removeActiveResource(resource: IResource) { this.activeResources.splice(this.activeResources.indexOf(resource), 1); if (this.activeResources.length === 0) { @@ -134,7 +134,7 @@ export abstract class TopicEventHandler< } } - private registerResource(resource: CachedResource): void { + private registerResource(resource: IResource): void { let info = this.subscribedResources.get(resource); if (!info) { @@ -150,7 +150,7 @@ export abstract class TopicEventHandler< info.listeners++; } - private removeResource(resource: CachedResource): void { + private removeResource(resource: IResource): void { const info = this.subscribedResources.get(resource); if (info) { diff --git a/webapp/packages/core-utils/src/index.ts b/webapp/packages/core-utils/src/index.ts index ca2a5a0ccc..9ccaa2f809 100644 --- a/webapp/packages/core-utils/src/index.ts +++ b/webapp/packages/core-utils/src/index.ts @@ -68,3 +68,4 @@ export * from './createLastPromiseGetter'; export * from './removeMetadataFromBase64'; export * from './renamePathName'; export * from './removeLineBreak'; +export * from './replaceSubstring'; diff --git a/webapp/packages/core-utils/src/replaceSubstring.test.ts b/webapp/packages/core-utils/src/replaceSubstring.test.ts new file mode 100644 index 0000000000..4023e86fbf --- /dev/null +++ b/webapp/packages/core-utils/src/replaceSubstring.test.ts @@ -0,0 +1,28 @@ +import { replaceSubstring } from './replaceSubstring'; + +describe('replaceSubstring', () => { + it('should replace a substring correctly', () => { + const result = replaceSubstring('Hello, world!', 7, 12, 'there'); + expect(result).toBe('Hello, there!'); + }); + + it('should handle beginIndex at the start', () => { + const result = replaceSubstring('Hello, world!', 0, 5, 'Hi'); + expect(result).toBe('Hi, world!'); + }); + + it('should handle endIndex at the end', () => { + const result = replaceSubstring('Hello, world!', 7, 13, 'everyone'); + expect(result).toBe('Hello, everyone'); + }); + + it('should handle empty replacement', () => { + const result = replaceSubstring('Hello, world!', 7, 13, ''); + expect(result).toBe('Hello, '); + }); + + it('should handle replacement longer than the substring', () => { + const result = replaceSubstring('Hello, world!', 7, 12, 'everyone out there'); + expect(result).toBe('Hello, everyone out there!'); + }); +}); diff --git a/webapp/packages/core-utils/src/replaceSubstring.ts b/webapp/packages/core-utils/src/replaceSubstring.ts new file mode 100644 index 0000000000..b3c4075655 --- /dev/null +++ b/webapp/packages/core-utils/src/replaceSubstring.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 replaceSubstring(str: string, beginIndex: number, endIndex: number, replacement: string) { + const beforeSubstring = str.slice(0, beginIndex); + const afterSubstring = str.slice(endIndex); + return beforeSubstring + replacement + afterSubstring; +} diff --git a/webapp/packages/core-view/src/Menu/MenuService.ts b/webapp/packages/core-view/src/Menu/MenuService.ts index 3c877ac2e4..25671cdb6b 100644 --- a/webapp/packages/core-view/src/Menu/MenuService.ts +++ b/webapp/packages/core-view/src/Menu/MenuService.ts @@ -123,17 +123,17 @@ function filterApplicable(contexts: IDataContextProvider): (creator: IMenuItemsC const local = contexts.get(DATA_CONTEXT_MENU_LOCAL); return (creator: IMenuItemsCreator) => { - if (local) { - if (!creator.menus && !creator.contexts) { + if (creator.menus) { + const applicable = creator.menus.some(menu => contexts.hasValue(DATA_CONTEXT_MENU, menu, false)); + + if (!applicable) { return false; } + } - if (creator.menus) { - const applicable = creator.menus.some(menu => contexts.hasValue(DATA_CONTEXT_MENU, menu, false)); - - if (!applicable) { - return false; - } + if (local) { + if (!creator.menus && !creator.contexts) { + return false; } if (creator.contexts) { @@ -151,14 +151,6 @@ function filterApplicable(contexts: IDataContextProvider): (creator: IMenuItemsC return false; } - if (creator.menus) { - const applicable = creator.menus.some(menu => contexts.hasValue(DATA_CONTEXT_MENU, menu, false)); - - if (!applicable) { - return false; - } - } - return true; }; } diff --git a/webapp/packages/plugin-administration/src/Administration/Administration.tsx b/webapp/packages/plugin-administration/src/Administration/Administration.tsx index 5d73b38c6e..bb8c6276db 100644 --- a/webapp/packages/plugin-administration/src/Administration/Administration.tsx +++ b/webapp/packages/plugin-administration/src/Administration/Administration.tsx @@ -147,11 +147,9 @@ export const Administration = observer>(function - - - - - + + + optionsPanelService.close()} /> diff --git a/webapp/packages/plugin-administration/src/Administration/ItemContent.tsx b/webapp/packages/plugin-administration/src/Administration/ItemContent.tsx index 6977c1f519..06ce0f9b84 100644 --- a/webapp/packages/plugin-administration/src/Administration/ItemContent.tsx +++ b/webapp/packages/plugin-administration/src/Administration/ItemContent.tsx @@ -39,11 +39,19 @@ export const ItemContent = observer(function ItemContent({ activeScreen, if (sub) { const Component = sub.getComponent ? sub.getComponent() : item.getContentComponent(); - return ; + return ( + + + + ); } } const Component = item.getContentComponent(); - return ; + return ( + + + + ); }); diff --git a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.tsx b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.tsx index d4e3f7ba97..d4908a73e9 100644 --- a/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.tsx +++ b/webapp/packages/plugin-administration/src/ConfigurationWizard/ServerConfiguration/ServerConfigurationDriversForm.tsx @@ -13,6 +13,7 @@ import { Combobox, Group, GroupTitle, ITag, s, Tag, Tags, useResource, useS, use import { DBDriverResource } from '@cloudbeaver/core-connections'; import { CachedMapAllKey, resourceKeyList } from '@cloudbeaver/core-resource'; import type { ServerConfigInput } from '@cloudbeaver/core-sdk'; +import { isDefined } from '@cloudbeaver/core-utils'; import style from './ServerConfigurationDriversForm.m.css'; @@ -25,7 +26,7 @@ export const ServerConfigurationDriversForm = observer(function ServerCon const translate = useTranslate(); const driversResource = useResource(ServerConfigurationDriversForm, DBDriverResource, CachedMapAllKey); - const drivers = driversResource.resource.values.slice().sort(driversResource.resource.compare); + const drivers = driversResource.data.filter(isDefined).sort(driversResource.resource.compare); const tags: ITag[] = driversResource.resource .get(resourceKeyList(serverConfig.disabledDrivers || [])) diff --git a/webapp/packages/plugin-administration/src/locales/en.ts b/webapp/packages/plugin-administration/src/locales/en.ts index efe0d11eb7..e604f04af4 100644 --- a/webapp/packages/plugin-administration/src/locales/en.ts +++ b/webapp/packages/plugin-administration/src/locales/en.ts @@ -3,8 +3,8 @@ export default [ ['administration_server_configuration_save_confirmation_message', 'You are about to change critical settings. Are you sure?'], ['administration_configuration_wizard_welcome', 'Welcome'], - ['administration_configuration_wizard_welcome_step_description', 'Welcome to CloudBeaver'], - ['administration_configuration_wizard_welcome_title', 'Welcome to CloudBeaver, cloud database management system!'], + ['administration_configuration_wizard_welcome_step_description', 'Welcome to {alias:product_full_name}'], + ['administration_configuration_wizard_welcome_title', 'Welcome to {alias:product_full_name}, cloud database management system!'], [ 'administration_configuration_wizard_welcome_message', 'The easy configuration wizard will guide you through several simple steps to set up the CloudBeaver server. You will need to set server information and administrator credentials. You can set up additional server parameters once the easy configuration is completed.', diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfo.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfo.tsx index 867e158c80..92e7e78aeb 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfo.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UserForm/Info/UserFormInfo.tsx @@ -11,6 +11,7 @@ import { ColoredContainer, Container, FieldCheckbox, Group, GroupTitle, Placehol import { useService } from '@cloudbeaver/core-di'; import { TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; +import { AdministrationUsersManagementService } from '../../../../AdministrationUsersManagementService'; import type { UserFormProps } from '../AdministrationUserFormService'; import { UserFormInfoCredentials } from './UserFormInfoCredentials'; import { UserFormInfoMetaParameters } from './UserFormInfoMetaParameters'; @@ -23,10 +24,12 @@ export const UserFormInfo: TabContainerPanelComponent = observer( const tab = useTab(tabId); const tabState = useTabState(); const userFormInfoPartService = useService(UserFormInfoPartService); + const administrationUsersManagementService = useService(AdministrationUsersManagementService); - useAutoLoad(UserFormInfo, tabState, tab.selected); + useAutoLoad(UserFormInfo, [tabState, ...administrationUsersManagementService.loaders], tab.selected); const disabled = tabState.isLoading(); + const userManagementDisabled = administrationUsersManagementService.externalUserProviderEnabled; // let info: TLocalizationToken | null = null; // if (formState.mode === FormMode.Edit && tabState.isChanged()) { @@ -43,7 +46,7 @@ export const UserFormInfo: TabContainerPanelComponent = observer( {translate('authentication_user_status')} - + {translate('authentication_user_enabled')} diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserBootstrap.ts b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserBootstrap.ts index 2a8edb1a49..0122c1cea0 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserBootstrap.ts +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/CreateUserBootstrap.ts @@ -11,6 +11,7 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; import { ACTION_CREATE, ActionService, MenuService } from '@cloudbeaver/core-view'; +import { AdministrationUsersManagementService } from '../../../AdministrationUsersManagementService'; import { MENU_USERS_ADMINISTRATION } from '../../../Menus/MENU_USERS_ADMINISTRATION'; import { ADMINISTRATION_ITEM_USER_CREATE_PARAM } from '../ADMINISTRATION_ITEM_USER_CREATE_PARAM'; import { CreateUserService } from './CreateUserService'; @@ -22,6 +23,7 @@ export class CreateUserBootstrap extends Bootstrap { private readonly createUserService: CreateUserService, private readonly menuService: MenuService, private readonly actionService: ActionService, + private readonly administrationUsersManagementService: AdministrationUsersManagementService, ) { super(); } @@ -37,7 +39,7 @@ export class CreateUserBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'users-table-base', isActionApplicable: (context, action) => { - if (action === ACTION_CREATE) { + if (action === ACTION_CREATE && !this.administrationUsersManagementService.externalUserProviderEnabled) { return this.authProvidersResource.has(AUTH_PROVIDER_LOCAL_ID); } @@ -52,9 +54,10 @@ export class CreateUserBootstrap extends Bootstrap { return false; }, - getLoader: (context, action) => { - return getCachedMapResourceLoaderState(this.authProvidersResource, () => CachedMapAllKey); - }, + getLoader: () => [ + getCachedMapResourceLoaderState(this.authProvidersResource, () => CachedMapAllKey), + ...this.administrationUsersManagementService.loaders, + ], handler: (context, action) => { switch (action) { case ACTION_CREATE: diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/User.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/User.tsx index 8fdb3d96f4..7832f8b508 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/User.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/User.tsx @@ -9,10 +9,21 @@ import { observer } from 'mobx-react-lite'; import styled, { css, use } from 'reshadow'; import { AdminUser, UsersResource } from '@cloudbeaver/core-authentication'; -import { Checkbox, Loader, Placeholder, TableColumnValue, TableItem, TableItemExpand, TableItemSelect, useTranslate } from '@cloudbeaver/core-blocks'; +import { + Checkbox, + Loader, + Placeholder, + TableColumnValue, + TableItem, + TableItemExpand, + TableItemSelect, + useAutoLoad, + useTranslate, +} from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; +import { AdministrationUsersManagementService } from '../../../AdministrationUsersManagementService'; import { UsersAdministrationService } from '../UsersAdministrationService'; import { UserEdit } from './UserEdit'; @@ -36,8 +47,11 @@ export const User = observer(function User({ user, displayAuthRole, selec const teams = user.grantedTeams.join(', '); const usersService = useService(UsersResource); const notificationService = useService(NotificationService); + const administrationUsersManagementService = useService(AdministrationUsersManagementService); const translate = useTranslate(); + useAutoLoad(User, administrationUsersManagementService.loaders); + async function handleEnabledCheckboxChange(enabled: boolean) { try { await usersService.enableUser(user.userId, enabled); @@ -50,6 +64,8 @@ export const User = observer(function User({ user, displayAuthRole, selec ? translate('administration_teams_team_granted_users_permission_denied') : undefined; + const userManagementDisabled = administrationUsersManagementService.externalUserProviderEnabled; + return styled(styles)( {selectable && ( @@ -74,7 +90,7 @@ export const User = observer(function User({ user, displayAuthRole, selec diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationToolsPanel.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationToolsPanel.tsx index e778893bf2..a42168a3ad 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationToolsPanel.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersAdministrationToolsPanel.tsx @@ -7,7 +7,7 @@ */ import { observer } from 'mobx-react-lite'; -import { s, SContext, StyleRegistry, ToolsAction, ToolsPanel, useResource, useTranslate } from '@cloudbeaver/core-blocks'; +import { s, SContext, StyleRegistry, ToolsAction, ToolsPanel, useTranslate } from '@cloudbeaver/core-blocks'; import { MenuBar, MenuBarItemStyles } from '@cloudbeaver/core-ui'; import { useMenu } from '@cloudbeaver/core-view'; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersPage.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersPage.tsx index 1ce6337740..29bdd6d386 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersPage.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/Users/UsersTable/UsersPage.tsx @@ -10,9 +10,10 @@ import styled from 'reshadow'; import { ADMINISTRATION_TOOLS_PANEL_STYLES, IAdministrationItemSubItem } from '@cloudbeaver/core-administration'; import { AuthRolesResource } from '@cloudbeaver/core-authentication'; -import { ColoredContainer, Container, Group, Placeholder, useResource, useStyles } from '@cloudbeaver/core-blocks'; +import { ColoredContainer, Container, Group, Placeholder, useAutoLoad, useResource, useStyles } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; +import { AdministrationUsersManagementService } from '../../../AdministrationUsersManagementService'; import { CreateUser } from './CreateUser'; import { CreateUserService } from './CreateUserService'; import { UsersTableFilters } from './Filters/UsersTableFilters'; @@ -30,13 +31,16 @@ export const UsersPage = observer(function UsersPage({ sub, param }) { const style = useStyles(ADMINISTRATION_TOOLS_PANEL_STYLES); const createUserService = useService(CreateUserService); const authRolesResource = useResource(UsersPage, AuthRolesResource, undefined); + const administrationUsersManagementService = useService(AdministrationUsersManagementService); + useAutoLoad(UsersPage, administrationUsersManagementService.loaders); const filters = useUsersTableFilters(); const table = useUsersTable(filters); const create = param === 'create'; const displayAuthRole = authRolesResource.data.length > 0; const loading = authRolesResource.isLoading() || table.loadableState.isLoading(); + const userManagementDisabled = administrationUsersManagementService.externalUserProviderEnabled; return styled(style)( @@ -45,7 +49,7 @@ export const UsersPage = observer(function UsersPage({ sub, param }) { - {create && createUserService.state && ( + {create && createUserService.state && !userManagementDisabled && ( diff --git a/webapp/packages/plugin-authentication-administration/src/AdministrationUsersManagementService.ts b/webapp/packages/plugin-authentication-administration/src/AdministrationUsersManagementService.ts new file mode 100644 index 0000000000..3c97e3b681 --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/AdministrationUsersManagementService.ts @@ -0,0 +1,42 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { computed, makeObservable } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { type ILoadableState, isArraysEqual } from '@cloudbeaver/core-utils'; + +import { externalUserProviderStatusContext } from './externalUserProviderStatusContext'; + +@injectable() +export class AdministrationUsersManagementService { + get externalUserProviderEnabled(): boolean { + const contexts = this.getExternalUserProviderStatus.execute(); + const projectsContext = contexts.getContext(externalUserProviderStatusContext); + + return projectsContext.externalUserProviderEnabled; + } + + get loaders(): ILoadableState[] { + const contexts = this.getExternalUserProviderStatus.execute(); + const projectsContext = contexts.getContext(externalUserProviderStatusContext); + + return projectsContext.loaders; + } + + readonly getExternalUserProviderStatus: ISyncExecutor; + + constructor() { + makeObservable(this, { + externalUserProviderEnabled: computed, + loaders: computed({ equals: (a, b) => isArraysEqual(a, b) }), + }); + + this.getExternalUserProviderStatus = new SyncExecutor(); + } +} diff --git a/webapp/packages/plugin-authentication-administration/src/externalUserProviderStatusContext.ts b/webapp/packages/plugin-authentication-administration/src/externalUserProviderStatusContext.ts new file mode 100644 index 0000000000..9758e7223d --- /dev/null +++ b/webapp/packages/plugin-authentication-administration/src/externalUserProviderStatusContext.ts @@ -0,0 +1,28 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 { ILoadableState } from '@cloudbeaver/core-utils'; + +interface IExternalUserProviderStatusContext { + externalUserProviderEnabled: boolean; + loaders: ILoadableState[]; + setExternalUserProviderStatus(enabled: boolean): void; + addLoader(loader: ILoadableState): void; +} + +export function externalUserProviderStatusContext(): IExternalUserProviderStatusContext { + return { + externalUserProviderEnabled: false, + loaders: [], + setExternalUserProviderStatus(enabled: boolean) { + this.externalUserProviderEnabled = enabled; + }, + addLoader(loader: ILoadableState) { + this.loaders.push(loader); + }, + }; +} diff --git a/webapp/packages/plugin-authentication-administration/src/index.ts b/webapp/packages/plugin-authentication-administration/src/index.ts index 84d37c2ac7..5d86833a85 100644 --- a/webapp/packages/plugin-authentication-administration/src/index.ts +++ b/webapp/packages/plugin-authentication-administration/src/index.ts @@ -17,3 +17,5 @@ export * from './Administration/Users/UserForm/Info/UserFormInfoPartService'; export * from './Administration/IdentityProviders/IAuthConfigurationFormProps'; export * from './Administration/IdentityProviders/AuthConfigurationFormService'; export * from './Menus/MENU_USERS_ADMINISTRATION'; +export * from './AdministrationUsersManagementService'; +export * from './externalUserProviderStatusContext'; diff --git a/webapp/packages/plugin-authentication-administration/src/manifest.ts b/webapp/packages/plugin-authentication-administration/src/manifest.ts index 1bd7799b4c..3a47539343 100644 --- a/webapp/packages/plugin-authentication-administration/src/manifest.ts +++ b/webapp/packages/plugin-authentication-administration/src/manifest.ts @@ -30,6 +30,7 @@ import { UsersAdministrationNavigationService } from './Administration/Users/Use import { UsersAdministrationService } from './Administration/Users/UsersAdministrationService'; import { CreateUserBootstrap } from './Administration/Users/UsersTable/CreateUserBootstrap'; import { CreateUserService } from './Administration/Users/UsersTable/CreateUserService'; +import { AdministrationUsersManagementService } from './AdministrationUsersManagementService'; import { AuthenticationLocaleService } from './AuthenticationLocaleService'; import { PluginBootstrap } from './PluginBootstrap'; @@ -64,5 +65,6 @@ export const manifest: PluginManifest = { UserFormOriginPartBootstrap, UserFormConnectionAccessPartBootstrap, UserFormInfoPartService, + AdministrationUsersManagementService, ], }; diff --git a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx index f2d40524a8..fc1cee6b42 100644 --- a/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx +++ b/webapp/packages/plugin-data-viewer/src/ValuePanelPresentation/ImageValue/ImageValuePresentation.tsx @@ -8,7 +8,7 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react-lite'; -import { ActionIconButtonStyles, Button, Container, Fill, IconButton, s, useObservableRef, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { ActionIconButton, Button, Container, Fill, s, useObservableRef, useS, useTranslate } from '@cloudbeaver/core-blocks'; import { selectFiles } from '@cloudbeaver/core-browser'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; @@ -44,33 +44,14 @@ const Tools = observer(function Tools({ loading, stretch, onToggleS return ( - {onSave && ( - - )} - {onUpload && ( - - )} + {onSave && } + {onUpload && } {onToggleStretch && ( - diff --git a/webapp/packages/plugin-devtools/src/DevToolsService.ts b/webapp/packages/plugin-devtools/src/DevToolsService.ts index 9d0f33e5cf..3e291d82e7 100644 --- a/webapp/packages/plugin-devtools/src/DevToolsService.ts +++ b/webapp/packages/plugin-devtools/src/DevToolsService.ts @@ -14,6 +14,7 @@ import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; interface IDevToolsSettings { enabled: boolean; distributed: boolean; + configuration: boolean; } const DEVTOOLS = 'devtools'; @@ -28,6 +29,10 @@ export class DevToolsService { return this.settings.distributed; } + get isConfiguration(): boolean { + return this.settings.configuration; + } + private readonly settings: IDevToolsSettings; constructor(private readonly serverConfigResource: ServerConfigResource, private readonly autoSaveService: LocalStorageSaveService) { @@ -37,23 +42,29 @@ export class DevToolsService { settings: observable, }); this.autoSaveService.withAutoSave(DEVTOOLS, this.settings, getDefaultDevToolsSettings); - this.serverConfigResource.onDataUpdate.addHandler(this.syncDistributedMode.bind(this)); + this.serverConfigResource.onDataUpdate.addHandler(this.syncSettingsOverride.bind(this)); } switch() { this.settings.enabled = !this.settings.enabled; - this.syncDistributedMode(); + this.syncSettingsOverride(); } setDistributedMode(distributed: boolean) { this.settings.distributed = distributed; - this.syncDistributedMode(); + this.syncSettingsOverride(); + } + + setConfigurationMode(configuration: boolean) { + this.settings.configuration = configuration; + this.syncSettingsOverride(); } - private syncDistributedMode() { + private syncSettingsOverride() { if (this.isEnabled) { if (this.serverConfigResource.data) { this.serverConfigResource.data.distributed = this.isDistributed; + this.serverConfigResource.data.configurationMode = this.isConfiguration; } } } @@ -61,7 +72,8 @@ export class DevToolsService { function getDefaultDevToolsSettings(): IDevToolsSettings { return { - enabled: false, + enabled: process.env.NODE_ENV === 'development', distributed: false, + configuration: false, }; } diff --git a/webapp/packages/plugin-devtools/src/PluginBootstrap.ts b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts index ac43aac65e..046818ea5d 100644 --- a/webapp/packages/plugin-devtools/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-devtools/src/PluginBootstrap.ts @@ -14,6 +14,7 @@ import { TOP_NAV_BAR_SETTINGS_MENU } from '@cloudbeaver/plugin-settings-menu'; import { MENU_USER_PROFILE } from '@cloudbeaver/plugin-user-profile'; import { ACTION_DEVTOOLS } from './actions/ACTION_DEVTOOLS'; +import { ACTION_DEVTOOLS_MODE_CONFIGURATION } from './actions/ACTION_DEVTOOLS_MODE_CONFIGURATION'; import { ACTION_DEVTOOLS_MODE_DISTRIBUTED } from './actions/ACTION_DEVTOOLS_MODE_DISTRIBUTED'; import { DATA_CONTEXT_MENU_SEARCH } from './ContextMenu/DATA_CONTEXT_MENU_SEARCH'; import { SearchResourceMenuItem } from './ContextMenu/SearchResourceMenuItem'; @@ -97,7 +98,16 @@ export class PluginBootstrap extends Bootstrap { ]; } - return [new SearchResourceMenuItem(), ACTION_DEVTOOLS_MODE_DISTRIBUTED, MENU_PLUGINS, ...items]; + return [new SearchResourceMenuItem(), ACTION_DEVTOOLS_MODE_DISTRIBUTED, ACTION_DEVTOOLS_MODE_CONFIGURATION, MENU_PLUGINS, ...items]; + }, + }); + + this.actionService.addHandler({ + id: 'devtools-mode-configuration', + isActionApplicable: (context, action) => action === ACTION_DEVTOOLS_MODE_CONFIGURATION, + isChecked: () => this.devToolsService.isConfiguration, + handler: () => { + this.devToolsService.setConfigurationMode(!this.devToolsService.isConfiguration); }, }); diff --git a/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS_MODE_CONFIGURATION.ts b/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS_MODE_CONFIGURATION.ts new file mode 100644 index 0000000000..74b3bdf1ef --- /dev/null +++ b/webapp/packages/plugin-devtools/src/actions/ACTION_DEVTOOLS_MODE_CONFIGURATION.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 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 { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_DEVTOOLS_MODE_CONFIGURATION = createAction('devtools-mode-configuration', { + type: 'checkbox', + label: 'Easy config mode', + tooltip: 'Enable easy config mode', +}); diff --git a/webapp/packages/plugin-navigation-tree-filters/src/NavigationTreeFiltersDialog/FiltersTableItem.tsx b/webapp/packages/plugin-navigation-tree-filters/src/NavigationTreeFiltersDialog/FiltersTableItem.tsx index c316e6b100..be6f80e702 100644 --- a/webapp/packages/plugin-navigation-tree-filters/src/NavigationTreeFiltersDialog/FiltersTableItem.tsx +++ b/webapp/packages/plugin-navigation-tree-filters/src/NavigationTreeFiltersDialog/FiltersTableItem.tsx @@ -7,7 +7,7 @@ */ import { observer } from 'mobx-react-lite'; -import { ActionIconButtonStyles, IconButton, s, TableColumnValue, TableItem, useS } from '@cloudbeaver/core-blocks'; +import { ActionIconButton, s, TableColumnValue, TableItem, useS } from '@cloudbeaver/core-blocks'; import styles from './FiltersTableItem.m.css'; @@ -20,13 +20,13 @@ interface Props { } export const FiltersTableItem = observer(function FiltersTableItem({ id, name, disabled, className, onDelete }) { - const style = useS(ActionIconButtonStyles, styles); + const style = useS(styles); return ( {name} - onDelete(id)} /> + onDelete(id)} /> ); diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx index aa5350864b..90099bae30 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx @@ -10,9 +10,8 @@ import React, { useState } from 'react'; import styled from 'reshadow'; import { - ActionIconButtonStyles, + ActionIconButton, Fill, - IconButton, IconButtonStyles, PlaceholderElement, s, @@ -56,7 +55,7 @@ export const ElementsTreeTools = observer>(functi const translate = useTranslate(); const [opened, setOpen] = useState(false); const deprecatedStyles = useStyles(style); - const styles = useS(ElementsTreeToolsStyles, ElementsTreeToolsIconButtonStyles, ActionIconButtonStyles); + const styles = useS(ElementsTreeToolsStyles, ElementsTreeToolsIconButtonStyles); useCaptureViewContext(context => { context?.set(DATA_CONTEXT_NAV_TREE_ROOT, tree.baseRoot); @@ -70,21 +69,21 @@ export const ElementsTreeTools = observer>(functi
{tree.settings?.configurable && ( - setOpen(!opened)} /> )} - tree.refresh(root)} /> diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts index 20b99daca7..f84a1f4ab4 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/ISQLEditorData.ts @@ -59,6 +59,7 @@ export interface ISQLEditorData { executeScript(): Promise; switchEditing(): Promise; getHintProposals(position: number, simple: boolean): Promise; + getResolvedSegment(): Promise; executeQueryAction( segment: ISQLScriptSegment | undefined, action: (query: ISQLScriptSegment) => Promise, diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBar.m.css b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBar.m.css index 6622623401..a8066255f9 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBar.m.css +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/SqlEditorActionsMenuBar.m.css @@ -1,3 +1,8 @@ +.sqlActions { + display: flex; + flex-direction: column; +} + .sqlActions.menuBar { height: unset; width: 32px; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts index 1b275724b8..0e9eccaf61 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useSqlEditor.ts @@ -54,7 +54,6 @@ interface ISQLEditorDataPrivate extends ISQLEditorData { updateParserScripts(): Promise; loadDatabaseDataModels(): Promise; getExecutingQuery(script: boolean): ISQLScriptSegment | undefined; - getResolvedSegment(): Promise; getSubQuery(): ISQLScriptSegment | undefined; } diff --git a/webapp/packages/product-default/src/LocaleService.ts b/webapp/packages/product-default/src/LocaleService.ts new file mode 100644 index 0000000000..e3649a06b4 --- /dev/null +++ b/webapp/packages/product-default/src/LocaleService.ts @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2023 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 readonly 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; + case 'it': + return (await import('./locales/it')).default; + case 'zh': + return (await import('./locales/zh')).default; + default: + return (await import('./locales/en')).default; + } + } +} diff --git a/webapp/packages/product-default/src/locales/en.ts b/webapp/packages/product-default/src/locales/en.ts new file mode 100644 index 0000000000..4e6434307f --- /dev/null +++ b/webapp/packages/product-default/src/locales/en.ts @@ -0,0 +1,4 @@ +export default [ + ['product_name', 'CE'], + ['product_full_name', 'CloudBeaver Community'], +]; diff --git a/webapp/packages/product-default/src/locales/it.ts b/webapp/packages/product-default/src/locales/it.ts new file mode 100644 index 0000000000..4e6434307f --- /dev/null +++ b/webapp/packages/product-default/src/locales/it.ts @@ -0,0 +1,4 @@ +export default [ + ['product_name', 'CE'], + ['product_full_name', 'CloudBeaver Community'], +]; diff --git a/webapp/packages/product-default/src/locales/ru.ts b/webapp/packages/product-default/src/locales/ru.ts new file mode 100644 index 0000000000..4e6434307f --- /dev/null +++ b/webapp/packages/product-default/src/locales/ru.ts @@ -0,0 +1,4 @@ +export default [ + ['product_name', 'CE'], + ['product_full_name', 'CloudBeaver Community'], +]; diff --git a/webapp/packages/product-default/src/locales/zh.ts b/webapp/packages/product-default/src/locales/zh.ts new file mode 100644 index 0000000000..4e6434307f --- /dev/null +++ b/webapp/packages/product-default/src/locales/zh.ts @@ -0,0 +1,4 @@ +export default [ + ['product_name', 'CE'], + ['product_full_name', 'CloudBeaver Community'], +]; diff --git a/webapp/packages/product-default/src/manifest.ts b/webapp/packages/product-default/src/manifest.ts index 6ad91cadbd..7663e77c49 100644 --- a/webapp/packages/product-default/src/manifest.ts +++ b/webapp/packages/product-default/src/manifest.ts @@ -7,6 +7,7 @@ */ import type { PluginManifest } from '@cloudbeaver/core-di'; +import { LocaleService } from './LocaleService'; import { ProductBootstrap } from './ProductBootstrap'; import { ProductConfigService } from './ProductConfigService'; @@ -15,5 +16,5 @@ export const defaultProductManifest: PluginManifest = { name: 'Default Product', }, - providers: [ProductBootstrap, ProductConfigService], + providers: [ProductBootstrap, ProductConfigService, LocaleService], };