diff --git a/.vscode/launch.json b/.vscode/launch.json index 48e9e385c6..7a20f1048b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,16 +7,13 @@ "name": "Chrome", "url": "http://localhost:8080", "webRoot": "${workspaceFolder}/..", + "outFiles": [ + "${workspaceFolder}/../cloudbeaver/packages/**/dist/**/*.{js,jsx}" + ], + "smartStep": true, "sourceMaps": true, - "sourceMapPathOverrides": { - "webpack:///*": "${workspaceFolder}/../*" - }, - "skipFiles": [ - "/**", - "**/node_modules/**", - "${workspaceFolder}/webapp/**/node_modules/**/*.js", - "${workspaceFolder}/webapp/**/dist/**/*.js" - ] + "disableNetworkCache": true, + "skipFiles": ["/**", "**/node_modules/**"] }, { "type": "java", diff --git a/config/core/product.conf b/config/core/product.conf deleted file mode 100644 index 794e407b07..0000000000 --- a/config/core/product.conf +++ /dev/null @@ -1,32 +0,0 @@ -// Product configuration. Customized web application behavior -// It is in JSONC format -{ - // Global properties - core: { - // User defaults - user: { - defaultTheme: "light", - defaultLanguage: "en" - }, - app: { - // Log viewer config - logViewer: { - refreshTimeout: 3000, - logBatchSize: 1000, - maxLogRecords: 2000, - maxFailedRequests: 3 - } - } - }, - // Notifications config - core_events: { - notificationsPool: 20 - }, - plugin_data_spreadsheet_new: { - hidden: false - }, - plugin_data_export: { - disabled: false - }, - sql.proposals.insert.table.alias: true -} diff --git a/config/sample-databases/DefaultConfiguration/cloudbeaver.conf b/config/sample-databases/DefaultConfiguration/cloudbeaver.conf index 4102f85217..bc430e6e37 100644 --- a/config/sample-databases/DefaultConfiguration/cloudbeaver.conf +++ b/config/sample-databases/DefaultConfiguration/cloudbeaver.conf @@ -11,7 +11,21 @@ rootURI: "/", serviceURI: "/api/", - productConfiguration: "conf/product.conf", + productSettings: { + # Global properties + core.theming.theme: 'light', + core.localization.localization: 'en', + plugin.sql-editor.autoSave: true, + plugin.sql-editor.disabled: false, + # max size of the file that can be uploaded to the editor (in kilobytes) + plugin.sql-editor.maxFileSize: 10240, + plugin.log-viewer.disabled: false, + plugin.log-viewer.logBatchSize: 1000, + plugin.log-viewer.maxFailedRequests: 3, + plugin.log-viewer.maxLogRecords: 2000, + plugin.log-viewer.refreshTimeout: 3000, + sql.proposals.insert.table.alias: PLAIN + }, expireSessionAfterPeriod: 1800000, diff --git a/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf b/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf index aceb52c885..973c329836 100644 --- a/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf +++ b/config/sample-databases/SQLiteConfiguration/cloudbeaver.conf @@ -9,7 +9,21 @@ rootURI: "/", serviceURI: "/api/", - productConfiguration: "conf/product.conf", + productSettings: { + # Global properties + core.theming.theme: 'light', + core.localization.localization: 'en', + plugin.sql-editor.autoSave: true, + plugin.sql-editor.disabled: false, + # max size of the file that can be uploaded to the editor (in kilobytes) + plugin.sql-editor.maxFileSize: 10240, + plugin.log-viewer.disabled: false, + plugin.log-viewer.logBatchSize: 1000, + plugin.log-viewer.maxFailedRequests: 3, + plugin.log-viewer.maxLogRecords: 2000, + plugin.log-viewer.refreshTimeout: 3000, + sql.proposals.insert.table.alias: PLAIN + }, expireSessionAfterPeriod: 1800000, diff --git a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebPropertyInfo.java b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebPropertyInfo.java index 44c983c2d7..881d9f2922 100644 --- a/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebPropertyInfo.java +++ b/server/bundles/io.cloudbeaver.model/src/io/cloudbeaver/model/WebPropertyInfo.java @@ -18,6 +18,7 @@ package io.cloudbeaver.model; import io.cloudbeaver.model.session.WebSession; import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.model.*; import org.jkiss.dbeaver.model.connection.DBPDriverConfigurationType; @@ -27,6 +28,7 @@ import org.jkiss.dbeaver.model.meta.Property; import org.jkiss.dbeaver.model.meta.PropertyLength; import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; import org.jkiss.dbeaver.model.preferences.DBPPropertySource; +import org.jkiss.dbeaver.registry.settings.ProductSettingDescriptor; import org.jkiss.dbeaver.runtime.properties.ObjectPropertyDescriptor; import org.jkiss.utils.CommonUtils; @@ -242,4 +244,12 @@ public class WebPropertyInfo { return CommonUtils.toString(value); } + @Nullable + @Property + public List getScopes() { + if (property instanceof ProductSettingDescriptor productSettingDescriptor) { + return productSettingDescriptor.getScopes(); + } + return null; + } } diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index faf8c4a166..3b90f3f59a 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -38,6 +38,8 @@ type ObjectPropertyInfo { supportedConfigurationTypes: [ String! ] # Required parameters required: Boolean! @since(version: "23.3.1") + # Parameter scopes + scopes:[String!] } enum ObjectPropertyLength { @@ -162,6 +164,16 @@ type ServerConfig { passwordPolicyConfiguration: PasswordPolicyConfig! @since(version: "23.3.3") } +type ProductSettingsGroup @since(version: "24.0.1") { + id: ID! + displayName: String! +} + +type ProductSettings @since(version: "24.0.1") { + groups: [ProductSettingsGroup!]! + settings: [ObjectPropertyInfo!]! # each property is associated with a group by category +} + type SessionInfo { createTime: String! lastAccessTime: String! @@ -530,6 +542,8 @@ extend type Query { # Returns server config serverConfig: ServerConfig! + productSettings: ProductSettings! @since(version: "24.0.1") + # Returns session state ( initialize if not ) sessionState: SessionInfo! diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductSettings.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductSettings.java new file mode 100644 index 0000000000..25cbd78f26 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebProductSettings.java @@ -0,0 +1,58 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.model; + +import io.cloudbeaver.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.impl.PropertyGroupDescriptor; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.registry.settings.ProductSettingDescriptor; + +import java.util.List; +import java.util.stream.Collectors; + +public class WebProductSettings { + @NotNull + private final WebSession webSession; + @NotNull + private final List> groups; + + public WebProductSettings( + @NotNull WebSession webSession, + @NotNull List> groups + ) { + this.webSession = webSession; + this.groups = groups; + } + + @NotNull + @Property + public List getGroups() { + return groups.stream() + .map(WebSettingsGroupInfo::new) + .collect(Collectors.toList()); + } + + @NotNull + @Property + public List getSettings() { + return groups.stream() + .flatMap(group -> group.getSettings().stream()) + .map(setting -> new WebPropertyInfo(webSession, setting)) + .collect(Collectors.toList()); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebSettingsGroupInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebSettingsGroupInfo.java new file mode 100644 index 0000000000..4ba073e621 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebSettingsGroupInfo.java @@ -0,0 +1,43 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.model; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.impl.PropertyGroupDescriptor; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.registry.settings.ProductSettingDescriptor; + +public class WebSettingsGroupInfo { + @NotNull + private final PropertyGroupDescriptor groupDescriptor; + + public WebSettingsGroupInfo(@NotNull PropertyGroupDescriptor groupDescriptor) { + this.groupDescriptor = groupDescriptor; + } + + @NotNull + @Property + public String getId() { + return groupDescriptor.getFullId(); + } + + @NotNull + @Property + public String getDisplayName() { + return groupDescriptor.getDisplayName(); + } +} 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 cc8e837009..280a8ec598 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 @@ -728,6 +728,7 @@ public abstract class CBApplication extends BaseWebApp public void saveProductConfiguration(SMCredentialsProvider credentialsProvider, Map productConfiguration) throws DBException { getServerConfigurationController().saveProductConfiguration(productConfiguration); + flushConfiguration(credentialsProvider); sendConfigChangedEvent(credentialsProvider); } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java index 672c94a040..f87ae5e1cd 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBConstants.java @@ -36,7 +36,10 @@ public class CBConstants { public static final String PARAM_SERVICES_URI = "serviceURI"; public static final String PARAM_DRIVERS_LOCATION = "driversLocation"; public static final String PARAM_WORKSPACE_LOCATION = "workspaceLocation"; + //legacy path to a product.conf file + @Deprecated public static final String PARAM_PRODUCT_CONFIGURATION = "productConfiguration"; + public static final String PARAM_PRODUCT_SETTINGS = "productSettings"; public static final String PARAM_EXTERNAL_PROPERTIES = "externalProperties"; public static final String PARAM_STATIC_CONTENT = "staticContent"; public static final String PARAM_RESOURCE_QUOTAS = "resourceQuotas"; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfig.java index 73f935ada6..2b0b6d0bf7 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfig.java @@ -20,11 +20,13 @@ import com.google.gson.annotations.SerializedName; import io.cloudbeaver.auth.CBAuthConstants; import io.cloudbeaver.model.app.WebServerConfiguration; import io.cloudbeaver.service.security.db.WebDatabaseConfig; +import org.jkiss.code.NotNull; import org.jkiss.dbeaver.Log; import org.jkiss.utils.CommonUtils; import java.net.InetAddress; import java.net.UnknownHostException; +import java.util.HashMap; import java.util.Map; public class CBServerConfig implements WebServerConfiguration { @@ -46,6 +48,7 @@ public class CBServerConfig implements WebServerConfiguration { private long maxSessionIdleTime = CBAuthConstants.MAX_SESSION_IDLE_TIME; private boolean develMode = false; private boolean enableSecurityManager = false; + private final Map productSettings = new HashMap<>(); @SerializedName("database") private WebDatabaseConfig databaseConfiguration = new WebDatabaseConfig(); @@ -184,4 +187,8 @@ public class CBServerConfig implements WebServerConfiguration { return enableSecurityManager; } + @NotNull + public Map getProductSettings() { + return productSettings; + } } diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java index 2565717ce6..19c8a3018b 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationController.java @@ -50,12 +50,12 @@ import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; -public abstract class CBServerConfigurationController extends BaseServerConfigurationController { +public abstract class CBServerConfigurationController + extends BaseServerConfigurationController { private static final Log log = Log.getLog(CBServerConfigurationController.class); // Configurations - protected final Map productConfiguration = new HashMap<>(); protected final SMControllerConfiguration securityManagerConfiguration = new SMControllerConfiguration(); private final T serverConfiguration; private final CBAppConfig appConfiguration = new CBAppConfig(); @@ -113,7 +113,7 @@ public abstract class CBServerConfigurationController // Merge new config with old one mergeOldConfiguration(prevConfig); - patchConfigurationWithProperties(productConfiguration); + patchConfigurationWithProperties(getServerConfiguration().getProductSettings()); } protected void parseConfiguration(Map configProps) throws DBException { @@ -123,8 +123,11 @@ public abstract class CBServerConfigurationController patchConfigurationWithProperties(configProps); // patch again because properties can be changed Gson gson = getGson(); + Map currentConfigurationAsMap = gson.fromJson(gson.toJson(getServerConfiguration()), + JSONUtils.MAP_TYPE_TOKEN); + serverConfig = WebAppUtils.mergeConfigurations(currentConfigurationAsMap, serverConfig); gson.fromJson( - gson.toJsonTree(serverConfig), + gson.toJson(serverConfig), getServerConfiguration().getClass() ); @@ -132,16 +135,14 @@ public abstract class CBServerConfigurationController //SM config gson.fromJson( - gson.toJsonTree(JSONUtils.getObject(serverConfig, CBConstants.PARAM_SM_CONFIGURATION)), + gson.toJson(JSONUtils.getObject(serverConfig, CBConstants.PARAM_SM_CONFIGURATION)), SMControllerConfiguration.class ); // App config Map appConfig = JSONUtils.getObject(configProps, "app"); validateConfiguration(appConfig); - gson.fromJson(gson.toJsonTree(appConfig), CBAppConfig.class); - + gson.fromJson(gson.toJson(appConfig), CBAppConfig.class); readProductConfiguration(serverConfig, gson); - } public T parseServerConfiguration() { @@ -212,26 +213,31 @@ public abstract class CBServerConfigurationController protected void readProductConfiguration(Map serverConfig, Gson gson) throws DBException { - String productConfigPath = WebAppUtils.getRelativePath( - JSONUtils.getString( - serverConfig, - CBConstants.PARAM_PRODUCT_CONFIGURATION, - CBConstants.DEFAULT_PRODUCT_CONFIGURATION - ), - CBApplication.getInstance().getHomeDirectory().toString() - ); - - if (!CommonUtils.isEmpty(productConfigPath)) { - File productConfigFile = new File(productConfigPath); - if (!productConfigFile.exists()) { - log.error("Product configuration file not found (" + productConfigFile.getAbsolutePath() + "'"); - } else { - log.debug("Load product configuration from '" + productConfigFile.getAbsolutePath() + "'"); - try (Reader reader = new InputStreamReader(new FileInputStream(productConfigFile), - StandardCharsets.UTF_8)) { - productConfiguration.putAll(JSONUtils.parseMap(gson, reader)); - } catch (Exception e) { - throw new DBException("Error reading product configuration", e); + // legacy configuration with path to product.conf file + if (!serverConfig.containsKey(CBConstants.PARAM_PRODUCT_SETTINGS) + && serverConfig.get(CBConstants.PARAM_PRODUCT_CONFIGURATION) instanceof String + ) { + String productConfigPath = WebAppUtils.getRelativePath( + JSONUtils.getString( + serverConfig, + CBConstants.PARAM_PRODUCT_CONFIGURATION, + CBConstants.DEFAULT_PRODUCT_CONFIGURATION + ), + CBApplication.getInstance().getHomeDirectory().toString() + ); + if (!CommonUtils.isEmpty(productConfigPath)) { + File productConfigFile = new File(productConfigPath); + if (!productConfigFile.exists()) { + log.error("Product configuration file not found (" + productConfigFile.getAbsolutePath() + "'"); + } else { + log.debug("Load product configuration from '" + productConfigFile.getAbsolutePath() + "'"); + try (Reader reader = new InputStreamReader(new FileInputStream(productConfigFile), + StandardCharsets.UTF_8)) { + serverConfiguration.getProductSettings() + .putAll(WebAppUtils.flattenMap(JSONUtils.parseMap(gson, reader))); + } catch (Exception e) { + throw new DBException("Error reading product configuration", e); + } } } } @@ -241,10 +247,12 @@ public abstract class CBServerConfigurationController if (rtConfig.exists()) { log.debug("Load product runtime configuration from '" + rtConfig.getAbsolutePath() + "'"); try (Reader reader = new InputStreamReader(new FileInputStream(rtConfig), StandardCharsets.UTF_8)) { - productConfiguration.putAll(JSONUtils.parseMap(gson, reader)); - Map flattenConfig = WebAppUtils.flattenMap(this.productConfiguration); - this.productConfiguration.clear(); - this.productConfiguration.putAll(flattenConfig); + var runtimeProductSettings = JSONUtils.parseMap(gson, reader); + var productSettings = serverConfiguration.getProductSettings(); + productSettings.putAll(runtimeProductSettings); + Map flattenConfig = WebAppUtils.flattenMap(productSettings); + productSettings.clear(); + productSettings.putAll(flattenConfig); } catch (Exception e) { throw new DBException("Error reading product runtime configuration", e); } @@ -332,12 +340,14 @@ public abstract class CBServerConfigurationController writeRuntimeConfig(getRuntimeAppConfigPath(), configurationProperties); } - private void writeRuntimeConfig(Path runtimeConfigPath, Map configurationProperties) throws DBException { + private void writeRuntimeConfig(Path runtimeConfigPath, Map configurationProperties) + throws DBException { if (Files.exists(runtimeConfigPath)) { ContentUtils.makeFileBackup(runtimeConfigPath); } - try (Writer out = new OutputStreamWriter(new FileOutputStream(runtimeConfigPath.toFile()), StandardCharsets.UTF_8)) { + try (Writer out = new OutputStreamWriter(new FileOutputStream(runtimeConfigPath.toFile()), + StandardCharsets.UTF_8)) { Gson gson = new GsonBuilder() .setLenient() .setPrettyPrinting() @@ -358,7 +368,10 @@ public abstract class CBServerConfigurationController Map rootConfig = new LinkedHashMap<>(); { var originServerConfig = BaseWebApplication.getServerConfigProps(this.originalConfigurationProperties); // get server properties from original configuration file - var serverConfigProperties = collectServerConfigProperties(newServerName, newServerURL, sessionExpireTime, originServerConfig); + var serverConfigProperties = collectServerConfigProperties(newServerName, + newServerURL, + sessionExpireTime, + originServerConfig); rootConfig.put("server", serverConfigProperties); } { @@ -494,6 +507,15 @@ public abstract class CBServerConfigurationController CBConstants.PARAM_SESSION_EXPIRE_PERIOD, sessionExpireTime); } + var productConfigProperties = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + Map oldProductRuntimeConfig = JSONUtils.getObject(originServerConfig, + CBConstants.PARAM_PRODUCT_SETTINGS); + if (!CommonUtils.isEmpty(getServerConfiguration().getProductSettings())) { + for (Map.Entry mp : getServerConfiguration().getProductSettings().entrySet()) { + copyConfigValue(oldProductRuntimeConfig, productConfigProperties, mp.getKey(), mp.getValue()); + } + serverConfigProperties.put(CBConstants.PARAM_PRODUCT_SETTINGS, productConfigProperties); + } return serverConfigProperties; } @@ -557,10 +579,10 @@ public abstract class CBServerConfigurationController } public void saveProductConfiguration(Map productConfiguration) throws DBException { - Map mergedConfig = WebAppUtils.mergeConfigurations(this.productConfiguration, productConfiguration); - writeRuntimeConfig(getRuntimeProductConfigFilePath(), mergedConfig); - this.productConfiguration.clear(); - this.productConfiguration.putAll(WebAppUtils.flattenMap(mergedConfig)); + Map productSettings = getServerConfiguration().getProductSettings(); + Map mergedConfig = WebAppUtils.mergeConfigurations(productSettings, productConfiguration); + productSettings.clear(); + productSettings.putAll(WebAppUtils.flattenMap(mergedConfig)); } public T getServerConfiguration() { @@ -572,7 +594,7 @@ public abstract class CBServerConfigurationController } public Map getProductConfiguration() { - return productConfiguration; + return getServerConfiguration().getProductSettings(); } public SMControllerConfiguration getSecurityManagerConfiguration() { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java index 3ea925ad7c..c15ef7c49d 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBServerConfigurationControllerEmbedded.java @@ -20,18 +20,11 @@ import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.InstanceCreator; import io.cloudbeaver.service.security.db.WebDatabaseConfig; -import io.cloudbeaver.utils.WebAppUtils; import org.jkiss.code.NotNull; -import org.jkiss.dbeaver.DBException; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.data.json.JSONUtils; import org.jkiss.utils.CommonUtils; -import java.io.File; -import java.io.FileInputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; @@ -46,45 +39,6 @@ public class CBServerConfigurationControllerEmbedded e super(serverConfig); } - @Override - protected void readProductConfiguration(Map serverConfig, Gson gson) - throws DBException { - String productConfigPath = WebAppUtils.getRelativePath( - JSONUtils.getString( - serverConfig, - CBConstants.PARAM_PRODUCT_CONFIGURATION, - CBConstants.DEFAULT_PRODUCT_CONFIGURATION - ), - WebAppUtils.getWebApplication().getHomeDirectory().toString() - ); - - if (!CommonUtils.isEmpty(productConfigPath)) { - File productConfigFile = new File(productConfigPath); - if (!productConfigFile.exists()) { - log.error("Product configuration file not found (" + productConfigFile.getAbsolutePath() + "'"); - } else { - log.debug("Load product configuration from '" + productConfigFile.getAbsolutePath() + "'"); - try (Reader reader = new InputStreamReader(new FileInputStream(productConfigFile), - StandardCharsets.UTF_8)) { - productConfiguration.putAll(JSONUtils.parseMap(gson, reader)); - } catch (Exception e) { - throw new DBException("Error reading product configuration", e); - } - } - } - - // Add product config from runtime - File rtConfig = getRuntimeProductConfigFilePath().toFile(); - if (rtConfig.exists()) { - log.debug("Load product runtime configuration from '" + rtConfig.getAbsolutePath() + "'"); - try (Reader reader = new InputStreamReader(new FileInputStream(rtConfig), StandardCharsets.UTF_8)) { - productConfiguration.putAll(JSONUtils.parseMap(gson, reader)); - } catch (Exception e) { - throw new DBException("Error reading product runtime configuration", e); - } - } - } - @NotNull @Override protected Map collectServerConfigProperties( diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java index c8b8eb1b39..cabced5886 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/DBWServiceCore.java @@ -41,6 +41,9 @@ public interface DBWServiceCore extends DBWService { @WebAction(authRequired = false) WebServerConfig getServerConfig() throws DBWebException; + @WebAction(authRequired = false) + WebProductSettings getProductSettings(@NotNull WebSession webSession); + @WebAction List getDriverList(@NotNull WebSession webSession, String driverId) throws DBWebException; diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java index 7c55eb2c37..a04c44847f 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/WebServiceBindingCore.java @@ -53,6 +53,7 @@ public class WebServiceBindingCore extends WebServiceBindingBase WebSessionManager sessionManager = platform.getSessionManager(); model.getQueryType() .dataFetcher("serverConfig", env -> getService(env).getServerConfig()) + .dataFetcher("productSettings", env -> getService(env).getProductSettings(getWebSession(env))) .dataFetcher("driverList", env -> getService(env).getDriverList(getWebSession(env), env.getArgument("id"))) .dataFetcher("authModels", env -> getService(env).getAuthModels(getWebSession(env))) diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java index b7ecbe852f..93484258f9 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/core/impl/WebServiceCore.java @@ -62,6 +62,7 @@ import org.jkiss.dbeaver.registry.DataSourceDescriptor; import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; import org.jkiss.dbeaver.registry.network.NetworkHandlerDescriptor; import org.jkiss.dbeaver.registry.network.NetworkHandlerRegistry; +import org.jkiss.dbeaver.registry.settings.ProductSettingsRegistry; import org.jkiss.dbeaver.runtime.jobs.ConnectionTestJob; import org.jkiss.dbeaver.utils.RuntimeUtils; import org.jkiss.utils.CommonUtils; @@ -177,8 +178,7 @@ public class WebServiceCore implements DBWServiceCore { DBPDataSourceRegistry registry = project.getDataSourceRegistry(); for (DBPDataSourceContainer ds : registry.getDataSources()) { if (ds.isTemplate() && - CBPlatform.getInstance().getApplicableDrivers().contains(ds.getDriver())) - { + CBPlatform.getInstance().getApplicableDrivers().contains(ds.getDriver())) { result.add(new WebConnectionInfo(webSession, ds)); } } @@ -206,13 +206,13 @@ public class WebServiceCore implements DBWServiceCore { @NotNull DBPProject project ) { return project.getDataSourceRegistry().getAllFolders().stream() - .map(f -> new WebConnectionFolderInfo(webSession, f)).collect(Collectors.toList()); + .map(f -> new WebConnectionFolderInfo(webSession, f)).collect(Collectors.toList()); } @Override public String[] getSessionPermissions(@NotNull WebSession webSession) throws DBWebException { if (CBApplication.getInstance().isConfigurationMode()) { - return new String[] { + return new String[]{ DBWConstants.PERMISSION_ADMIN }; } @@ -224,8 +224,8 @@ public class WebServiceCore implements DBWServiceCore { @NotNull WebSession webSession, @Nullable String defaultLocale, @NotNull HttpServletRequest servletRequest, - @NotNull HttpServletResponse servletResponse) throws DBWebException - { + @NotNull HttpServletResponse servletResponse + ) throws DBWebException { for (WebSessionHandlerDescriptor hd : WebHandlerRegistry.getInstance().getSessionHandlers()) { try { hd.getInstance().handleSessionOpen(webSession, servletRequest, servletResponse); @@ -255,7 +255,11 @@ public class WebServiceCore implements DBWServiceCore { } @Override - public List readSessionLog(@NotNull WebSession webSession, Integer maxEntries, Boolean clearEntries) { + public List readSessionLog( + @NotNull WebSession webSession, + Integer maxEntries, + Boolean clearEntries + ) { return webSession.readLog(maxEntries, clearEntries); } @@ -297,7 +301,8 @@ public class WebServiceCore implements DBWServiceCore { } @Override - public boolean refreshSessionConnections(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) throws DBWebException { + public boolean refreshSessionConnections(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) + throws DBWebException { WebSession session = CBPlatform.getInstance().getSessionManager().getWebSession(request, response); if (session == null) { return false; @@ -372,10 +377,12 @@ public class WebServiceCore implements DBWServiceCore { if (networkCredentials != null) { networkCredentials.forEach(c -> { if (CommonUtils.toBoolean(c.isSavePassword())) { - DBWHandlerConfiguration handlerCfg = dataSourceContainer.getConnectionConfiguration().getHandler(c.getId()); + DBWHandlerConfiguration handlerCfg = dataSourceContainer.getConnectionConfiguration() + .getHandler(c.getId()); if (handlerCfg != null && // check username param only for ssh config - !(CommonUtils.isEmpty(c.getUserName()) && CommonUtils.equalObjects(handlerCfg.getType(), DBWHandlerType.TUNNEL)) + !(CommonUtils.isEmpty(c.getUserName()) && CommonUtils.equalObjects(handlerCfg.getType(), + DBWHandlerType.TUNNEL)) ) { WebDataSourceUtils.updateHandlerCredentials(handlerCfg, c); handlerCfg.setSavePassword(true); @@ -398,14 +405,16 @@ public class WebServiceCore implements DBWServiceCore { if (project.isUseSecretStorage()) { try { dataSourceContainer.persistSecrets( - DBSSecretController.getProjectSecretController(dataSourceContainer.getProject()) + DBSSecretController.getProjectSecretController(dataSourceContainer.getProject()) ); } catch (DBException e) { throw new DBWebException("Failed to save credentials", e); } } - WebDataSourceUtils.saveCredentialsInDataSource(connectionInfo, dataSourceContainer, dataSourceContainer.getConnectionConfiguration()); + WebDataSourceUtils.saveCredentialsInDataSource(connectionInfo, + dataSourceContainer, + dataSourceContainer.getConnectionConfiguration()); saveConfig[0] = true; } if (WebServiceUtils.isGlobalProject(dataSourceContainer.getProject())) { @@ -440,7 +449,8 @@ public class WebServiceCore implements DBWServiceCore { if (connectionConfig.isTemplate()) { connectionConfig.setSaveCredentials(false); } - DBPDataSourceContainer newDataSource = WebServiceUtils.createConnectionFromConfig(connectionConfig, sessionRegistry); + DBPDataSourceContainer newDataSource = WebServiceUtils.createConnectionFromConfig(connectionConfig, + sessionRegistry); if (CommonUtils.isEmpty(newDataSource.getName())) { newDataSource.setName(CommonUtils.notNull(connectionConfig.getName(), "NewConnection")); } @@ -456,7 +466,8 @@ public class WebServiceCore implements DBWServiceCore { WebConnectionInfo connectionInfo = new WebConnectionInfo(webSession, newDataSource); webSession.addConnection(connectionInfo); - webSession.addInfoMessage("New connection was created - " + WebServiceUtils.getConnectionContainerInfo(newDataSource)); + webSession.addInfoMessage("New connection was created - " + WebServiceUtils.getConnectionContainerInfo( + newDataSource)); WebEventUtils.addDataSourceUpdatedEvent( webSession.getProjectById(projectId), webSession, @@ -471,7 +482,8 @@ public class WebServiceCore implements DBWServiceCore { public WebConnectionInfo updateConnection( @NotNull WebSession webSession, @Nullable String projectId, - @NotNull WebConnectionConfig config) throws DBWebException { + @NotNull WebConnectionConfig config + ) throws DBWebException { // Do not check for custom connection option. Already created connections can be edited. // Also template connections can be edited // if (!CBApplication.getInstance().getAppConfiguration().isSupportsCustomConnections()) { @@ -494,7 +506,9 @@ public class WebServiceCore implements DBWServiceCore { dataSource.setFolder(config.getFolder() != null ? sessionRegistry.getFolder(config.getFolder()) : null); - WebServiceUtils.setConnectionConfiguration(dataSource.getDriver(), dataSource.getConnectionConfiguration(), config); + WebServiceUtils.setConnectionConfiguration(dataSource.getDriver(), + dataSource.getConnectionConfiguration(), + config); // we should check that the config has changed but not check for password changes dataSource.setSharedCredentials(config.isSharedCredentials()); @@ -543,7 +557,10 @@ public class WebServiceCore implements DBWServiceCore { return connectionInfo; } - private WSDataSourceProperty getDatasourceEventProperty(DataSourceDescriptor oldDataSource, DBPDataSourceContainer dataSource) { + private WSDataSourceProperty getDatasourceEventProperty( + DataSourceDescriptor oldDataSource, + DBPDataSourceContainer dataSource + ) { if (!oldDataSource.equalConfiguration((DataSourceDescriptor) dataSource)) { return WSDataSourceProperty.CONFIGURATION; } @@ -583,8 +600,8 @@ public class WebServiceCore implements DBWServiceCore { @NotNull WebSession webSession, @NotNull String projectId, @NotNull String templateId, - @Nullable String connectionName) throws DBWebException - { + @Nullable String connectionName + ) throws DBWebException { DBPDataSourceRegistry templateRegistry = getProjectById(webSession, projectId).getDataSourceRegistry(); DBPDataSourceContainer dataSourceTemplate = templateRegistry.getDataSource(templateId); if (dataSourceTemplate == null) { @@ -618,7 +635,8 @@ public class WebServiceCore implements DBWServiceCore { @NotNull WebSession webSession, @Nullable String projectId, @NotNull String nodePath, - @NotNull WebConnectionConfig config) throws DBWebException { + @NotNull WebConnectionConfig config + ) throws DBWebException { try { DBNModel navigatorModel = webSession.getNavigatorModel(); DBPDataSourceRegistry dataSourceRegistry = getProjectById(webSession, projectId).getDataSourceRegistry(); @@ -630,7 +648,7 @@ public class WebServiceCore implements DBWServiceCore { if (!(srcNode instanceof DBNDataSource)) { throw new DBException("Node '" + nodePath + "' is not a datasource node"); } - DBPDataSourceContainer dataSourceTemplate = ((DBNDataSource)srcNode).getDataSourceContainer(); + DBPDataSourceContainer dataSourceTemplate = ((DBNDataSource) srcNode).getDataSourceContainer(); DBPDataSourceContainer newDataSource = dataSourceRegistry.createDataSource(dataSourceTemplate); @@ -718,7 +736,9 @@ public class WebServiceCore implements DBWServiceCore { testDataSource = (DataSourceDescriptor) WebServiceUtils.createConnectionFromConfig(connectionConfig, sessionRegistry); } - webSession.provideAuthParameters(webSession.getProgressMonitor(), testDataSource, testDataSource.getConnectionConfiguration()); + webSession.provideAuthParameters(webSession.getProgressMonitor(), + testDataSource, + testDataSource.getConnectionConfiguration()); testDataSource.setSavePassword(true); // We need for test to avoid password callback if (DataSourceDescriptor.class.isAssignableFrom(testDataSource.getClass())) { ((DataSourceDescriptor) testDataSource).setAccessCheckRequired(!webSession.hasPermission(DBWConstants.PERMISSION_ADMIN)); @@ -742,21 +762,26 @@ public class WebServiceCore implements DBWServiceCore { } @Override - public WebNetworkEndpointInfo testNetworkHandler(@NotNull WebSession webSession, @NotNull WebNetworkHandlerConfigInput nhConfig) throws DBWebException { + public WebNetworkEndpointInfo testNetworkHandler( + @NotNull WebSession webSession, + @NotNull WebNetworkHandlerConfigInput nhConfig + ) throws DBWebException { DBRProgressMonitor monitor = webSession.getProgressMonitor(); monitor.beginTask("Instantiate SSH tunnel", 2); - NetworkHandlerDescriptor handlerDescriptor = NetworkHandlerRegistry.getInstance().getDescriptor(nhConfig.getId()); + NetworkHandlerDescriptor handlerDescriptor = NetworkHandlerRegistry.getInstance() + .getDescriptor(nhConfig.getId()); if (handlerDescriptor == null) { throw new DBWebException("Network handler '" + nhConfig.getId() + "' not found"); } try { DBWNetworkHandler handler = handlerDescriptor.createHandler(DBWNetworkHandler.class); if (handler instanceof DBWTunnel) { - DBWTunnel tunnel = (DBWTunnel)handler; + DBWTunnel tunnel = (DBWTunnel) handler; DBPConnectionConfiguration connectionConfig = new DBPConnectionConfiguration(); connectionConfig.setHostName(DBConstants.HOST_LOCALHOST); - connectionConfig.setHostPort(CommonUtils.toString(nhConfig.getProperties().get(DBWHandlerConfiguration.PROP_PORT))); + connectionConfig.setHostPort(CommonUtils.toString(nhConfig.getProperties() + .get(DBWHandlerConfiguration.PROP_PORT))); try { monitor.subTask("Initialize tunnel"); @@ -860,7 +885,9 @@ public class WebServiceCore implements DBWServiceCore { } WebProjectImpl project = getProjectById(session, projectId); DBPDataSourceRegistry sessionRegistry = project.getDataSourceRegistry(); - DBPDataSourceFolder newFolder = WebConnectionFolderUtils.createFolder(parentNode, folderName, sessionRegistry); + DBPDataSourceFolder newFolder = WebConnectionFolderUtils.createFolder(parentNode, + folderName, + sessionRegistry); WebConnectionFolderInfo folderInfo = new WebConnectionFolderInfo(session, newFolder); WebServiceUtils.updateConfigAndRefreshDatabases(session, projectId); WebEventUtils.addNavigatorNodeUpdatedEvent( @@ -936,7 +963,7 @@ public class WebServiceCore implements DBWServiceCore { WebSession webSession, @Nullable String projectId, String id, DBNBrowseSettings settings ) throws DBWebException { WebConnectionInfo connectionInfo = webSession.getWebConnectionInfo(projectId, id); - DataSourceDescriptor dataSourceDescriptor = ((DataSourceDescriptor)connectionInfo.getDataSourceContainer()); + DataSourceDescriptor dataSourceDescriptor = ((DataSourceDescriptor) connectionInfo.getDataSourceContainer()); dataSourceDescriptor.setNavigatorSettings(settings); dataSourceDescriptor.persistConfiguration(); WebEventUtils.addDataSourceUpdatedEvent( @@ -949,7 +976,8 @@ public class WebServiceCore implements DBWServiceCore { } @Override - public WebAsyncTaskInfo getAsyncTaskInfo(WebSession webSession, String taskId, Boolean removeOnFinish) throws DBWebException { + public WebAsyncTaskInfo getAsyncTaskInfo(WebSession webSession, String taskId, Boolean removeOnFinish) + throws DBWebException { return webSession.asyncTaskStatus(taskId, CommonUtils.toBoolean(removeOnFinish)); } @@ -958,6 +986,11 @@ public class WebServiceCore implements DBWServiceCore { return webSession.asyncTaskCancel(taskId); } + @Override + public WebProductSettings getProductSettings(@NotNull WebSession webSession) { + return new WebProductSettings(webSession, ProductSettingsRegistry.getInstance().getSettings()); + } + private WebProjectImpl getProjectById(WebSession webSession, String projectId) throws DBWebException { WebProjectImpl project = webSession.getProjectById(projectId); if (project == null) { diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java index f91dd573b1..469dc1ef4a 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/service/sql/WebSQLCompletionContext.java @@ -16,11 +16,13 @@ */ package io.cloudbeaver.service.sql; +import io.cloudbeaver.server.CBPlatform; import org.jkiss.code.NotNull; import org.jkiss.code.Nullable; import org.jkiss.dbeaver.Log; import org.jkiss.dbeaver.model.*; import org.jkiss.dbeaver.model.exec.DBCExecutionContext; +import org.jkiss.dbeaver.model.sql.SQLModelPreferences; import org.jkiss.dbeaver.model.sql.SQLSyntaxManager; import org.jkiss.dbeaver.model.sql.completion.SQLCompletionContext; import org.jkiss.dbeaver.model.sql.completion.SQLCompletionProposalBase; @@ -66,7 +68,9 @@ public class WebSQLCompletionContext implements SQLCompletionContext { @Override public boolean isUseFQNames() { - return false; + return CBPlatform.getInstance() + .getPreferenceStore() + .getBoolean(SQLModelPreferences.SQL_EDITOR_PROPOSAL_ALWAYS_FQ); } @Override diff --git a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls index 06085838cf..74e2319cb0 100644 --- a/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls +++ b/server/bundles/io.cloudbeaver.service.auth/schema/service.auth.graphqls @@ -163,9 +163,12 @@ extend type Query { } + extend type Mutation { # Set user config parameter. If parameter value is null then removes the parameter setUserConfigurationParameter(name: String!, value: Object): Boolean! + # Updates user preferences + setUserPreferences(preferences: Object!): UserInfo! @since(version: "24.0.1") } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java index c0c9c22c24..54c1e0efab 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/DBWServiceAuth.java @@ -69,4 +69,10 @@ public interface DBWServiceAuth extends DBWService { @NotNull String name, @Nullable String value) throws DBWebException; + @WebAction() + WebUserInfo setUserConfigurationParameters( + @NotNull WebSession webSession, + @NotNull Map parameters + ) throws DBWebException; + } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java index 9ecc77b00a..c480e48380 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/WebServiceBindingAuth.java @@ -73,6 +73,9 @@ public class WebServiceBindingAuth extends WebServiceBindingBase env -> getService(env).setUserConfigurationParameter(getWebSession(env), env.getArgument("name"), env.getArgument("value"))) + .dataFetcher("setUserPreferences", + env -> getService(env).setUserConfigurationParameters(getWebSession(env), + env.getArgument("preferences"))) ; } } diff --git a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java index f590be3c38..e38962a1f9 100644 --- a/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java +++ b/server/bundles/io.cloudbeaver.service.auth/src/io/cloudbeaver/service/auth/impl/WebServiceAuthImpl.java @@ -18,7 +18,6 @@ package io.cloudbeaver.service.auth.impl; import io.cloudbeaver.DBWebException; import io.cloudbeaver.WebServiceUtils; -import io.cloudbeaver.auth.SMAuthProviderFederated; import io.cloudbeaver.auth.SMSignOutLinkProvider; import io.cloudbeaver.auth.provider.local.LocalAuthProvider; import io.cloudbeaver.model.WebPropertyInfo; @@ -249,14 +248,40 @@ public class WebServiceAuthImpl implements DBWServiceAuth { @NotNull WebSession webSession, @NotNull String name, @Nullable String value + ) throws DBWebException { + if (webSession.getUser() == null) { + throw new DBWebException("Preferences cannot be changed for anonymous user"); + } + return setPreference(webSession, name, value); + } + + private static boolean setPreference( + @NotNull WebSession webSession, + @NotNull String name, + @Nullable Object value ) throws DBWebException { webSession.addInfoMessage("Set user parameter - " + name); try { - webSession.getSecurityController().setCurrentUserParameter(name, value); + String serializedValue = value == null ? null : value.toString(); + webSession.getSecurityController().setCurrentUserParameter(name, serializedValue); return true; } catch (DBException e) { throw new DBWebException("Error setting user parameter", e); } } + @Override + public WebUserInfo setUserConfigurationParameters( + @NotNull WebSession webSession, + @NotNull Map parameters + ) throws DBWebException { + if (webSession.getUser() == null) { + throw new DBWebException("Preferences cannot be changed for anonymous user"); + } + for (Map.Entry parameter : parameters.entrySet()) { + setPreference(webSession, parameter.getKey(), parameter.getValue()); + } + return new WebUserInfo(webSession, webSession.getUser()); + } + } diff --git a/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf b/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf index c74507b25a..6e12420206 100644 --- a/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf +++ b/server/test/io.cloudbeaver.test.platform/workspace/conf/cloudbeaver.conf @@ -10,7 +10,7 @@ rootURI: "/", serviceURI: "/api/", - productConfiguration: "workspace/conf/product.conf", + productSettings: {}, expireSessionAfterPeriod: 1800000, diff --git a/server/test/io.cloudbeaver.test.platform/workspace/conf/product.conf b/server/test/io.cloudbeaver.test.platform/workspace/conf/product.conf deleted file mode 100644 index f74c459cb0..0000000000 --- a/server/test/io.cloudbeaver.test.platform/workspace/conf/product.conf +++ /dev/null @@ -1,39 +0,0 @@ -// Product configuration. Customized web application behavior -// It is in JSONC format -{ - // Global properties - core: { - // User defaults - user: { - defaultTheme: "light", - defaultLanguage: "en" - }, - app: { - // Log viewer config - logViewer: { - refreshTimeout: 3000, - logBatchSize: 1000, - maxLogRecords: 2000, - maxFailedRequests: 3 - }, - sqlEditor: { - # max size of the file that can be uploaded to the editor (in kilobytes) - maxFileSize: 100 - } - } - }, - // Notifications config - core_events: { - notificationsPool: 20 - }, - plugin_data_spreadsheet_new: { - hidden: false - }, - plugin_data_export: { - disabled: false - }, - // ERD config - plugin_erd_viewer: { - maxColumnsToDisplay: 7500 - } -} diff --git a/webapp/packages/core-administration/package.json b/webapp/packages/core-administration/package.json index 93a07d8f77..b485188e9d 100644 --- a/webapp/packages/core-administration/package.json +++ b/webapp/packages/core-administration/package.json @@ -27,7 +27,7 @@ "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-routing": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", - "@cloudbeaver/core-settings": "~0.1.0", + "@cloudbeaver/core-storage": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "mobx": "^6.12.0" }, diff --git a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts index 5260f4e949..09c8843256 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts @@ -12,7 +12,7 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { Executor, IExecutor } from '@cloudbeaver/core-executor'; import { EAdminPermission, PermissionsService, ServerConfigResource, SessionPermissionsResource } from '@cloudbeaver/core-root'; import { RouterState, ScreenService } from '@cloudbeaver/core-routing'; -import { SettingsService } from '@cloudbeaver/core-settings'; +import { StorageService } from '@cloudbeaver/core-storage'; import { GlobalConstants } from '@cloudbeaver/core-utils'; import { AdministrationItemService } from '../AdministrationItem/AdministrationItemService'; @@ -69,7 +69,7 @@ export class AdministrationScreenService { private readonly permissionsService: PermissionsService, private readonly screenService: ScreenService, private readonly administrationItemService: AdministrationItemService, - private readonly settingsService: SettingsService, + private readonly storageService: StorageService, private readonly serverConfigResource: ServerConfigResource, private readonly notificationService: NotificationService, ) { @@ -84,8 +84,8 @@ export class AdministrationScreenService { activeScreen: computed, }); - this.settingsService.registerSettings(ADMINISTRATION_ITEMS_STATE, this.itemState, () => new Map()); - this.settingsService.registerSettings(ADMINISTRATION_INFO, this.info, getDefaultAdministrationScreenInfo); + this.storageService.registerSettings(ADMINISTRATION_ITEMS_STATE, this.itemState, () => new Map()); + this.storageService.registerSettings(ADMINISTRATION_INFO, this.info, getDefaultAdministrationScreenInfo); this.permissionsResource.onDataUpdate.addPostHandler(() => { this.checkPermissions(this.screenService.routerService.state); }); diff --git a/webapp/packages/core-administration/tsconfig.json b/webapp/packages/core-administration/tsconfig.json index 8e1c77361d..ea42ca1036 100644 --- a/webapp/packages/core-administration/tsconfig.json +++ b/webapp/packages/core-administration/tsconfig.json @@ -34,7 +34,7 @@ "path": "../core-sdk/tsconfig.json" }, { - "path": "../core-settings/tsconfig.json" + "path": "../core-storage/tsconfig.json" }, { "path": "../core-utils/tsconfig.json" diff --git a/webapp/packages/core-authentication/package.json b/webapp/packages/core-authentication/package.json index 20f6f4a548..f3c2711625 100644 --- a/webapp/packages/core-authentication/package.json +++ b/webapp/packages/core-authentication/package.json @@ -22,29 +22,23 @@ "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-executor": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-routing": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", - "@cloudbeaver/core-theming": "~0.1.0", + "@cloudbeaver/core-storage": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "mobx": "^6.12.0" }, "peerDependencies": {}, "devDependencies": { - "@cloudbeaver/core-browser": "~0.1.0", "@cloudbeaver/core-client-activity": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-product": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-routing": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", - "@cloudbeaver/core-theming": "~0.1.0", "@cloudbeaver/tests-runner": "~0.1.0", "@testing-library/jest-dom": "^6.4.2", "@types/jest": "^29.5.12", diff --git a/webapp/packages/core-authentication/src/AUTH_SETTINGS_GROUP.ts b/webapp/packages/core-authentication/src/AUTH_SETTINGS_GROUP.ts index bc35d8aa03..5c0aaee17b 100644 --- a/webapp/packages/core-authentication/src/AUTH_SETTINGS_GROUP.ts +++ b/webapp/packages/core-authentication/src/AUTH_SETTINGS_GROUP.ts @@ -5,6 +5,6 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-plugin'; +import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-settings'; export const AUTH_SETTINGS_GROUP = ROOT_SETTINGS_GROUP.createSubGroup('core_authentication_auth_settings_group'); diff --git a/webapp/packages/core-authentication/src/AuthInfoService.ts b/webapp/packages/core-authentication/src/AuthInfoService.ts index ca8b6c48c2..3dd0d707ed 100644 --- a/webapp/packages/core-authentication/src/AuthInfoService.ts +++ b/webapp/packages/core-authentication/src/AuthInfoService.ts @@ -37,10 +37,6 @@ export class AuthInfoService { ); } - async logout(): Promise { - await this.userInfoResource.logout(); - } - private federatedAuthentication( providerId: string, options: ILoginOptions, diff --git a/webapp/packages/core-authentication/src/AuthSettingsService.test.ts b/webapp/packages/core-authentication/src/AuthSettingsService.test.ts index 2a8f059912..31c74d25c6 100644 --- a/webapp/packages/core-authentication/src/AuthSettingsService.test.ts +++ b/webapp/packages/core-authentication/src/AuthSettingsService.test.ts @@ -7,53 +7,38 @@ */ import '@testing-library/jest-dom'; -import { coreBrowserManifest } from '@cloudbeaver/core-browser'; import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; -import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; +import '@cloudbeaver/core-root/dist/__custom_mocks__/expectWebsocketClosedMessage'; import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; import { coreRoutingManifest } from '@cloudbeaver/core-routing'; import { coreSDKManifest } from '@cloudbeaver/core-sdk'; import { coreSettingsManifest } from '@cloudbeaver/core-settings'; -import { coreThemingManifest } from '@cloudbeaver/core-theming'; +import { expectNoDeprecatedSettingMessage } from '@cloudbeaver/core-settings/dist/__custom_mocks__/expectDeprecatedSettingMessage'; import { createApp } from '@cloudbeaver/tests-runner'; import { mockAuthentication } from './__custom_mocks__/mockAuthentication'; -import { AuthSettings, AuthSettingsService } from './AuthSettingsService'; +import { AuthSettingsService } from './AuthSettingsService'; import { coreAuthenticationManifest } from './manifest'; const endpoint = createGQLEndpoint(); +const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); const app = createApp( coreAuthenticationManifest, - coreEventsManifest, - corePluginManifest, - coreProductManifest, coreRootManifest, coreSDKManifest, coreSettingsManifest, - coreBrowserManifest, coreRoutingManifest, - coreThemingManifest, coreLocalizationManifest, coreClientActivityManifest, ); -const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); - -beforeAll(() => app.init()); - const equalConfig = { - core: { - authentication: { - disableAnonymousAccess: true, - } as AuthSettings, - }, + 'core.authentication.disableAnonymousAccess': true, }; test('Read settings', async () => { @@ -64,5 +49,6 @@ test('Read settings', async () => { await config.refresh(); - expect(settings.settings.getValue('disableAnonymousAccess')).toBe(true); + expect(settings.disableAnonymousAccess).toBe(true); + expectNoDeprecatedSettingMessage(); }); diff --git a/webapp/packages/core-authentication/src/AuthSettingsService.ts b/webapp/packages/core-authentication/src/AuthSettingsService.ts index 5b819b2751..eab876a6cf 100644 --- a/webapp/packages/core-authentication/src/AuthSettingsService.ts +++ b/webapp/packages/core-authentication/src/AuthSettingsService.ts @@ -6,21 +6,27 @@ * you may not use this file except in compliance with the License. */ import { injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; -import { schema } from '@cloudbeaver/core-utils'; +import { SettingsManagerService, SettingsProvider, SettingsProviderService } from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; const settingsSchema = schema.object({ - disableAnonymousAccess: schema.coerce.boolean().default(false), + 'core.authentication.disableAnonymousAccess': schemaExtra.stringedBoolean().default(false), }); export type AuthSettings = schema.infer; @injectable() export class AuthSettingsService { - readonly settings: PluginSettings; + get disableAnonymousAccess(): boolean { + return this.settings.getValue('core.authentication.disableAnonymousAccess'); + } + readonly settings: SettingsProvider; - constructor(private readonly pluginManagerService: PluginManagerService, private readonly settingsManagerService: SettingsManagerService) { - this.settings = this.pluginManagerService.createSettings('authentication', 'core', settingsSchema); + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + ) { + this.settings = this.settingsProviderService.createSettings(settingsSchema); this.registerSettings(); } diff --git a/webapp/packages/core-authentication/src/UserConfigurationBootstrap.ts b/webapp/packages/core-authentication/src/UserConfigurationBootstrap.ts index 7223001c4f..d0ce9a3410 100644 --- a/webapp/packages/core-authentication/src/UserConfigurationBootstrap.ts +++ b/webapp/packages/core-authentication/src/UserConfigurationBootstrap.ts @@ -6,58 +6,15 @@ * 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'; -import { SessionResource } from '@cloudbeaver/core-root'; -import { ThemeService } from '@cloudbeaver/core-theming'; import { UserInfoResource } from './UserInfoResource'; -const USER_APP_THEME = 'app.theme'; -const USER_APP_LANGUAGE = 'app.language'; - @injectable() export class UserConfigurationBootstrap extends Bootstrap { - constructor( - private readonly userInfoResource: UserInfoResource, - private readonly themeService: ThemeService, - private readonly localizationService: LocalizationService, - private readonly sessionResource: SessionResource, - ) { + constructor(private readonly userInfoResource: UserInfoResource) { super(); - this.userInfoResource.onDataUpdate.addHandler(() => { - const theme = this.userInfoResource.getConfigurationParameter(USER_APP_THEME); - - if (typeof theme === 'string') { - this.themeService.setTheme(theme); - } - - const language = this.userInfoResource.getConfigurationParameter(USER_APP_LANGUAGE); - - if (typeof language === 'string') { - this.localizationService.setLocale(language); - this.sessionResource.changeLanguage(language); - } - }); - - this.themeService.onChange.addHandler(async theme => { - const currentTheme = this.userInfoResource.getConfigurationParameter(USER_APP_THEME); - - if (currentTheme !== theme.id) { - await this.userInfoResource.setConfigurationParameter(USER_APP_THEME, theme.id); - } - }); - - this.localizationService.onChange.addHandler(async locale => { - const currentLocale = this.userInfoResource.getConfigurationParameter(USER_APP_LANGUAGE); - - if (currentLocale !== locale) { - await this.userInfoResource.setConfigurationParameter(USER_APP_LANGUAGE, locale); - } - }); } - register(): void {} - async load(): Promise { await this.userInfoResource.load(); } diff --git a/webapp/packages/core-authentication/src/UserDataService.ts b/webapp/packages/core-authentication/src/UserDataService.ts index 317336e35d..fcaa223d89 100644 --- a/webapp/packages/core-authentication/src/UserDataService.ts +++ b/webapp/packages/core-authentication/src/UserDataService.ts @@ -8,7 +8,7 @@ import { makeObservable, observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; -import { SettingsService } from '@cloudbeaver/core-settings'; +import { StorageService } from '@cloudbeaver/core-storage'; import { TempMap } from '@cloudbeaver/core-utils'; import { UserInfoResource } from './UserInfoResource'; @@ -18,7 +18,10 @@ export class UserDataService { private readonly userData: Map>; private readonly tempData: TempMap>; - constructor(private readonly userInfoResource: UserInfoResource, private readonly settingsService: SettingsService) { + constructor( + private readonly userInfoResource: UserInfoResource, + private readonly storageService: StorageService, + ) { this.userData = new Map(); makeObservable(this, { @@ -27,7 +30,7 @@ export class UserDataService { this.tempData = new TempMap(this.userData); - this.settingsService.registerSettings('user_data', this.userData, () => new Map()); + this.storageService.registerSettings('user_data', this.userData, () => new Map()); } getUserData>(key: string, defaultValue: () => T, validate?: (data: T) => boolean): T { diff --git a/webapp/packages/core-authentication/src/UserInfoResource.ts b/webapp/packages/core-authentication/src/UserInfoResource.ts index a82a506560..472cbe2300 100644 --- a/webapp/packages/core-authentication/src/UserInfoResource.ts +++ b/webapp/packages/core-authentication/src/UserInfoResource.ts @@ -166,6 +166,20 @@ export class UserInfoResource extends CachedDataResource): Promise { + await this.performUpdate(undefined, [], async () => { + const { user } = await this.graphQLService.sdk.updateUserPreferences({ + preferences, + ...this.getDefaultIncludes(), + ...this.getIncludesMap(), + }); + + this.setData(user as UserInfo | null); + }); + + return this.data; + } + async setConfigurationParameter(key: string, value: any): Promise { await this.load(); @@ -173,7 +187,7 @@ export class UserInfoResource extends CachedDataResource { + await this.performUpdate(undefined, [], async () => { await this.graphQLService.sdk.setUserConfigurationParameter({ name: key, value, @@ -189,6 +203,15 @@ export class UserInfoResource extends CachedDataResource { + await this.performUpdate(undefined, [], async () => { + await this.graphQLService.sdk.authChangeLocalPassword({ + oldPassword: this.authProviderService.hashValue(oldPassword), + newPassword: this.authProviderService.hashValue(newPassword), + }); + }); + } + async deleteConfigurationParameter(key: ResourceKeySimple): Promise { await this.load(); @@ -197,7 +220,7 @@ export class UserInfoResource extends CachedDataResource { + await this.performUpdate(undefined, [], async () => { await ResourceKeyUtils.forEachAsync(key, async name => { await this.graphQLService.sdk.setUserConfigurationParameter({ name, diff --git a/webapp/packages/core-authentication/src/UsersResource.ts b/webapp/packages/core-authentication/src/UsersResource.ts index 143495710d..221bec3190 100644 --- a/webapp/packages/core-authentication/src/UsersResource.ts +++ b/webapp/packages/core-authentication/src/UsersResource.ts @@ -217,13 +217,6 @@ export class UsersResource extends CachedMapResource { - await this.graphQLService.sdk.authChangeLocalPassword({ - oldPassword: this.authProviderService.hashValue(oldPassword), - newPassword: this.authProviderService.hashValue(newPassword), - }); - } - async delete(key: ResourceKeySimple): Promise { await ResourceKeyUtils.forEachAsync(key, async key => { if (this.isActiveUser(key)) { diff --git a/webapp/packages/core-authentication/tsconfig.json b/webapp/packages/core-authentication/tsconfig.json index 9b1bd78411..7a9ba996d5 100644 --- a/webapp/packages/core-authentication/tsconfig.json +++ b/webapp/packages/core-authentication/tsconfig.json @@ -11,9 +11,6 @@ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "references": [ - { - "path": "../core-browser/tsconfig.json" - }, { "path": "../core-client-activity/tsconfig.json" }, @@ -23,9 +20,6 @@ { "path": "../core-di/tsconfig.json" }, - { - "path": "../core-events/tsconfig.json" - }, { "path": "../core-executor/tsconfig.json" }, @@ -35,15 +29,6 @@ { "path": "../core-localization/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-product/tsconfig.json" - }, { "path": "../core-resource/tsconfig.json" }, @@ -72,10 +57,7 @@ "path": "../core-settings/tsconfig.json" }, { - "path": "../core-theming/tsconfig.json" - }, - { - "path": "../core-theming/tsconfig.json" + "path": "../core-storage/tsconfig.json" }, { "path": "../core-utils/tsconfig.json" diff --git a/webapp/packages/core-blocks/package.json b/webapp/packages/core-blocks/package.json index 2117f7d4d1..6fcb28f243 100644 --- a/webapp/packages/core-blocks/package.json +++ b/webapp/packages/core-blocks/package.json @@ -39,14 +39,8 @@ }, "peerDependencies": {}, "devDependencies": { - "@cloudbeaver/core-browser": "~0.1.0", - "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-product": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", - "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-theming": "~0.1.0", "@cloudbeaver/tests-runner": "~0.1.0", diff --git a/webapp/packages/core-blocks/src/AppRefreshButton.tsx b/webapp/packages/core-blocks/src/AppRefreshButton.tsx index 2482df6765..02c27d8809 100644 --- a/webapp/packages/core-blocks/src/AppRefreshButton.tsx +++ b/webapp/packages/core-blocks/src/AppRefreshButton.tsx @@ -20,7 +20,7 @@ export const AppRefreshButton: React.FC = function AppRefreshButton({ cl const app = useService(App); function refresh() { - app.start(); + app.restart(); } return ( diff --git a/webapp/packages/core-blocks/src/Containers/Group.m.css b/webapp/packages/core-blocks/src/Containers/Group.m.css index c2f128d261..c5f59ff949 100644 --- a/webapp/packages/core-blocks/src/Containers/Group.m.css +++ b/webapp/packages/core-blocks/src/Containers/Group.m.css @@ -18,8 +18,18 @@ padding: 24px; border-radius: var(--theme-group-element-radius); + > .groupTitle { + padding: 24px 0; + margin: -24px 0; + } + &.dense { padding: 8px; + + > .groupTitle { + padding: 8px 0; + margin: -8px 0; + } } &.form > * { @@ -32,6 +42,7 @@ > .groupTitle { padding: 24px; + margin: 0; } } diff --git a/webapp/packages/core-blocks/src/Containers/GroupTitle.m.css b/webapp/packages/core-blocks/src/Containers/GroupTitle.m.css index 2d17c69d79..6535b7892d 100644 --- a/webapp/packages/core-blocks/src/Containers/GroupTitle.m.css +++ b/webapp/packages/core-blocks/src/Containers/GroupTitle.m.css @@ -5,4 +5,12 @@ margin: 0; text-transform: uppercase; opacity: 0.9; + background-color: inherit; + + &.sticky { + position: sticky; + top: 0; + z-index: 1; + opacity: 1; + } } diff --git a/webapp/packages/core-blocks/src/Containers/GroupTitle.tsx b/webapp/packages/core-blocks/src/Containers/GroupTitle.tsx index 0db588fb75..d0acbe9ead 100644 --- a/webapp/packages/core-blocks/src/Containers/GroupTitle.tsx +++ b/webapp/packages/core-blocks/src/Containers/GroupTitle.tsx @@ -12,9 +12,17 @@ import styles from './GroupTitle.m.css'; import type { ILayoutSizeProps } from './ILayoutSizeProps'; import elementsSizeStyles from './shared/ElementsSize.m.css'; -export const GroupTitle: React.FC> = function GroupTitle({ className, ...rest }) { +interface Props { + sticky?: boolean; +} + +export const GroupTitle: React.FC> = function GroupTitle({ + sticky, + className, + ...rest +}) { const style = useS(styles, elementsSizeStyles); const divProps = filterLayoutFakeProps(rest); const layoutProps = getLayoutProps(rest); - return

; + return

; }; diff --git a/webapp/packages/core-blocks/src/ErrorMessage.test.tsx b/webapp/packages/core-blocks/src/ErrorMessage.test.tsx index c6590c4480..3fa58e3665 100644 --- a/webapp/packages/core-blocks/src/ErrorMessage.test.tsx +++ b/webapp/packages/core-blocks/src/ErrorMessage.test.tsx @@ -8,40 +8,15 @@ import '@testing-library/jest-dom'; import { screen } from '@testing-library/react'; -import { coreBrowserManifest } from '@cloudbeaver/core-browser'; -import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; -import { coreRootManifest } from '@cloudbeaver/core-root'; -import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; -import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; -import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; -import { coreSDKManifest } from '@cloudbeaver/core-sdk'; import { coreSettingsManifest } from '@cloudbeaver/core-settings'; import { coreThemingManifest } from '@cloudbeaver/core-theming'; import { createApp, renderInApp } from '@cloudbeaver/tests-runner'; import { ErrorMessage } from './ErrorMessage'; -const endpoint = createGQLEndpoint(); -const app = createApp( - coreEventsManifest, - corePluginManifest, - coreProductManifest, - coreRootManifest, - coreSDKManifest, - coreSettingsManifest, - coreBrowserManifest, - coreThemingManifest, - coreLocalizationManifest, - coreClientActivityManifest, -); - -mockGraphQL(...mockAppInit(endpoint)); - -beforeAll(() => app.init()); +const app = createApp(coreEventsManifest, coreSettingsManifest, coreThemingManifest, coreLocalizationManifest); test('icons.svg#name', () => { renderInApp(, app); diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/useCheckboxState.ts b/webapp/packages/core-blocks/src/FormControls/Checkboxes/useCheckboxState.ts index c0932e862a..45ce2005ed 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/useCheckboxState.ts +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/useCheckboxState.ts @@ -5,6 +5,7 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { runInAction } from 'mobx'; import { useContext, useState } from 'react'; import { useObjectRef } from '../../useObjectRef'; @@ -65,36 +66,38 @@ export function useCheckboxState(options: CheckboxStateOpti () => ({ checked, change(event: React.ChangeEvent) { - const { inverse, state, name, value, onChange, count, context } = optionsRef; - let checked = event.target.checked; + runInAction(() => { + const { inverse, state, name, value, onChange, count, context } = optionsRef; + let checked = event.target.checked; - if (inverse) { - checked = !checked; - } + if (inverse) { + checked = !checked; + } - if (state !== undefined && name !== undefined) { - const currentState = state[name as TKey]; + if (state !== undefined && name !== undefined) { + const currentState = state[name as TKey]; - if (typeof value === 'string') { - if (Array.isArray(currentState)) { - const elementIndex = currentState.indexOf(value); - if (checked && elementIndex === -1) { - currentState.push(value); - } else if (elementIndex !== -1) { - currentState.splice(elementIndex, 1); + if (typeof value === 'string') { + if (Array.isArray(currentState)) { + const elementIndex = currentState.indexOf(value); + if (checked && elementIndex === -1) { + currentState.push(value); + } else if (elementIndex !== -1) { + currentState.splice(elementIndex, 1); + } + } else { + state[name as TKey] = value; } } else { - state[name as TKey] = value; + state[name as TKey] = checked; } - } else { - state[name as TKey] = checked; } - } - onChange?.(checked, name as TKey); - context?.change(checked, name); + onChange?.(checked, name as TKey); + context?.change(checked, name); - refresh(count + 1); + refresh(count + 1); + }); }, }), { checked }, diff --git a/webapp/packages/core-blocks/src/Loader/useAutoLoad.ts b/webapp/packages/core-blocks/src/Loader/useAutoLoad.ts index 7c0afa549e..fcebe417c0 100644 --- a/webapp/packages/core-blocks/src/Loader/useAutoLoad.ts +++ b/webapp/packages/core-blocks/src/Loader/useAutoLoad.ts @@ -11,13 +11,13 @@ import type { ILoadableState } from '@cloudbeaver/core-utils'; import { getComputed } from '../getComputed'; -export function useAutoLoad(component: { name: string }, state: ILoadableState | ILoadableState[], enabled = true, lazy = false) { +export function useAutoLoad(component: { name: string }, state: ILoadableState | ReadonlyArray, enabled = true, lazy = false) { const [loadFunctionName] = useState(`${component.name}.useAutoLoad(...)` as const); if (!Array.isArray(state)) { - state = [state]; + state = [state] as ReadonlyArray; } - for (const loader of state as ILoadableState[]) { + for (const loader of state as ReadonlyArray) { getComputed( // activate mobx subscriptions () => (!loader.isLoaded() || loader.isOutdated?.() === true) && !loader.isError(), @@ -30,7 +30,7 @@ export function useAutoLoad(component: { name: string }, state: ILoadableState | return; } - for (const loader of state as ILoadableState[]) { + for (const loader of state as ReadonlyArray) { if (loader.isError() || (loader.lazy === true && !lazy)) { continue; } diff --git a/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx index 0f18fe4aae..20f73297ca 100644 --- a/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx +++ b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx @@ -7,7 +7,7 @@ */ import { observer } from 'mobx-react-lite'; -import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; +import { getObjectPropertyType, type ObjectPropertyInfo, type ObjectPropertyType } from '@cloudbeaver/core-sdk'; import { removeMetadataFromDataURL } from '@cloudbeaver/core-utils'; import { FieldCheckbox } from '../../FormControls/Checkboxes/FieldCheckbox'; @@ -19,7 +19,6 @@ import { isControlPresented } from '../../FormControls/isControlPresented'; import { Textarea } from '../../FormControls/Textarea'; import { Link } from '../../Link'; import { useTranslate } from '../../localization/useTranslate'; -import { type ControlType, getPropertyControlType } from './getPropertyControlType'; const RESERVED_KEYWORDS = ['no', 'off', 'new-password']; @@ -39,7 +38,7 @@ interface RenderFieldProps { onFocus?: (event: React.FocusEvent) => void; } -function getValue(value: any, controlType: ControlType) { +function getValue(value: any, controlType: ObjectPropertyType) { const checkbox = controlType === 'checkbox'; if (value === null || value === undefined) { @@ -70,7 +69,7 @@ export const RenderField = observer(function RenderField({ }) { const translate = useTranslate(); - const controlType = getPropertyControlType(property); + const controlType = getObjectPropertyType(property); const password = property.features.includes('password'); const required = property.required && !readOnly; @@ -132,7 +131,7 @@ export const RenderField = observer(function RenderField({ ); } - if (controlType === 'combobox') { + if (controlType === 'selector') { if (state !== undefined) { return ( { const fallback = 'Loading...'; @@ -45,6 +49,7 @@ describe('importLazyComponent', () => { expect(screen.getByText(fallback)).toBeTruthy(); await waitFor(() => expect(screen.getByText(errorText)).toBeTruthy()); + expect(consoleSpy.error).toHaveBeenCalledWith(expect.stringMatching(/The above error occurred in one of your React components.*/)); expect(componentImporter).toHaveBeenCalled(); }); }); diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts index d90556d56d..31f7480ee0 100644 --- a/webapp/packages/core-blocks/src/index.ts +++ b/webapp/packages/core-blocks/src/index.ts @@ -54,7 +54,6 @@ export { MenuTrigger, type MenuState } from './MenuPanel/MenuTrigger'; export * from './ObjectPropertyInfo/ObjectPropertyInfoForm/ObjectPropertyInfoForm'; export * from './ObjectPropertyInfo/useObjectPropertyCategories'; -export * from './ObjectPropertyInfo/ObjectPropertyInfoForm/getPropertyControlType'; export * from './Overlay/Overlay'; export * from './Overlay/OverlayActions'; diff --git a/webapp/packages/core-blocks/src/useS.ts b/webapp/packages/core-blocks/src/useS.ts index 38f9d82a08..7997747e78 100644 --- a/webapp/packages/core-blocks/src/useS.ts +++ b/webapp/packages/core-blocks/src/useS.ts @@ -27,10 +27,10 @@ type ExtractStyles = Intersect< ? U extends BaseStyles ? U : U extends (theme: string) => Promise - ? A extends BaseStyles - ? A + ? A extends BaseStyles + ? A + : never : never - : never : never >; @@ -47,7 +47,7 @@ export function useS(...componentStyles: [...T]): Ex const [patch, forceUpdate] = useState(0); const loadedStyles = useRef([]); const themeService = useService(ThemeService); - const [currentThemeId, setCurrentThemeId] = useState(() => themeService.currentThemeId); + const [currentThemeId, setCurrentThemeId] = useState(() => themeService.themeId); const lastThemeRef = useRef(currentThemeId); // @ts-ignore const filteredStyles = themeService.mapStyles(componentStyles.flat(Infinity).filter(Boolean) as Style[], context); @@ -57,7 +57,7 @@ export function useS(...componentStyles: [...T]): Ex executor: themeService.onChange, handlers: [ function updateThemeId(theme) { - if (currentThemeId !== themeService.currentThemeId && trackTheme) { + if (currentThemeId !== themeService.themeId && trackTheme) { setCurrentThemeId(theme.id); } }, diff --git a/webapp/packages/core-blocks/src/useStyles.ts b/webapp/packages/core-blocks/src/useStyles.ts index a093e543c2..49fcfd0fdf 100644 --- a/webapp/packages/core-blocks/src/useStyles.ts +++ b/webapp/packages/core-blocks/src/useStyles.ts @@ -29,7 +29,7 @@ export function useStyles(...componentStyles: ComponentStyle[]): Record([]); const loadedStyles = useRef([]); const themeService = useService(ThemeService); - const [currentThemeId, setCurrentThemeId] = useState(() => themeService.currentThemeId); + const [currentThemeId, setCurrentThemeId] = useState(() => themeService.themeId); const lastThemeRef = useRef(currentThemeId); //@ts-ignore const filteredStyles = componentStyles.flat(Infinity).filter(Boolean) as Style[]; @@ -39,7 +39,7 @@ export function useStyles(...componentStyles: ComponentStyle[]): Record app.init()); - const testValueA = false; const testValueB = true; const equalConfigA = { - core: { - cookies: { - disabled: testValueA, - }, - browser: { - 'cookies.disabled': testValueA, - } as CookiesSettings, - }, + 'core.cookies.disabled': testValueA, + 'core.browser.cookies.disabled': testValueA, }; const equalConfigB = { - core: { - cookies: { - disabled: testValueB, - }, - }, + 'core.cookies.disabled': testValueB, }; test('New settings override deprecated settings', async () => { @@ -71,7 +59,8 @@ test('New settings override deprecated settings', async () => { await config.refresh(); - expect(settings.settings.getValue('cookies.disabled')).toBe(testValueA); + expect(settings.disabled).toBe(testValueA); + expectNoDeprecatedSettingMessage(); }); test('New settings fall back to deprecated settings', async () => { @@ -82,5 +71,6 @@ test('New settings fall back to deprecated settings', async () => { await config.refresh(); - expect(settings.settings.getValue('cookies.disabled')).toBe(testValueB); + expect(settings.disabled).toBe(testValueB); + expectDeprecatedSettingMessage(); }); diff --git a/webapp/packages/core-browser-settings/src/BrowserSettingsService.ts b/webapp/packages/core-browser-settings/src/BrowserSettingsService.ts index 8cc12a2224..3d1d982566 100644 --- a/webapp/packages/core-browser-settings/src/BrowserSettingsService.ts +++ b/webapp/packages/core-browser-settings/src/BrowserSettingsService.ts @@ -6,32 +6,43 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { createSettingsAliasResolver, PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; -import { ServerSettingsResolverService, ServerSettingsService } from '@cloudbeaver/core-root'; -import { schema } from '@cloudbeaver/core-utils'; +import { + createSettingsAliasResolver, + ROOT_SETTINGS_LAYER, + SettingsManagerService, + SettingsProvider, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; const settingsSchema = schema.object({ - 'cookies.disabled': schema.coerce.boolean().default(false), + 'core.browser.cookies.disabled': schemaExtra.stringedBoolean().default(false), }); export type CookiesSettings = schema.infer; @injectable() export class BrowserSettingsService extends Dependency { - readonly settings: PluginSettings; + get disabled(): boolean { + return this.settings.getValue('core.browser.cookies.disabled'); + } + readonly settings: SettingsProvider; constructor( - private readonly pluginManagerService: PluginManagerService, + private readonly settingsProviderService: SettingsProviderService, private readonly settingsManagerService: SettingsManagerService, - private readonly serverSettingsService: ServerSettingsService, - private readonly serverSettingsResolverService: ServerSettingsResolverService, + private readonly settingsResolverService: SettingsResolverService, ) { super(); - this.settings = this.pluginManagerService.createSettings('browser', 'core', settingsSchema); + this.settings = this.settingsProviderService.createSettings(settingsSchema); - this.serverSettingsResolverService.addResolver( + this.settingsResolverService.addResolver( + ROOT_SETTINGS_LAYER, /** @deprecated Use settings instead, will be removed in 23.0.0 */ - createSettingsAliasResolver(this.serverSettingsService, this.settings, 'core'), + createSettingsAliasResolver(this.settingsResolverService, this.settings, { + 'core.browser.cookies.disabled': 'core.cookies.disabled', + }), ); this.registerSettings(); } diff --git a/webapp/packages/core-browser-settings/tsconfig.json b/webapp/packages/core-browser-settings/tsconfig.json index dec9a7e73a..72eb723f7c 100644 --- a/webapp/packages/core-browser-settings/tsconfig.json +++ b/webapp/packages/core-browser-settings/tsconfig.json @@ -26,18 +26,6 @@ { "path": "../core-localization/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-product/tsconfig.json" - }, - { - "path": "../core-root/tsconfig.json" - }, { "path": "../core-root/tsconfig.json" }, @@ -47,6 +35,9 @@ { "path": "../core-settings/tsconfig.json" }, + { + "path": "../core-settings/tsconfig.json" + }, { "path": "../core-utils/tsconfig.json" }, diff --git a/webapp/packages/core-cli/bin/typescript-resolve-references.js b/webapp/packages/core-cli/bin/typescript-resolve-references.js index 7b418d2432..fdbff046ec 100755 --- a/webapp/packages/core-cli/bin/typescript-resolve-references.js +++ b/webapp/packages/core-cli/bin/typescript-resolve-references.js @@ -26,24 +26,34 @@ typescriptConfig.references = []; // typescriptRootConfig.references = typescriptRootConfig.references || []; for (const dependency of dependencies) { - if (!dependency.startsWith('@cloudbeaver')) { + if (dependency === pkg.name) { + console.error(`Self reference detected: ${dependency}`); continue; } - const dependencyPath = resolve(require.resolve(join(dependency, 'src', 'index.ts'), { paths: nodeModules }), '../../tsconfig.json'); - typescriptConfig.references.push({ - path: upath.relative(currentDir, dependencyPath), - }); - typescriptConfig.references.sort((a, b) => a.path.localeCompare(b.path)); + try { + if (!dependency.startsWith('@cloudbeaver')) { + continue; + } + const dependencyPath = resolve(require.resolve(join(dependency, 'src', 'index.ts'), { paths: nodeModules }), '../../tsconfig.json'); + typescriptConfig.references.push({ + path: upath.relative(currentDir, dependencyPath), + }); - // const relativePath = relative(tsRootPath, dependencyPath); + // const relativePath = relative(tsRootPath, dependencyPath); - // if (!typescriptRootConfig.references.find(ref => ref.path === relativePath)) { - // typescriptRootConfig.references.push({ - // path: relativePath, - // }); - // } + // if (!typescriptRootConfig.references.find(ref => ref.path === relativePath)) { + // typescriptRootConfig.references.push({ + // path: relativePath, + // }); + // } + } catch (e) { + console.error(`Failed to resolve ${dependency}`); + } } +typescriptConfig.references = [...new Set(typescriptConfig.references)]; +typescriptConfig.references.sort((a, b) => a.path.localeCompare(b.path)); + typescriptConfig.compilerOptions = { rootDir: 'src', outDir: 'dist', diff --git a/webapp/packages/core-cli/bin/validate-dependencies.js b/webapp/packages/core-cli/bin/validate-dependencies.js index 3c26a9170b..bc68c847ae 100755 --- a/webapp/packages/core-cli/bin/validate-dependencies.js +++ b/webapp/packages/core-cli/bin/validate-dependencies.js @@ -26,7 +26,7 @@ const devDependencies = new Set(); let isSuccess = true; const sourceFilesIterator = glob.globIterateSync('**/*.{ts,tsx,scss,css}', { cwd: currentPackageSrcPath }); -const importRegex = /(import|export) ((type |)([\w,\s]*?)(\{[\w\s\n,]*?\}|) from |)['"]((@[\w-]*\/[\w-]*)|([^\\.].*?))(\/.*)*['"]/g; +const importRegex = /(import|export) ((type |)([\w,\s*]*?)(\{[\w\s\n,]*?\}|) from |)['"]((@[\w-]*\/[\w-]*)|([^\\.].*?))(\/.*)*['"]/g; const testFileRegex = /((__custom_mocks__|__tests__).*|\.test)\.tsx?$/i; const tsFileRegex = /\.ts$/i; const tsxFileRegex = /\.tsx$/i; diff --git a/webapp/packages/core-cli/configs/webpack.product.dev.config.js b/webapp/packages/core-cli/configs/webpack.product.dev.config.js index cf457018a3..674309cf95 100644 --- a/webapp/packages/core-cli/configs/webpack.product.dev.config.js +++ b/webapp/packages/core-cli/configs/webpack.product.dev.config.js @@ -45,6 +45,9 @@ module.exports = (env, argv) => { index, sso, }, + output: { + devtoolModuleFilenameTemplate: 'file:///[absolute-resource-path]', + }, watchOptions: { aggregateTimeout: 3000, ignored: ['**/node_modules', '**/packages/*/src/**/*.{ts,tsx}'], diff --git a/webapp/packages/core-connections/package.json b/webapp/packages/core-connections/package.json index edab5962fe..e24cb673e1 100644 --- a/webapp/packages/core-connections/package.json +++ b/webapp/packages/core-connections/package.json @@ -27,11 +27,11 @@ "@cloudbeaver/core-extensions": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "mobx": "^6.12.0", "react": "^18.2.0" diff --git a/webapp/packages/core-connections/src/CONNECTIONS_SETTINGS_GROUP.ts b/webapp/packages/core-connections/src/CONNECTIONS_SETTINGS_GROUP.ts index 24b49ab811..1224f98229 100644 --- a/webapp/packages/core-connections/src/CONNECTIONS_SETTINGS_GROUP.ts +++ b/webapp/packages/core-connections/src/CONNECTIONS_SETTINGS_GROUP.ts @@ -5,6 +5,6 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-plugin'; +import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-settings'; export const CONNECTIONS_SETTINGS_GROUP = ROOT_SETTINGS_GROUP.createSubGroup('core_connections_connections_settings_group'); diff --git a/webapp/packages/core-connections/src/ConnectionsSettingsService.ts b/webapp/packages/core-connections/src/ConnectionsSettingsService.ts index 004ad75f87..42610067d7 100644 --- a/webapp/packages/core-connections/src/ConnectionsSettingsService.ts +++ b/webapp/packages/core-connections/src/ConnectionsSettingsService.ts @@ -6,22 +6,28 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; -import { schema } from '@cloudbeaver/core-utils'; +import { SettingsManagerService, SettingsProvider, SettingsProviderService } from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; const settingsSchema = schema.object({ - disabled: schema.coerce.boolean().default(false), + 'core.connections.disabled': schemaExtra.stringedBoolean().default(false), }); export type ConnectionsSettings = schema.infer; @injectable() export class ConnectionsSettingsService extends Dependency { - readonly settings: PluginSettings; + get disabled(): boolean { + return this.settings.getValue('core.connections.disabled'); + } + readonly settings: SettingsProvider; - constructor(private readonly pluginManagerService: PluginManagerService, private readonly settingsManagerService: SettingsManagerService) { + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + ) { super(); - this.settings = this.pluginManagerService.createSettings('connections', 'core', settingsSchema); + this.settings = this.settingsProviderService.createSettings(settingsSchema); this.registerSettings(); } diff --git a/webapp/packages/core-connections/tsconfig.json b/webapp/packages/core-connections/tsconfig.json index 6ec68f135f..3dde8d23ce 100644 --- a/webapp/packages/core-connections/tsconfig.json +++ b/webapp/packages/core-connections/tsconfig.json @@ -36,9 +36,6 @@ { "path": "../core-navigation-tree/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, { "path": "../core-projects/tsconfig.json" }, @@ -51,6 +48,9 @@ { "path": "../core-sdk/tsconfig.json" }, + { + "path": "../core-settings/tsconfig.json" + }, { "path": "../core-utils/tsconfig.json" } diff --git a/webapp/packages/core-di/src/App.ts b/webapp/packages/core-di/src/App.ts index 0882d60e84..85a886ab61 100644 --- a/webapp/packages/core-di/src/App.ts +++ b/webapp/packages/core-di/src/App.ts @@ -10,6 +10,7 @@ import { Executor, IExecutor } from '@cloudbeaver/core-executor'; import { Bootstrap } from './Bootstrap'; import { Dependency } from './Dependency'; import type { DIContainer } from './DIContainer'; +import { Disposable } from './Disposable'; import type { IServiceCollection, IServiceConstructor, IServiceInjector } from './IApp'; import { IDiWrapper, inversifyWrapper } from './inversifyWrapper'; import type { PluginManifest } from './PluginManifest'; @@ -34,6 +35,15 @@ export class App { await this.onStart.execute(); } + async restart(): Promise { + this.dispose(); + await this.start(); + } + + dispose(): void { + this.diWrapper.collection.unbindAll(); + } + getPlugins(): PluginManifest[] { return [...this.plugins]; } @@ -60,8 +70,6 @@ export class App { // first phase register all dependencies private registerServices(): void { - this.diWrapper.collection.unbindAll(); - this.getServiceCollection().addServiceByClass(App, this); for (const service of this.getServices()) { diff --git a/webapp/packages/core-di/src/Bootstrap.ts b/webapp/packages/core-di/src/Bootstrap.ts index b80c7d6e9b..031f0a506e 100644 --- a/webapp/packages/core-di/src/Bootstrap.ts +++ b/webapp/packages/core-di/src/Bootstrap.ts @@ -5,10 +5,11 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { Disposable } from './Disposable'; import { injectable } from './injectable'; @injectable() -export abstract class Bootstrap { +export abstract class Bootstrap extends Disposable { register(): void | Promise {} load(): void | Promise {} } diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/IUserProfileFormState.ts b/webapp/packages/core-di/src/Disposable.ts similarity index 55% rename from webapp/packages/plugin-user-profile/src/UserProfileForm/IUserProfileFormState.ts rename to webapp/packages/core-di/src/Disposable.ts index 7b61d8926f..a4cf862e7f 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/IUserProfileFormState.ts +++ b/webapp/packages/core-di/src/Disposable.ts @@ -5,11 +5,12 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { IFormStateInfo } from '@cloudbeaver/core-ui'; +import { preDestroy } from 'inversify'; -export interface IUserProfileFormState { - info: IFormStateInfo; - validate: () => Promise; +import { injectable } from './injectable'; - dispose: () => void; +@injectable() +export abstract class Disposable { + @preDestroy() + dispose(): void {} } diff --git a/webapp/packages/core-events/package.json b/webapp/packages/core-events/package.json index ea17e3d0f0..55e4240a92 100644 --- a/webapp/packages/core-events/package.json +++ b/webapp/packages/core-events/package.json @@ -20,19 +20,15 @@ "dependencies": { "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-executor": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "mobx": "^6.12.0" }, "peerDependencies": {}, "devDependencies": { - "@cloudbeaver/core-browser": "~0.1.0", "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-product": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", diff --git a/webapp/packages/core-events/src/EventsSettingsService.test.ts b/webapp/packages/core-events/src/EventsSettingsService.test.ts index 667d98c0c1..64e00bdaf6 100644 --- a/webapp/packages/core-events/src/EventsSettingsService.test.ts +++ b/webapp/packages/core-events/src/EventsSettingsService.test.ts @@ -7,55 +7,45 @@ */ import '@testing-library/jest-dom'; -import { coreBrowserManifest } from '@cloudbeaver/core-browser'; import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; +import '@cloudbeaver/core-root/dist/__custom_mocks__/expectWebsocketClosedMessage'; import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; import { coreSDKManifest } from '@cloudbeaver/core-sdk'; import { coreSettingsManifest } from '@cloudbeaver/core-settings'; +import { + expectDeprecatedSettingMessage, + expectNoDeprecatedSettingMessage, +} from '@cloudbeaver/core-settings/dist/__custom_mocks__/expectDeprecatedSettingMessage'; import { createApp } from '@cloudbeaver/tests-runner'; -import { EventsSettings, EventsSettingsService } from './EventsSettingsService'; +import { EventsSettingsService } from './EventsSettingsService'; import { coreEventsManifest } from './manifest'; const endpoint = createGQLEndpoint(); +const server = mockGraphQL(...mockAppInit(endpoint)); const app = createApp( coreEventsManifest, - corePluginManifest, coreSettingsManifest, coreLocalizationManifest, - coreBrowserManifest, - coreProductManifest, coreRootManifest, coreSDKManifest, coreClientActivityManifest, ); -const server = mockGraphQL(...mockAppInit(endpoint)); - -beforeAll(() => app.init()); - const oldConfig = { - core_events: { - notificationsPool: 1, - maxPersistentAllow: 2, - } as EventsSettings, + 'core_events.notificationsPool': '1', + 'core_events.maxPersistentAllow': '2', }; const overrideConfig = { ...oldConfig, - plugin: { - notifications: { - notificationsPool: 3, - maxPersistentAllow: 4, - } as EventsSettings, - }, + 'plugin.notifications.notificationsPool': '3', + 'plugin.notifications.maxPersistentAllow': '4', }; test('New settings override deprecated settings', async () => { @@ -66,8 +56,9 @@ test('New settings override deprecated settings', async () => { await config.refresh(); - expect(settings.settings.getValue('notificationsPool')).toBe(3); - expect(settings.settings.getValue('maxPersistentAllow')).toBe(4); + expect(settings.notificationsPool).toBe(3); + expect(settings.maxPersistentAllow).toBe(4); + expectNoDeprecatedSettingMessage(); }); test('Deprecated settings accessible with new settings', async () => { @@ -78,6 +69,7 @@ test('Deprecated settings accessible with new settings', async () => { await config.refresh(); - expect(settings.settings.getValue('notificationsPool')).toBe(1); - expect(settings.settings.getValue('maxPersistentAllow')).toBe(2); + expect(settings.notificationsPool).toBe(1); + expect(settings.maxPersistentAllow).toBe(2); + expectDeprecatedSettingMessage(); }); diff --git a/webapp/packages/core-events/src/EventsSettingsService.ts b/webapp/packages/core-events/src/EventsSettingsService.ts index f85e4541dd..22f0869920 100644 --- a/webapp/packages/core-events/src/EventsSettingsService.ts +++ b/webapp/packages/core-events/src/EventsSettingsService.ts @@ -6,33 +6,48 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { createSettingsAliasResolver, PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; -import { ServerSettingsResolverService, ServerSettingsService } from '@cloudbeaver/core-root'; +import { + createSettingsAliasResolver, + ROOT_SETTINGS_LAYER, + SettingsManagerService, + SettingsProvider, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; import { schema } from '@cloudbeaver/core-utils'; const settingsSchema = schema.object({ - notificationsPool: schema.coerce.number().default(20), - maxPersistentAllow: schema.coerce.number().default(5), + 'plugin.notifications.notificationsPool': schema.coerce.number().default(20), + 'plugin.notifications.maxPersistentAllow': schema.coerce.number().default(5), }); export type EventsSettings = schema.infer; @injectable() export class EventsSettingsService extends Dependency { - readonly settings: PluginSettings; + get maxPersistentAllow(): number { + return this.settings.getValue('plugin.notifications.maxPersistentAllow'); + } + get notificationsPool(): number { + return this.settings.getValue('plugin.notifications.notificationsPool'); + } + readonly settings: SettingsProvider; constructor( - private readonly pluginManagerService: PluginManagerService, + private readonly settingsProviderService: SettingsProviderService, private readonly settingsManagerService: SettingsManagerService, - private readonly serverSettingsService: ServerSettingsService, - private readonly serverSettingsResolverService: ServerSettingsResolverService, + private readonly settingsResolverService: SettingsResolverService, ) { super(); - this.settings = this.pluginManagerService.createSettings('notifications', 'plugin', settingsSchema); + this.settings = this.settingsProviderService.createSettings(settingsSchema); - this.serverSettingsResolverService.addResolver( + this.settingsResolverService.addResolver( + ROOT_SETTINGS_LAYER, /** @deprecated Use settings instead, will be removed in 23.0.0 */ - createSettingsAliasResolver(this.serverSettingsService, this.settings, 'core_events'), + createSettingsAliasResolver(this.settingsResolverService, this.settings, { + 'plugin.notifications.maxPersistentAllow': 'core_events.maxPersistentAllow', + 'plugin.notifications.notificationsPool': 'core_events.notificationsPool', + }), ); this.registerSettings(); } diff --git a/webapp/packages/core-events/src/NOTIFICATIONS_SETTINGS_GROUP.ts b/webapp/packages/core-events/src/NOTIFICATIONS_SETTINGS_GROUP.ts index c50580dc22..31f5768609 100644 --- a/webapp/packages/core-events/src/NOTIFICATIONS_SETTINGS_GROUP.ts +++ b/webapp/packages/core-events/src/NOTIFICATIONS_SETTINGS_GROUP.ts @@ -5,6 +5,6 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-plugin'; +import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-settings'; export const NOTIFICATIONS_SETTINGS_GROUP = ROOT_SETTINGS_GROUP.createSubGroup('core_events_notifications_settings_group'); diff --git a/webapp/packages/core-events/src/NotificationService.ts b/webapp/packages/core-events/src/NotificationService.ts index 1220ea8409..410f51900f 100644 --- a/webapp/packages/core-events/src/NotificationService.ts +++ b/webapp/packages/core-events/src/NotificationService.ts @@ -52,7 +52,7 @@ export class NotificationService { if (options.persistent) { const persistentNotifications = this.notificationList.values.filter(value => value.persistent); - const maxPersistentAllow = this.settings.settings.getValue('maxPersistentAllow'); + const maxPersistentAllow = this.settings.maxPersistentAllow; if (persistentNotifications.length >= maxPersistentAllow) { throw new Error(`You cannot create more than ${maxPersistentAllow} persistent notification`); @@ -105,7 +105,7 @@ export class NotificationService { const filteredNotificationList = this.notificationList.values.filter(notification => !notification.persistent); - const notificationsPool = this.settings.settings.getValue('notificationsPool'); + const notificationsPool = this.settings.notificationsPool; if (filteredNotificationList.length > notificationsPool) { let i = 0; diff --git a/webapp/packages/core-events/tsconfig.json b/webapp/packages/core-events/tsconfig.json index 7e5ca46969..6093c6b809 100644 --- a/webapp/packages/core-events/tsconfig.json +++ b/webapp/packages/core-events/tsconfig.json @@ -6,9 +6,6 @@ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "references": [ - { - "path": "../core-browser/tsconfig.json" - }, { "path": "../core-client-activity/tsconfig.json" }, @@ -21,18 +18,6 @@ { "path": "../core-localization/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-product/tsconfig.json" - }, - { - "path": "../core-root/tsconfig.json" - }, { "path": "../core-root/tsconfig.json" }, @@ -45,6 +30,9 @@ { "path": "../core-settings/tsconfig.json" }, + { + "path": "../core-settings/tsconfig.json" + }, { "path": "../core-utils/tsconfig.json" }, diff --git a/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts b/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts index b1d7dcf6d2..8427727824 100644 --- a/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts +++ b/webapp/packages/core-executor/src/IExecutorHandlersCollection.ts @@ -32,9 +32,9 @@ export interface IExecutorHandlersCollection(context: ISyncContextLoader, creator: ISyncContextLoader): this; addContextCreator(context: IAsyncContextLoader, creator: IAsyncContextLoader): this; - before: (executor: IExecutorHandlersCollection, map?: ExecutorDataMap) => this; + before: (executor: IExecutorHandlersCollection, map?: ExecutorDataMap, filter?: ExecutorDataFilter) => this; removeBefore: (executor: IExecutorHandlersCollection) => void; - next: (executor: IExecutorHandlersCollection, map?: ExecutorDataMap) => this; + next: (executor: IExecutorHandlersCollection, map?: ExecutorDataMap, filter?: ExecutorDataFilter) => this; removeNext: (executor: IExecutorHandlersCollection) => void; addCollection: (collection: IExecutorHandlersCollection) => this; hasHandler: (handler: IExecutorHandler) => boolean; diff --git a/webapp/packages/core-localization/package.json b/webapp/packages/core-localization/package.json index 2c21f98de4..bfe7a6dc7f 100644 --- a/webapp/packages/core-localization/package.json +++ b/webapp/packages/core-localization/package.json @@ -20,7 +20,6 @@ "dependencies": { "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-executor": "~0.1.0", - "@cloudbeaver/core-settings": "~0.1.0", "mobx": "^6.12.0" }, "peerDependencies": {}, diff --git a/webapp/packages/core-localization/src/DEFAULT_LOCALE.ts b/webapp/packages/core-localization/src/DEFAULT_LOCALE.ts index 9cf9ebc0d1..c492c4f628 100644 --- a/webapp/packages/core-localization/src/DEFAULT_LOCALE.ts +++ b/webapp/packages/core-localization/src/DEFAULT_LOCALE.ts @@ -7,4 +7,4 @@ */ import type { ILocale } from './ILocale'; -export const DEFAULT_LOCALE: ILocale = { isoCode: 'en', displayName: 'English' }; +export const DEFAULT_LOCALE: ILocale = { isoCode: 'en', name: 'English' }; diff --git a/webapp/packages/core-localization/src/ILocale.ts b/webapp/packages/core-localization/src/ILocale.ts index 4073867a1b..124b8ef86b 100644 --- a/webapp/packages/core-localization/src/ILocale.ts +++ b/webapp/packages/core-localization/src/ILocale.ts @@ -8,5 +8,6 @@ export interface ILocale { isoCode: string; - displayName: string; + name: string; + nativeName?: string; } diff --git a/webapp/packages/core-localization/src/LocalizationService.ts b/webapp/packages/core-localization/src/LocalizationService.ts index dd3804ac4f..98679e895f 100644 --- a/webapp/packages/core-localization/src/LocalizationService.ts +++ b/webapp/packages/core-localization/src/LocalizationService.ts @@ -5,61 +5,52 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, makeObservable, observable, reaction } from 'mobx'; +import { IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; -import { SettingsService } from '@cloudbeaver/core-settings'; +import { Executor, IExecutor } from '@cloudbeaver/core-executor'; import { DEFAULT_LOCALE } from './DEFAULT_LOCALE'; import type { ILocale } from './ILocale'; import type { ILocaleProvider } from './ILocaleProvider'; import type { TLocalizationToken } from './TLocalizationToken'; -const LANG_SETTINGS_KEY = 'langSettings'; - -interface ISettings { - language: string | null; -} - @injectable() export class LocalizationService extends Bootstrap { get currentLanguage(): string { - const lang = this.settings.language; + const lang = this.language; if (lang !== null && this.isLanguageSupported(lang)) { return lang; } - if (this.isLanguageSupported(this.defaultLanguage)) { - return this.defaultLanguage; + if (this.isLanguageSupported(DEFAULT_LOCALE.isoCode)) { + return DEFAULT_LOCALE.isoCode; } return this.supportedLanguages[0].isoCode; } - settings: ISettings; supportedLanguages: ILocale[]; - readonly onChange: ISyncExecutor; - private defaultLanguage: string; + readonly onChange: IExecutor; + private language: string | null; private readonly localeMap: Map> = new Map(); private readonly localeProviders: ILocaleProvider[] = []; + private reactionDisposer: IReactionDisposer | null; - constructor(private readonly settingsService: SettingsService) { + constructor() { super(); - this.settings = getDefaultLocalizationSettings(); this.supportedLanguages = [DEFAULT_LOCALE]; - this.defaultLanguage = DEFAULT_LOCALE.isoCode; - this.onChange = new SyncExecutor(); + this.language = null; + this.reactionDisposer = null; + this.onChange = new Executor(); - makeObservable(this, { - defaultLanguage: observable, + makeObservable(this, { + language: observable, supportedLanguages: observable, - settings: observable, localeMap: observable.shallow, // observable.shallow - don't treat locales as observables - setCurrentLocale: action, }); } @@ -78,8 +69,8 @@ export class LocalizationService extends Bootstrap { } } - setDefaultLanguage(lang: string) { - this.defaultLanguage = lang; + setLanguage(lang: string) { + this.language = lang; } readonly translate = (token: T, fallback?: T, args: Record = {}): T => { @@ -89,8 +80,8 @@ export class LocalizationService extends Bootstrap { let translation = this.localeMap.get(this.currentLanguage)?.get(token as TLocalizationToken); - if (!translation) { - translation = this.localeMap.get(this.defaultLanguage)?.get(token as TLocalizationToken); + if (translation === undefined) { + translation = this.localeMap.get(DEFAULT_LOCALE.isoCode)?.get(token as TLocalizationToken); } if (typeof translation === 'string') { @@ -110,40 +101,69 @@ export class LocalizationService extends Bootstrap { return token; }; - register(): void | Promise { - this.settingsService.registerSettings(LANG_SETTINGS_KEY, this.settings, getDefaultLocalizationSettings); // load user state locale + register(): void { this.setSupportedLanguages([ { isoCode: 'en', - displayName: 'English', + name: 'English', + nativeName: 'English', }, { isoCode: 'ru', - displayName: 'Русский', + name: 'Russian', + nativeName: 'Русский', }, { isoCode: 'it', - displayName: 'Italiano', + name: 'Italian', + nativeName: 'Italiano', }, { isoCode: 'zh', - displayName: '中文', + name: 'Chinese', + nativeName: '中文', }, ]); this.addProvider(this.coreProvider.bind(this)); + this.reactionDisposer = reaction( + () => this.currentLanguage, + lang => { + this.loadLocale(lang); + }, + { + fireImmediately: true, + }, + ); } async load(): Promise { - await this.loadLocaleAsync(this.defaultLanguage); - await this.autoLoadCurrentLanguage(); + await this.loadLocale(DEFAULT_LOCALE.isoCode); + await this.loadLocale(this.currentLanguage); } - async changeLocaleAsync(key: string): Promise { + dispose(): void { + if (this.reactionDisposer) { + this.reactionDisposer(); + } + } + + async changeLocale(key: string): Promise { + const prevLocale = this.currentLanguage; if (key === this.currentLanguage) { return; } - await this.setLocale(key); - this.onChange.execute(this.currentLanguage); + if (!this.isLanguageSupported(key)) { + throw new Error(`Language '${key}' is not supported`); + } + + await this.loadLocale(key); + try { + this.setLanguage(key); + await this.onChange.execute(key); + } catch (e) { + this.setLanguage(prevLocale); + throw e; + } } private async coreProvider(locale: string) { @@ -159,20 +179,7 @@ export class LocalizationService extends Bootstrap { } } - private setCurrentLocale(lang: string | null) { - this.settings.language = lang; - } - - async setLocale(key: string) { - if (!this.isLanguageSupported(key)) { - throw new Error(`Language '${key}' is not supported`); - } - - this.setCurrentLocale(key); - await this.loadLocaleAsync(key); - } - - private async loadLocaleAsync(localeKey: string): Promise { + private async loadLocale(localeKey: string): Promise { if (this.localeMap.has(localeKey)) { return; } @@ -185,35 +192,4 @@ export class LocalizationService extends Bootstrap { } this.localeMap.set(localeKey, locale); } - - private async autoLoadCurrentLanguage() { - let resolve: (value: void | PromiseLike) => void; - let reject: (reason?: any) => void; - let promise: Promise | null = new Promise((res, rej) => { - resolve = res; - reject = rej; - }); - - reaction( - () => this.currentLanguage, - lang => { - this.loadLocaleAsync(lang) - .then(resolve, reject) - .finally(() => { - promise = null; - }); - }, - { - fireImmediately: true, - }, - ); - - await promise; - } -} - -function getDefaultLocalizationSettings(): ISettings { - return { - language: null, - }; } diff --git a/webapp/packages/core-localization/tsconfig.json b/webapp/packages/core-localization/tsconfig.json index d145aa11fb..df9dba157c 100644 --- a/webapp/packages/core-localization/tsconfig.json +++ b/webapp/packages/core-localization/tsconfig.json @@ -11,9 +11,6 @@ }, { "path": "../core-executor/tsconfig.json" - }, - { - "path": "../core-settings/tsconfig.json" } ], "include": [ diff --git a/webapp/packages/core-navigation-tree/package.json b/webapp/packages/core-navigation-tree/package.json index 622d1b98cb..abb8aad7c6 100644 --- a/webapp/packages/core-navigation-tree/package.json +++ b/webapp/packages/core-navigation-tree/package.json @@ -24,31 +24,28 @@ "@cloudbeaver/core-executor": "~0.1.0", "@cloudbeaver/core-extensions": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "mobx": "^6.12.0" }, "peerDependencies": {}, "devDependencies": { - "@cloudbeaver/core-app": "~0.1.0", "@cloudbeaver/core-authentication": "~0.1.0", "@cloudbeaver/core-browser": "~0.1.0", "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-product": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-routing": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", - "@cloudbeaver/core-theming": "~0.1.0", + "@cloudbeaver/core-storage": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", "@cloudbeaver/tests-runner": "~0.1.0", diff --git a/webapp/packages/core-navigation-tree/src/NAVIGATION_TREE_SETTINGS_GROUP.ts b/webapp/packages/core-navigation-tree/src/NAVIGATION_TREE_SETTINGS_GROUP.ts index 661f952ecd..99de5c3d8b 100644 --- a/webapp/packages/core-navigation-tree/src/NAVIGATION_TREE_SETTINGS_GROUP.ts +++ b/webapp/packages/core-navigation-tree/src/NAVIGATION_TREE_SETTINGS_GROUP.ts @@ -5,6 +5,6 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-plugin'; +import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-settings'; export const NAVIGATION_TREE_SETTINGS_GROUP = ROOT_SETTINGS_GROUP.createSubGroup('core_navigation_tree_navigation_tree_settings_group'); diff --git a/webapp/packages/core-navigation-tree/src/NavTreeSettingsService.test.ts b/webapp/packages/core-navigation-tree/src/NavTreeSettingsService.test.ts index fd75fb9af1..ec0d22004f 100644 --- a/webapp/packages/core-navigation-tree/src/NavTreeSettingsService.test.ts +++ b/webapp/packages/core-navigation-tree/src/NavTreeSettingsService.test.ts @@ -7,78 +7,64 @@ */ import '@testing-library/jest-dom'; -import { coreAppManifest } from '@cloudbeaver/core-app'; import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; import { mockAuthentication } from '@cloudbeaver/core-authentication/dist/__custom_mocks__/mockAuthentication'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; import { coreProjectsManifest } from '@cloudbeaver/core-projects'; import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; +import '@cloudbeaver/core-root/dist/__custom_mocks__/expectWebsocketClosedMessage'; import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; import { coreRoutingManifest } from '@cloudbeaver/core-routing'; import { coreSDKManifest } from '@cloudbeaver/core-sdk'; import { coreSettingsManifest } from '@cloudbeaver/core-settings'; -import { coreThemingManifest } from '@cloudbeaver/core-theming'; +import { + expectDeprecatedSettingMessage, + expectNoDeprecatedSettingMessage, +} from '@cloudbeaver/core-settings/dist/__custom_mocks__/expectDeprecatedSettingMessage'; +import { coreStorageManifest } from '@cloudbeaver/core-storage'; import { coreUIManifest } from '@cloudbeaver/core-ui'; import { coreViewManifest } from '@cloudbeaver/core-view'; import { createApp } from '@cloudbeaver/tests-runner'; import { coreNavigationTree } from './manifest'; -import { NavTreeSettings, NavTreeSettingsService } from './NavTreeSettingsService'; +import { NavTreeSettingsService } from './NavTreeSettingsService'; const endpoint = createGQLEndpoint(); +const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); const app = createApp( coreNavigationTree, coreEventsManifest, - corePluginManifest, - coreProductManifest, + coreStorageManifest, coreRootManifest, coreSDKManifest, coreSettingsManifest, coreBrowserManifest, coreRoutingManifest, - coreThemingManifest, coreLocalizationManifest, coreAuthenticationManifest, - coreAppManifest, coreProjectsManifest, coreUIManifest, coreViewManifest, coreClientActivityManifest, ); -const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); - -beforeAll(() => app.init()); - const deprecatedSettings = { - 'core.app': { - navigationTree: { - childrenLimit: 1, - }, - metadata: { - editing: false, - deleting: false, - }, - }, + 'core.app.navigationTree.childrenLimit': 100, + 'core.app.metadata.editing': false, + 'core.app.metadata.deleting': false, }; const newSettings = { ...deprecatedSettings, - core: { - 'navigation-tree': { - childrenLimit: 2, - editing: true, - deleting: true, - } as NavTreeSettings, - }, + 'core.navigation-tree.childrenLimit': 200, + 'core.navigation-tree.editing': true, + 'core.navigation-tree.deleting': true, }; test('New settings override deprecated', async () => { @@ -89,9 +75,10 @@ test('New settings override deprecated', async () => { await config.refresh(); - expect(settings.settings.getValue('childrenLimit')).toBe(2); - expect(settings.settings.getValue('editing')).toBe(true); - expect(settings.settings.getValue('deleting')).toBe(true); + expect(settings.childrenLimit).toBe(200); + expect(settings.editing).toBe(true); + expect(settings.deleting).toBe(true); + expectNoDeprecatedSettingMessage(); }); test('Deprecated settings are used if new settings are not defined', async () => { @@ -102,7 +89,8 @@ test('Deprecated settings are used if new settings are not defined', async () => await config.refresh(); - expect(settings.settings.getValue('childrenLimit')).toBe(1); - expect(settings.settings.getValue('editing')).toBe(false); - expect(settings.settings.getValue('deleting')).toBe(false); + expect(settings.childrenLimit).toBe(100); + expect(settings.editing).toBe(false); + expect(settings.deleting).toBe(false); + expectDeprecatedSettingMessage(); }); diff --git a/webapp/packages/core-navigation-tree/src/NavTreeSettingsService.ts b/webapp/packages/core-navigation-tree/src/NavTreeSettingsService.ts index 391c39d0e9..829dbf7a7a 100644 --- a/webapp/packages/core-navigation-tree/src/NavTreeSettingsService.ts +++ b/webapp/packages/core-navigation-tree/src/NavTreeSettingsService.ts @@ -6,34 +6,54 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { createSettingsAliasResolver, PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; -import { ServerSettingsResolverService, ServerSettingsService } from '@cloudbeaver/core-root'; -import { schema } from '@cloudbeaver/core-utils'; +import { + createSettingsAliasResolver, + ROOT_SETTINGS_LAYER, + SettingsManagerService, + SettingsProvider, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; const settingsSchema = schema.object({ - childrenLimit: schema.coerce.number().default(100), - editing: schema.coerce.boolean().default(true), - deleting: schema.coerce.boolean().default(true), + 'core.navigation-tree.childrenLimit': schema.coerce.number().min(10).max(1000).default(100), + 'core.navigation-tree.editing': schemaExtra.stringedBoolean().default(true), + 'core.navigation-tree.deleting': schemaExtra.stringedBoolean().default(true), }); export type NavTreeSettings = schema.infer; @injectable() export class NavTreeSettingsService extends Dependency { - readonly settings: PluginSettings; + get childrenLimit(): number { + return this.settings.getValue('core.navigation-tree.childrenLimit'); + } + get editing(): boolean { + return this.settings.getValue('core.navigation-tree.editing'); + } + get deleting(): boolean { + return this.settings.getValue('core.navigation-tree.deleting'); + } + readonly settings: SettingsProvider; constructor( - private readonly pluginManagerService: PluginManagerService, + private readonly settingsProviderService: SettingsProviderService, private readonly settingsManagerService: SettingsManagerService, - private readonly serverSettingsService: ServerSettingsService, - private readonly serverSettingsResolverService: ServerSettingsResolverService, + private readonly settingsResolverService: SettingsResolverService, ) { super(); - this.settings = this.pluginManagerService.createSettings('navigation-tree', 'core', settingsSchema); - this.serverSettingsResolverService.addResolver( + this.settings = this.settingsProviderService.createSettings(settingsSchema); + this.settingsResolverService.addResolver( + ROOT_SETTINGS_LAYER, /** @deprecated Use settings instead, will be removed in 23.0.0 */ - createSettingsAliasResolver(this.serverSettingsService, this.settings, 'core.app.navigationTree'), - createSettingsAliasResolver(this.serverSettingsService, this.settings, 'core.app.metadata'), + createSettingsAliasResolver(this.settingsResolverService, this.settings, { + 'core.navigation-tree.childrenLimit': 'core.app.navigationTree.childrenLimit', + }), + createSettingsAliasResolver(this.settingsResolverService, this.settings, { + 'core.navigation-tree.deleting': 'core.app.metadata.deleting', + 'core.navigation-tree.editing': 'core.app.metadata.editing', + }), ); this.registerSettings(); @@ -42,8 +62,11 @@ export class NavTreeSettingsService extends Dependency { private registerSettings() { this.settingsManagerService.registerSettings(this.settings, () => [ // { - // group: NAVIGATION_TREE_SETTINGS_GROUP, // key: 'childrenLimit', + // access: { + // accessor: ['server'], + // }, + // group: NAVIGATION_TREE_SETTINGS_GROUP, // name: 'Children limit', // type: ESettingsValueType.Input, // }, diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts index 566b9f30f0..b32c1283f5 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/NavTreeResource.ts @@ -65,7 +65,7 @@ export class NavTreeResource extends CachedMapResource; get childrenLimit(): number { - return this.navTreeSettingsService.settings.getValue('childrenLimit'); + return this.navTreeSettingsService.childrenLimit; } constructor( diff --git a/webapp/packages/core-navigation-tree/tsconfig.json b/webapp/packages/core-navigation-tree/tsconfig.json index c126841b41..9e4011c201 100644 --- a/webapp/packages/core-navigation-tree/tsconfig.json +++ b/webapp/packages/core-navigation-tree/tsconfig.json @@ -6,9 +6,6 @@ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "references": [ - { - "path": "../core-app/tsconfig.json" - }, { "path": "../core-authentication/tsconfig.json" }, @@ -42,15 +39,6 @@ { "path": "../core-localization/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-product/tsconfig.json" - }, { "path": "../core-projects/tsconfig.json" }, @@ -79,7 +67,10 @@ "path": "../core-settings/tsconfig.json" }, { - "path": "../core-theming/tsconfig.json" + "path": "../core-settings/tsconfig.json" + }, + { + "path": "../core-storage/tsconfig.json" }, { "path": "../core-ui/tsconfig.json" diff --git a/webapp/packages/core-plugin/src/PluginManagerService.ts b/webapp/packages/core-plugin/src/PluginManagerService.ts deleted file mode 100644 index c61e5a7c97..0000000000 --- a/webapp/packages/core-plugin/src/PluginManagerService.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { injectable } from '@cloudbeaver/core-di'; -import { ProductSettingsService } from '@cloudbeaver/core-product'; -import type { SettingsScopeType } from '@cloudbeaver/core-settings'; -import type { schema } from '@cloudbeaver/core-utils'; - -import { PluginSettings } from './PluginSettings'; - -@injectable() -export class PluginManagerService { - readonly store: Map; - constructor(private readonly productSettingsService: ProductSettingsService) { - this.store = new Map(); - } - - createSettings(scope: string, scopeType: SettingsScopeType, schema: TSchema) { - const key = scopeType + '.' + scope; - const settings = new PluginSettings(this.productSettingsService, key, schema); - - this.store.set(key, settings); - return settings; - } - - getSettings(id: string): PluginSettings | undefined; - getSettings(scope: string, scopeType: SettingsScopeType): PluginSettings | undefined; - getSettings(scope: string, scopeType?: SettingsScopeType): PluginSettings | undefined { - if (scopeType === undefined) { - return this.store.get(scope); - } - return this.store.get(scopeType + '.' + scope); - } -} diff --git a/webapp/packages/core-plugin/src/PluginSettings.ts b/webapp/packages/core-plugin/src/PluginSettings.ts deleted file mode 100644 index 08950110e5..0000000000 --- a/webapp/packages/core-plugin/src/PluginSettings.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import type { ISettingsSource } from '@cloudbeaver/core-settings'; -import type { schema } from '@cloudbeaver/core-utils'; - -export class PluginSettings implements ISettingsSource { - constructor(private readonly source: ISettingsSource, readonly scope: string, readonly schema: TSchema) {} - - isReadOnly>(key: TKey): boolean { - return this.source.has(key) || false; - } - - has>(key: TKey): boolean { - return true; - } - - isValueDefault>(key: TKey): boolean { - return !this.source.has(this.scopedKey(key)); - } - - getDefaultValue>(key: TKey): schema.infer[TKey] { - const value = this.source.getDefaultValue(this.scopedKey(key)); - const schema = this.schema.shape[key as any]; - const result = schema.safeParse(value); - - if (result.success) { - return result.data; - } - - return schema.parse(undefined); - } - - getValue>(key: TKey): schema.infer[TKey] { - let value = undefined; - - if (this.source.has(this.scopedKey(key))) { - value = this.source.getValue(this.scopedKey(key)); - } - - const schema = this.schema.shape[key as any]; - const result = schema.safeParse(value); - - if (result.success) { - return result.data; - } - - return schema.parse(undefined); - } - - setValue>(key: TKey, value: schema.infer[TKey]): void { - throw new Error(`Can't set value for key ${String(this.scopedKey(key))}`); - } - - clear(): void { - throw new Error("Can't clear settings"); - } - - private scopedKey(key: string | number | symbol): string { - return `${this.scope}.${String(key)}`; - } -} diff --git a/webapp/packages/core-plugin/src/SettingsManager/SettingsGroup.ts b/webapp/packages/core-plugin/src/SettingsManager/SettingsGroup.ts deleted file mode 100644 index 401bf0a9ca..0000000000 --- a/webapp/packages/core-plugin/src/SettingsManager/SettingsGroup.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { uuid } from '@cloudbeaver/core-utils'; - -export class SettingsGroup { - readonly id: string; - get subGroups(): ReadonlyArray { - return this.#subGroups; - } - - protected groups: Map; - #subGroups: SettingsGroup[]; - - constructor(readonly name: string, readonly parent?: SettingsGroup) { - this.id = uuid(); - this.#subGroups = []; - this.groups = parent?.groups || new Map(); - - this.groups.set(this.id, this); - } - - get(id: string): SettingsGroup | undefined { - return this.groups.get(id); - } - - createSubGroup(name: string): SettingsGroup { - const subGroup = new SettingsGroup(name, this); - - this.#subGroups.push(subGroup); - - return subGroup; - } -} diff --git a/webapp/packages/core-plugin/src/SettingsManager/SettingsManagerService.ts b/webapp/packages/core-plugin/src/SettingsManager/SettingsManagerService.ts deleted file mode 100644 index b9f3e37ba0..0000000000 --- a/webapp/packages/core-plugin/src/SettingsManager/SettingsManagerService.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { injectable } from '@cloudbeaver/core-di'; -import type { schema } from '@cloudbeaver/core-utils'; - -import type { PluginSettings } from '../PluginSettings'; -import type { ISettingDescriptionWithScope, SettingsDescriptionGetter } from './ISettingDescription'; - -interface ScopeSettingsItem { - scope: string; - schema: schema.AnyZodObject; - settingsGetter: SettingsDescriptionGetter; -} - -@injectable() -export class SettingsManagerService { - get settings(): ReadonlyArray { - return this.#settings; - } - - #settings: ScopeSettingsItem[]; - - constructor() { - this.#settings = []; - } - - registerSettings( - settingsSource: PluginSettings, - settingsGetter: SettingsDescriptionGetter>, - ) { - this.#settings.push({ - scope: settingsSource.scope, - schema: settingsSource.schema, - settingsGetter, - }); - } - - getSettings(): ISettingDescriptionWithScope[] { - const settings: ISettingDescriptionWithScope[] = []; - - for (const { schema, scope, settingsGetter } of this.settings) { - for (const setting of settingsGetter()) { - settings.push({ ...setting, scope, schema: schema.shape[setting.key] }); - } - } - - return settings; - } -} diff --git a/webapp/packages/core-plugin/src/createDeprecatedResolver.ts b/webapp/packages/core-plugin/src/createDeprecatedResolver.ts deleted file mode 100644 index 6884bf37f9..0000000000 --- a/webapp/packages/core-plugin/src/createDeprecatedResolver.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import type { ISettingsSource } from '@cloudbeaver/core-settings'; -import type { schema } from '@cloudbeaver/core-utils'; - -import type { PluginSettings } from './PluginSettings'; - -type SettingsMapping = { - [key in keyof TTarget]: keyof TSource; -}; - -export function createSettingsAliasResolver( - source: ISettingsSource, - target: PluginSettings, - scope: string, - mappings?: SettingsMapping, schema.infer>, -): ISettingsSource { - type targetSchema = schema.infer; - type sourceSchema = schema.infer; - - function isApplicable(key: keyof targetSchema): boolean { - return String(key).startsWith(target.scope + '.'); - } - function unscopeKey(key: keyof targetSchema): keyof targetSchema { - return String(key).replace(target.scope + '.', '') as unknown as keyof targetSchema; - } - function scopeKey(key: keyof sourceSchema): keyof sourceSchema { - return `${scope}.${String(key)}` as unknown as keyof sourceSchema; - } - function mapKey(key: keyof targetSchema): keyof sourceSchema { - key = unscopeKey(key); - - return scopeKey(mappings?.[key] || (key as unknown as keyof sourceSchema)); - } - return { - has(key) { - if (!isApplicable(key)) { - return false; - } - return source.has(mapKey(key)); - }, - isReadOnly(key) { - if (!isApplicable(key)) { - return true; - } - return source.isReadOnly(mapKey(key)); - }, - getDefaultValue(key) { - if (!isApplicable(key)) { - return undefined; - } - return source.getDefaultValue(mapKey(key)); - }, - getValue(key) { - if (!isApplicable(key)) { - return undefined; - } - return source.getValue(mapKey(key)); - }, - setValue(key, value) { - if (!isApplicable(key)) { - return; - } - source.setValue(mapKey(key), value); - }, - clear() { - source.clear(); - }, - }; -} diff --git a/webapp/packages/core-plugin/src/index.ts b/webapp/packages/core-plugin/src/index.ts deleted file mode 100644 index 769ebfb1d9..0000000000 --- a/webapp/packages/core-plugin/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export * from './SettingsManager/ESettingsValueType'; -export * from './SettingsManager/ISettingDescription'; -export * from './SettingsManager/SettingsGroup'; -export * from './SettingsManager/SettingsManagerService'; -export * from './SettingsManager/SettingsGroups/INTERFACE_SETTINGS_GROUP'; -export * from './SettingsManager/SettingsGroups/ROOT_SETTINGS_GROUP'; -export * from './PluginSettings'; -export * from './PluginManagerService'; -export * from './createDeprecatedResolver'; -export * from './manifest'; diff --git a/webapp/packages/core-product/package.json b/webapp/packages/core-product/package.json index fc552a8eaa..4c48170207 100644 --- a/webapp/packages/core-product/package.json +++ b/webapp/packages/core-product/package.json @@ -18,7 +18,6 @@ }, "dependencies": { "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "mobx": "^6.12.0" diff --git a/webapp/packages/core-product/src/ProductManagerService.ts b/webapp/packages/core-product/src/ProductManagerService.ts deleted file mode 100644 index 5b7832740f..0000000000 --- a/webapp/packages/core-product/src/ProductManagerService.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { injectable } from '@cloudbeaver/core-di'; -import { parseJSONFlat } from '@cloudbeaver/core-utils'; - -import { ProductSettingsService } from './ProductSettingsService'; - -@injectable() -export class ProductManagerService { - constructor(private readonly productSettingsService: ProductSettingsService) {} - - setSettings(object: any) { - parseJSONFlat(object, this.productSettingsService.setValue.bind(this.productSettingsService), undefined); - } -} diff --git a/webapp/packages/core-product/src/ProductSettingsService.ts b/webapp/packages/core-product/src/ProductSettingsService.ts index 687d3a496e..4897a2101c 100644 --- a/webapp/packages/core-product/src/ProductSettingsService.ts +++ b/webapp/packages/core-product/src/ProductSettingsService.ts @@ -5,47 +5,64 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { makeObservable, observable } from 'mobx'; +import { action, makeObservable, observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; -import { SessionSettingsService } from '@cloudbeaver/core-root'; -import type { ISettingsSource } from '@cloudbeaver/core-settings'; +import { createSettingsLayer, ROOT_SETTINGS_LAYER, SettingsSource } from '@cloudbeaver/core-settings'; + +export const PRODUCT_SETTINGS_LAYER = createSettingsLayer(ROOT_SETTINGS_LAYER, 'product'); @injectable() -export class ProductSettingsService implements ISettingsSource { +export class ProductSettingsService extends SettingsSource { private readonly settings: Map; - constructor(private readonly sessionSettingsService: SessionSettingsService) { + constructor() { + super(); this.settings = new Map(); makeObservable(this, { - settings: observable, + clear: action, + settings: observable.shallow, }); } has(key: any): boolean { - return this.settings.has(key) || this.sessionSettingsService?.has(key) || false; + return this.settings.has(key) || super.has(key) || false; } isReadOnly(): boolean { return true; } - getDefaultValue(key: any): any { - return this.sessionSettingsService.getDefaultValue(key) ?? this.settings.get(key); - } - getValue(key: any): any { - if (this.sessionSettingsService.has(key)) { - return this.sessionSettingsService.getValue(key); - } return this.settings.get(key); } setValue(key: any, value: any): void { - this.settings.set(key, value); + this.update(() => { + this.settings.set(key, value); + }); + } + + setSettingsObject(settings: Record): void { + this.update(() => { + if (settings && typeof settings === 'object') { + for (const [key, value] of Object.entries(settings)) { + this.setValue(key, value); + } + } + }); } clear(): void { - this.settings.clear(); + this.update(() => { + super.clear(); + this.settings.clear(); + }); + } + + async save(): Promise {} + + protected getSnapshot(): Record { + return Object.fromEntries(this.settings.entries()); } } diff --git a/webapp/packages/core-product/src/index.ts b/webapp/packages/core-product/src/index.ts index 610bfa0dca..3d2aabcff2 100644 --- a/webapp/packages/core-product/src/index.ts +++ b/webapp/packages/core-product/src/index.ts @@ -1,3 +1,2 @@ -export * from './ProductManagerService'; export * from './ProductSettingsService'; export * from './manifest'; diff --git a/webapp/packages/core-product/src/manifest.ts b/webapp/packages/core-product/src/manifest.ts index a3f21b5562..2e41f1e753 100644 --- a/webapp/packages/core-product/src/manifest.ts +++ b/webapp/packages/core-product/src/manifest.ts @@ -7,7 +7,6 @@ */ import type { PluginManifest } from '@cloudbeaver/core-di'; -import { ProductManagerService } from './ProductManagerService'; import { ProductSettingsService } from './ProductSettingsService'; export const coreProductManifest: PluginManifest = { @@ -15,5 +14,5 @@ export const coreProductManifest: PluginManifest = { name: 'Core Product', }, - providers: [ProductManagerService, ProductSettingsService], + providers: [ProductSettingsService], }; diff --git a/webapp/packages/core-product/tsconfig.json b/webapp/packages/core-product/tsconfig.json index 19fa5c895d..168ad92f08 100644 --- a/webapp/packages/core-product/tsconfig.json +++ b/webapp/packages/core-product/tsconfig.json @@ -9,9 +9,6 @@ { "path": "../core-di/tsconfig.json" }, - { - "path": "../core-root/tsconfig.json" - }, { "path": "../core-settings/tsconfig.json" }, diff --git a/webapp/packages/core-resource-manager/package.json b/webapp/packages/core-resource-manager/package.json index f72372251d..00972df607 100644 --- a/webapp/packages/core-resource-manager/package.json +++ b/webapp/packages/core-resource-manager/package.json @@ -22,11 +22,11 @@ "@cloudbeaver/core-executor": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "mobx": "^6.12.0" }, diff --git a/webapp/packages/core-resource-manager/src/RESOURCE_MANAGER_SETTINGS_GROUP.ts b/webapp/packages/core-resource-manager/src/RESOURCE_MANAGER_SETTINGS_GROUP.ts index 93aaab5ac3..6967714eda 100644 --- a/webapp/packages/core-resource-manager/src/RESOURCE_MANAGER_SETTINGS_GROUP.ts +++ b/webapp/packages/core-resource-manager/src/RESOURCE_MANAGER_SETTINGS_GROUP.ts @@ -5,6 +5,6 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-plugin'; +import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-settings'; export const RESOURCE_MANAGER_SETTINGS_GROUP = ROOT_SETTINGS_GROUP.createSubGroup('core_resource_manager_resource_manager_settings_group'); diff --git a/webapp/packages/core-resource-manager/tsconfig.json b/webapp/packages/core-resource-manager/tsconfig.json index 02689895de..0bbb290f03 100644 --- a/webapp/packages/core-resource-manager/tsconfig.json +++ b/webapp/packages/core-resource-manager/tsconfig.json @@ -21,9 +21,6 @@ { "path": "../core-navigation-tree/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, { "path": "../core-projects/tsconfig.json" }, @@ -36,6 +33,9 @@ { "path": "../core-sdk/tsconfig.json" }, + { + "path": "../core-settings/tsconfig.json" + }, { "path": "../core-utils/tsconfig.json" } diff --git a/webapp/packages/core-root/package.json b/webapp/packages/core-root/package.json index 1463778f9e..0a8e7bc5a7 100644 --- a/webapp/packages/core-root/package.json +++ b/webapp/packages/core-root/package.json @@ -20,6 +20,8 @@ "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-executor": "~0.1.0", + "@cloudbeaver/core-localization": "~0.1.0", + "@cloudbeaver/core-product": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", @@ -30,6 +32,7 @@ "peerDependencies": {}, "devDependencies": { "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/tests-runner": "~0.1.0", "@types/jest": "^29.5.12", "msw": "^2.2.2", "typescript": "^5.3.3" diff --git a/webapp/packages/core-root/src/RootBootstrap.ts b/webapp/packages/core-root/src/RootBootstrap.ts new file mode 100644 index 0000000000..e2f2ed1668 --- /dev/null +++ b/webapp/packages/core-root/src/RootBootstrap.ts @@ -0,0 +1,33 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { SettingsManagerService, SettingsResolverService } from '@cloudbeaver/core-settings'; + +import { ServerSettingsManagerService } from './Settings/ServerSettingsManagerService'; +import { SERVER_SETTINGS_LAYER, ServerSettingsService } from './Settings/ServerSettingsService'; + +@injectable() +export class RootBootstrap extends Bootstrap { + constructor( + private readonly settingsResolverService: SettingsResolverService, + private readonly serverSettingsService: ServerSettingsService, + private readonly settingsManagerService: SettingsManagerService, + private readonly serverSettingsManagerService: ServerSettingsManagerService, + ) { + super(); + } + + register(): void { + this.settingsManagerService.registerSettings( + this.serverSettingsManagerService.settingsProvider, + this.serverSettingsManagerService.getSettingsGetter(), + this.serverSettingsManagerService.loaders, + ); + this.settingsResolverService.addResolver(SERVER_SETTINGS_LAYER, this.serverSettingsService); + } +} diff --git a/webapp/packages/core-root/src/ServerSettingsResolverService.ts b/webapp/packages/core-root/src/ServerSettingsResolverService.ts deleted file mode 100644 index ad12b9dc7e..0000000000 --- a/webapp/packages/core-root/src/ServerSettingsResolverService.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { injectable } from '@cloudbeaver/core-di'; -import { SettingsResolverSource } from '@cloudbeaver/core-settings'; - -import { ServerSettingsService } from './ServerSettingsService'; - -@injectable() -export class ServerSettingsResolverService extends SettingsResolverSource { - constructor(serverSettingsService: ServerSettingsService) { - super(); - this.addResolver(serverSettingsService); - } -} diff --git a/webapp/packages/core-root/src/ServerSettingsService.ts b/webapp/packages/core-root/src/ServerSettingsService.ts deleted file mode 100644 index f7b317cde5..0000000000 --- a/webapp/packages/core-root/src/ServerSettingsService.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { action, makeObservable, observable } from 'mobx'; - -import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { type ISettingsSource, SettingsResolverService } from '@cloudbeaver/core-settings'; -import { parseJSONFlat, setByPath } from '@cloudbeaver/core-utils'; - -import { EAdminPermission } from './EAdminPermission'; -import { ServerConfigResource } from './ServerConfigResource'; -import { SessionPermissionsResource } from './SessionPermissionsResource'; - -@injectable() -export class ServerSettingsService extends Dependency implements ISettingsSource { - private readonly settings: Map; - private readonly changes: Map; - private lastConfig: any; - - get isChanged(): boolean { - return this.changes.size > 0; - } - - constructor( - readonly serverConfigResource: ServerConfigResource, - private readonly sessionPermissionsResource: SessionPermissionsResource, - private readonly settingsResolverService: SettingsResolverService, - ) { - super(); - this.lastConfig = null; - this.settings = new Map(); - this.changes = new Map(); - this.serverConfigResource.onDataUpdate.addHandler(this.refreshConfig.bind(this)); - - makeObservable(this, { - refreshConfig: action, - changes: observable, - settings: observable, - }); - } - - has(key: any): boolean { - return this.settingsResolverService.has(key) || this.settings.has(key) || this.changes.has(key); - } - - isReadOnly(key: any): boolean { - return this.settingsResolverService.has(key) || !this.sessionPermissionsResource.has(EAdminPermission.admin); - } - - getDefaultValue(key: any): any { - return this.settingsResolverService.getDefaultValue(key) ?? this.settings.get(key); - } - - getValue(key: any): any { - if (this.settingsResolverService.has(key)) { - return this.settingsResolverService.getValue(key); - } - - if (this.changes.has(key)) { - return this.changes.get(key); - } - - return this.settings.get(key); - } - - setValue(key: any, value: any): void { - if (this.settingsResolverService.has(key)) { - throw new Error(`Can't set value for key ${key}`); - } - - if (this.settings.get(key) === value) { - this.changes.delete(key); - } else { - this.changes.set(key, value); - } - } - - clear(): void { - this.settings.clear(); - this.changes.clear(); - } - - async save() { - const changes: Record = {}; - - for (const [key, value] of this.changes) { - changes[key] = value; - } - - await this.serverConfigResource.updateProductConfiguration(changes); - } - - resetChanges() { - this.changes.clear(); - } - - private refreshConfig() { - this.clear(); - - if (!this.serverConfigResource.data) { - this.lastConfig = null; - return; - } - - if (this.serverConfigResource.data.productConfiguration !== this.lastConfig) { - this.lastConfig = this.serverConfigResource.data.productConfiguration; - parseJSONFlat(this.serverConfigResource.data.productConfiguration, (key, value) => { - this.settings.set(key, value); - }); - } - } -} diff --git a/webapp/packages/core-root/src/SessionEventSource.ts b/webapp/packages/core-root/src/SessionEventSource.ts index 24c375a708..228b0121c2 100644 --- a/webapp/packages/core-root/src/SessionEventSource.ts +++ b/webapp/packages/core-root/src/SessionEventSource.ts @@ -5,7 +5,23 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { catchError, debounceTime, filter, interval, map, merge, Observable, repeat, retry, share, Subject, throwError } from 'rxjs'; +import { + catchError, + debounceTime, + delayWhen, + filter, + interval, + map, + merge, + Observable, + Observer, + of, + repeat, + retry, + share, + Subject, + throwError, +} from 'rxjs'; import { webSocket, WebSocketSubject } from 'rxjs/webSocket'; import { injectable } from '@cloudbeaver/core-di'; @@ -13,7 +29,6 @@ import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import { CbClientEventId as ClientEventId, EnvironmentService, - GraphQLService, CbServerEventId as ServerEventId, ServiceError, CbEventTopic as SessionEventTopic, @@ -50,17 +65,19 @@ export class SessionEventSource implements IServerEventEmitter; private readonly subject: WebSocketSubject; private readonly oldEventsSubject: Subject; + private readonly emitSubject: Subject; private readonly retryTimer: Observable; + private readonly disconnectSubject: Subject; private disconnected: boolean; constructor( - private readonly networkStateService: NetworkStateService, + networkStateService: NetworkStateService, private readonly sessionExpireService: SessionExpireService, - private readonly environmentService: EnvironmentService, - private readonly graphQLService: GraphQLService, + environmentService: EnvironmentService, ) { this.onInit = new SyncExecutor(); this.oldEventsSubject = new Subject(); + this.disconnectSubject = new Subject(); this.closeSubject = new Subject(); this.openSubject = new Subject(); this.errorSubject = new Subject(); @@ -74,6 +91,9 @@ export class SessionEventSource implements IServerEventEmitter { this.onInit.execute(); }); @@ -82,7 +102,7 @@ export class SessionEventSource implements IServerEventEmitter { console.error(error); @@ -94,7 +114,6 @@ export class SessionEventSource implements IServerEventEmitter(id: SessionEventId, callback: IServerEventCallback, mapTo: (event: ISessionEvent) => T = e => e as T): Subscription { const sub = this.eventsSubject .pipe( - this.handleErrors(), filter(event => event.id === id), map(mapTo), ) @@ -110,7 +129,7 @@ export class SessionEventSource implements IServerEventEmitter T = e => e as T, filterFn: (event: ISessionEvent) => boolean = () => true, ): Subscription { - const sub = this.eventsSubject.pipe(this.handleErrors(), filter(filterFn), map(mapTo)).subscribe(callback); + const sub = this.eventsSubject.pipe(filter(filterFn), map(mapTo)).subscribe(callback); return () => { sub.unsubscribe(); @@ -118,23 +137,60 @@ export class SessionEventSource implements IServerEventEmitter(topicId: SessionEventTopic, mapTo: (event: ISessionEvent) => T = e => e as T): Observable { - return merge( - this.subject.multiplex( - () => ({ id: ClientEventId.CbClientTopicSubscribe, topicId } as ITopicSubEvent), - () => ({ id: ClientEventId.CbClientTopicUnsubscribe, topicId } as ITopicSubEvent), - event => event.topicId === topicId, - ), - this.oldEventsSubject, - ).pipe(this.handleErrors(), map(mapTo)); + return new Observable((observer: Observer) => { + try { + this.emitSubject.next({ id: ClientEventId.CbClientTopicSubscribe, topicId } as ITopicSubEvent); + } catch (err) { + observer.error(err); + } + + const subscription = this.eventsSubject.subscribe({ + next: x => { + try { + if (x.topicId === topicId) { + observer.next(mapTo(x)); + } + } catch (err) { + observer.error(err); + } + }, + error: err => observer.error(err), + complete: () => observer.complete(), + }); + + return () => { + try { + this.emitSubject.next({ id: ClientEventId.CbClientTopicUnsubscribe, topicId } as ITopicSubEvent); + } catch (err) { + observer.error(err); + } + subscription.unsubscribe(); + }; + }); } emit(event: ISessionEvent): this { - this.subject.next(event); + this.emitSubject.next(event); return this; } + connect() { + this.disconnected = false; + this.disconnectSubject.next(this.disconnected); + } + disconnect() { this.disconnected = true; + this.disconnectSubject.next(this.disconnected); + } + + private handleDisconnected() { + return delayWhen(() => { + if (this.disconnected) { + return this.disconnectSubject.pipe(filter(disconnected => !disconnected)); + } + return of(true); + }); } private handleErrors() { diff --git a/webapp/packages/core-root/src/SessionResource.ts b/webapp/packages/core-root/src/SessionResource.ts index 19af9c4b3a..91d7d2038e 100644 --- a/webapp/packages/core-root/src/SessionResource.ts +++ b/webapp/packages/core-root/src/SessionResource.ts @@ -6,6 +6,7 @@ * you may not use this file except in compliance with the License. */ import { injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; import { CachedDataResource } from '@cloudbeaver/core-resource'; import { GraphQLService, SessionStateFragment } from '@cloudbeaver/core-sdk'; @@ -22,12 +23,12 @@ export interface ISessionAction { @injectable() export class SessionResource extends CachedDataResource { private action: ISessionAction | null; - private defaultLocale: string | undefined; constructor( private readonly graphQLService: GraphQLService, private readonly sessionInfoEventHandler: SessionInfoEventHandler, serverConfigResource: ServerConfigResource, + private readonly localizationService: LocalizationService, ) { super(() => null); @@ -51,10 +52,6 @@ export class SessionResource extends CachedDataResource { } } - setDefaultLocale(defaultLocale?: string): void { - this.defaultLocale = defaultLocale; - } - private handleSessionStateEvent(event: ISessionStateEvent) { this.performUpdate(undefined, [], async () => { if (!this.data) { @@ -81,7 +78,6 @@ export class SessionResource extends CachedDataResource { } await this.graphQLService.sdk.changeSessionLanguage({ locale }); - this.defaultLocale = locale; if (this.data) { this.data.locale = locale; } @@ -90,7 +86,7 @@ export class SessionResource extends CachedDataResource { } protected async loader(): Promise { - const { session } = await this.graphQLService.sdk.openSession({ defaultLocale: this.defaultLocale }); + const { session } = await this.graphQLService.sdk.openSession({ defaultLocale: this.localizationService.currentLanguage }); return session; } diff --git a/webapp/packages/core-root/src/SessionSettingsService.ts b/webapp/packages/core-root/src/SessionSettingsService.ts deleted file mode 100644 index 4e281215ad..0000000000 --- a/webapp/packages/core-root/src/SessionSettingsService.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { injectable } from '@cloudbeaver/core-di'; -import type { ISettingsSource } from '@cloudbeaver/core-settings'; - -import { ServerSettingsResolverService } from './ServerSettingsResolverService'; - -@injectable() -export class SessionSettingsService implements ISettingsSource { - constructor(private readonly serverSettingsResolverService: ServerSettingsResolverService) {} - - has(key: any): boolean { - return this.serverSettingsResolverService.has(key); - } - - isReadOnly(key: any): boolean { - return true; - } - - getDefaultValue(key: any): any { - return this.serverSettingsResolverService.getDefaultValue(key); - } - - getValue(key: any): any { - return this.serverSettingsResolverService.getValue(key); - } - setValue(key: any, value: any): void { - throw new Error('Method not implemented.'); - } - clear(): void { - throw new Error('Method not implemented.'); - } -} diff --git a/webapp/packages/core-root/src/Settings/SESSION_SETTINGS_LAYER.ts b/webapp/packages/core-root/src/Settings/SESSION_SETTINGS_LAYER.ts new file mode 100644 index 0000000000..b245257234 --- /dev/null +++ b/webapp/packages/core-root/src/Settings/SESSION_SETTINGS_LAYER.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createSettingsLayer } from '@cloudbeaver/core-settings'; + +import { SERVER_SETTINGS_LAYER } from './ServerSettingsService'; + +export const SESSION_SETTINGS_LAYER = createSettingsLayer(SERVER_SETTINGS_LAYER, 'session'); diff --git a/webapp/packages/core-root/src/Settings/ServerSettingsManagerService.ts b/webapp/packages/core-root/src/Settings/ServerSettingsManagerService.ts new file mode 100644 index 0000000000..9997e92bfb --- /dev/null +++ b/webapp/packages/core-root/src/Settings/ServerSettingsManagerService.ts @@ -0,0 +1,165 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { action, computed, makeObservable, observable } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import { getCachedDataResourceLoaderState } from '@cloudbeaver/core-resource'; +import { getObjectPropertyType, type ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; +import { + ESettingsValueType, + type ISettingDescription, + ROOT_SETTINGS_GROUP, + SettingDescriptionTransformer, + type SettingsDescriptionGetter, + type SettingsGroup, + SettingsProvider, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; +import { getPathParent, ILoadableState, schema } from '@cloudbeaver/core-utils'; + +import { ServerSettingsResource } from './ServerSettingsResource'; + +@injectable() +export class ServerSettingsManagerService { + readonly settingsProvider: SettingsProvider; + readonly loaders: ReadonlyArray; + + get providedSettings(): Set { + return new Set(this.settings.map(setting => setting.key as string)); + } + + private get settings(): ReadonlyArray> { + return (this.serverSettingsResource.data?.settings || []).map(this.mapSetting.bind(this)).filter(Boolean) as ISettingDescription[]; + } + + private serverGroups: Map; + private overrideGroups: Map; + private settingTransformers: Map; + + constructor( + private readonly serverSettingsResource: ServerSettingsResource, + private readonly settingsResolverService: SettingsResolverService, + ) { + this.serverGroups = new Map(); + this.overrideGroups = new Map(); + this.settingTransformers = new Map(); + this.settingsProvider = new SettingsProvider(this.settingsResolverService, schema.object({})); + this.loaders = [getCachedDataResourceLoaderState(this.serverSettingsResource, () => undefined)]; + + serverSettingsResource.onDataUpdate.addHandler(this.loadSettings.bind(this)); + makeObservable(this, { + settings: computed, + providedSettings: computed, + serverGroups: observable.shallow, + overrideGroups: observable.shallow, + settingTransformers: observable.shallow, + loadSettings: action, + }); + } + + getSettingsGetter(): SettingsDescriptionGetter { + return () => this.settings; + } + + setGroupOverride(group: string, override: SettingsGroup) { + this.overrideGroups.set(group, override); + } + + setSettingTransformer(key: string, transformer: SettingDescriptionTransformer) { + if (this.settingTransformers.has(key)) { + throw new Error('Setting transformer already exists'); + } + + this.settingTransformers.set(key, transformer); + } + + private mapSetting(property: ObjectPropertyInfo): ISettingDescription | null { + const key = property.id!; + const transformer = this.settingTransformers.get(key); + + const setting: ISettingDescription = { + key, + access: { + scope: this.mapSettingScope(property.scopes || []), + }, + group: this.getSettingGroup(property.category), + type: convertObjectPropertyInfoType(property), + + name: property.displayName!, + description: property.description!, + options: property.validValues?.map(value => ({ value, name: value })) || [], + }; + + if (transformer) { + return transformer(setting); + } + + return setting; + } + + private loadSettings() { + this.serverGroups.forEach(group => group.parent?.deleteSubGroup(group.id)); + this.serverGroups.clear(); + const groups = [...(this.serverSettingsResource.data?.groups || [])].sort((a, b) => a.id.localeCompare(b.id)); + + for (const group of groups) { + let parentGroup = ROOT_SETTINGS_GROUP; + const parent = getPathParent(group.id); + const parentGroupCache = this.serverGroups.get(parent); + + if (parent && parentGroupCache && ROOT_SETTINGS_GROUP.has(parentGroupCache.id)) { + parentGroup = ROOT_SETTINGS_GROUP.get(parentGroupCache.id)!; + } + + this.serverGroups.set(group.id, parentGroup.createSubGroup(group.displayName)); + } + } + + private getSettingGroup(group?: string): SettingsGroup { + if (!group) { + return ROOT_SETTINGS_GROUP; + } + + const overrideGroup = this.overrideGroups.get(group); + if (overrideGroup) { + return overrideGroup; + } + return this.serverGroups.get(group) || ROOT_SETTINGS_GROUP; + } + + private mapSettingScope(scopes: string[]): string[] { + if (scopes.length === 0) { + return ['server']; + } + + return scopes.map(scope => { + switch (scope) { + case 'global': + return 'server'; + case 'user': + return 'client'; + default: + return scope; + } + }); + } +} + +function convertObjectPropertyInfoType(property: ObjectPropertyInfo): ESettingsValueType { + switch (getObjectPropertyType(property)) { + case 'selector': + return ESettingsValueType.Select; + case 'checkbox': + return ESettingsValueType.Checkbox; + case 'textarea': + return ESettingsValueType.Textarea; + case 'input': + default: + return ESettingsValueType.Input; + } +} diff --git a/webapp/packages/core-root/src/Settings/ServerSettingsResource.ts b/webapp/packages/core-root/src/Settings/ServerSettingsResource.ts new file mode 100644 index 0000000000..6e9e763c85 --- /dev/null +++ b/webapp/packages/core-root/src/Settings/ServerSettingsResource.ts @@ -0,0 +1,32 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { CachedDataResource } from '@cloudbeaver/core-resource'; +import { GraphQLService, ProductSettings } from '@cloudbeaver/core-sdk'; + +import { ServerConfigResource } from '../ServerConfigResource'; + +export type ServerSettings = ProductSettings; + +@injectable() +export class ServerSettingsResource extends CachedDataResource { + constructor( + private readonly graphQLService: GraphQLService, + serverConfigResource: ServerConfigResource, + ) { + super(() => null, undefined, []); + + this.sync(serverConfigResource); + } + + protected async loader(): Promise { + const { settings } = await this.graphQLService.sdk.getProductSettings(); + + return settings as ServerSettings; + } +} diff --git a/webapp/packages/core-root/src/Settings/ServerSettingsService.ts b/webapp/packages/core-root/src/Settings/ServerSettingsService.ts new file mode 100644 index 0000000000..508aae3f8e --- /dev/null +++ b/webapp/packages/core-root/src/Settings/ServerSettingsService.ts @@ -0,0 +1,91 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { action, makeObservable, observable } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import { PRODUCT_SETTINGS_LAYER } from '@cloudbeaver/core-product'; +import { createSettingsLayer, SettingsSource } from '@cloudbeaver/core-settings'; + +import { EAdminPermission } from '../EAdminPermission'; +import { ServerConfigResource } from '../ServerConfigResource'; +import { SessionPermissionsResource } from '../SessionPermissionsResource'; + +export const SERVER_SETTINGS_LAYER = createSettingsLayer(PRODUCT_SETTINGS_LAYER, 'server'); + +@injectable() +export class ServerSettingsService extends SettingsSource { + private readonly settings: Map; + private lastConfig: any; + + constructor( + readonly serverConfigResource: ServerConfigResource, + private readonly sessionPermissionsResource: SessionPermissionsResource, + ) { + super(); + this.lastConfig = null; + this.settings = new Map(); + this.serverConfigResource.onDataUpdate.addHandler(this.refreshConfig.bind(this)); + + makeObservable(this, { + refreshConfig: action, + settings: observable.shallow, + clear: action, + }); + } + + has(key: any): boolean { + return this.settings.has(key) || super.has(key); + } + + isReadOnly(key: any): boolean { + return !this.sessionPermissionsResource.has(EAdminPermission.admin); + } + + getValue(key: any): any { + return this.settings.get(key); + } + + clear(): void { + this.update(() => { + super.clear(); + this.settings.clear(); + }); + } + + resetChanges() { + super.clear(); + } + + async save() { + await this.serverConfigResource.updateProductConfiguration(Object.fromEntries(this.changes)); + } + + protected getSnapshot() { + return Object.fromEntries(this.settings); + } + + private refreshConfig() { + this.update(() => { + this.clear(); + + if (!this.serverConfigResource.data) { + this.lastConfig = null; + return; + } + + if (this.serverConfigResource.data.productConfiguration !== this.lastConfig) { + this.lastConfig = this.serverConfigResource.data.productConfiguration; + if (this.lastConfig && typeof this.lastConfig === 'object') { + for (const [key, value] of Object.entries(this.lastConfig)) { + this.settings.set(key, value); + } + } + } + }); + } +} diff --git a/webapp/packages/core-root/src/__custom_mocks__/data/defaultProductConfiguration.ts b/webapp/packages/core-root/src/__custom_mocks__/data/defaultProductConfiguration.ts index 45ebe03607..6cd8f4ada0 100644 --- a/webapp/packages/core-root/src/__custom_mocks__/data/defaultProductConfiguration.ts +++ b/webapp/packages/core-root/src/__custom_mocks__/data/defaultProductConfiguration.ts @@ -6,29 +6,4 @@ * you may not use this file except in compliance with the License. */ -export const defaultProductConfiguration: Record = { - core_events: { - notificationsPool: 20.0, - }, - core: { - user: { - defaultTheme: 'light', - defaultLanguage: 'en', - }, - app: { - logViewer: { - refreshTimeout: 3000.0, - logBatchSize: 1000.0, - maxLogRecords: 2000.0, - maxFailedRequests: 3.0, - }, - }, - }, - plugin_data_export: { - disabled: false, - }, - plugin_data_spreadsheet_new: { - hidden: false, - }, - 'sql.proposals.insert.table.alias': true, -}; +export const defaultProductConfiguration: Record = {}; diff --git a/webapp/packages/core-root/src/__custom_mocks__/expectWebsocketClosedMessage.ts b/webapp/packages/core-root/src/__custom_mocks__/expectWebsocketClosedMessage.ts new file mode 100644 index 0000000000..443eb8ba29 --- /dev/null +++ b/webapp/packages/core-root/src/__custom_mocks__/expectWebsocketClosedMessage.ts @@ -0,0 +1,22 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { addKnownInfo, consoleSpy } from '@cloudbeaver/tests-runner'; + +const WEBSOCKET_CLOSED_MESSAGE_REGEX = /Websocket closed.*/; + +beforeAll(async () => { + addKnownInfo(WEBSOCKET_CLOSED_MESSAGE_REGEX); +}); + +export function expectWebsocketClosedMessage() { + expect(consoleSpy.info).toHaveBeenCalledWith(expect.stringMatching(WEBSOCKET_CLOSED_MESSAGE_REGEX)); +} + +export function expectNoWebsocketClosedMessage() { + expect(consoleSpy.info).not.toHaveBeenCalledWith(expect.stringMatching(WEBSOCKET_CLOSED_MESSAGE_REGEX)); +} diff --git a/webapp/packages/core-root/src/index.ts b/webapp/packages/core-root/src/index.ts index 8021c7a595..1f3c53e2b1 100644 --- a/webapp/packages/core-root/src/index.ts +++ b/webapp/packages/core-root/src/index.ts @@ -14,12 +14,12 @@ export * from './ServerConfigResource'; export * from './SessionEventSource'; export * from './QuotasService'; export * from './SessionResource'; -export * from './ServerSettingsResolverService'; -export * from './ServerSettingsService'; +export * from './Settings/ServerSettingsManagerService'; +export * from './Settings/ServerSettingsService'; +export * from './Settings/SESSION_SETTINGS_LAYER'; export * from './SessionActionService'; export * from './EAdminPermission'; export * from './SessionDataResource'; -export * from './SessionSettingsService'; export * from './SessionExpireService'; export * from './SessionExpireEventService'; export * from './SessionActivityService'; diff --git a/webapp/packages/core-root/src/manifest.ts b/webapp/packages/core-root/src/manifest.ts index 5bfb8c7871..2a3f9ab920 100644 --- a/webapp/packages/core-root/src/manifest.ts +++ b/webapp/packages/core-root/src/manifest.ts @@ -12,11 +12,10 @@ import { FeaturesResource } from './FeaturesResource'; import { NetworkStateService } from './NetworkStateService'; import { PermissionsService } from './PermissionsService'; import { QuotasService } from './QuotasService'; +import { RootBootstrap } from './RootBootstrap'; import { ServerConfigEventHandler } from './ServerConfigEventHandler'; import { ServerConfigResource } from './ServerConfigResource'; import { ServerNodeService } from './ServerNodeService'; -import { ServerSettingsResolverService } from './ServerSettingsResolverService'; -import { ServerSettingsService } from './ServerSettingsService'; import { SessionActionService } from './SessionActionService'; import { SessionActivityService } from './SessionActivityService'; import { SessionDataResource } from './SessionDataResource'; @@ -27,7 +26,9 @@ import { SessionInfoEventHandler } from './SessionInfoEventHandler'; import { SessionPermissionEventHandler } from './SessionPermissionEventHandler'; import { SessionPermissionsResource } from './SessionPermissionsResource'; import { SessionResource } from './SessionResource'; -import { SessionSettingsService } from './SessionSettingsService'; +import { ServerSettingsManagerService } from './Settings/ServerSettingsManagerService'; +import { ServerSettingsResource } from './Settings/ServerSettingsResource'; +import { ServerSettingsService } from './Settings/ServerSettingsService'; import { WindowEventsService } from './WindowEventsService'; export const coreRootManifest: PluginManifest = { @@ -48,7 +49,6 @@ export const coreRootManifest: PluginManifest = { SessionExpireEventService, ServerNodeService, SessionResource, - SessionSettingsService, WindowEventsService, QuotasService, ServerConfigEventHandler, @@ -57,6 +57,8 @@ export const coreRootManifest: PluginManifest = { SessionActivityService, DataSynchronizationService, SessionPermissionEventHandler, - ServerSettingsResolverService, + ServerSettingsResource, + ServerSettingsManagerService, + RootBootstrap, ], }; diff --git a/webapp/packages/core-root/tsconfig.json b/webapp/packages/core-root/tsconfig.json index e13ad37489..cf23a1b3e1 100644 --- a/webapp/packages/core-root/tsconfig.json +++ b/webapp/packages/core-root/tsconfig.json @@ -20,6 +20,12 @@ { "path": "../core-executor/tsconfig.json" }, + { + "path": "../core-localization/tsconfig.json" + }, + { + "path": "../core-product/tsconfig.json" + }, { "path": "../core-resource/tsconfig.json" }, @@ -34,6 +40,9 @@ }, { "path": "../core-utils/tsconfig.json" + }, + { + "path": "../tests-runner/tsconfig.json" } ], "include": [ diff --git a/webapp/packages/core-routing/src/RouterService.ts b/webapp/packages/core-routing/src/RouterService.ts index a2df63a694..61d9e04218 100644 --- a/webapp/packages/core-routing/src/RouterService.ts +++ b/webapp/packages/core-routing/src/RouterService.ts @@ -67,13 +67,12 @@ export class RouterService extends Bootstrap { return this.router.subscribe(subscriber); } - register(): void | Promise {} - load(): void | Promise { + load(): void { this.start(); } reload(): void { - this.app.start(); + this.app.restart(); // this.router.navigate( // this.route, // this.params, diff --git a/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/getPropertyControlType.ts b/webapp/packages/core-sdk/src/getObjectPropertyType.ts similarity index 71% rename from webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/getPropertyControlType.ts rename to webapp/packages/core-sdk/src/getObjectPropertyType.ts index ef66e254ae..03ba754407 100644 --- a/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/getPropertyControlType.ts +++ b/webapp/packages/core-sdk/src/getObjectPropertyType.ts @@ -5,17 +5,17 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; +import type { ObjectPropertyInfo } from './sdk'; -export type ControlType = 'checkbox' | 'combobox' | 'link' | 'input' | 'textarea' | 'file'; +export type ObjectPropertyType = 'checkbox' | 'selector' | 'link' | 'input' | 'textarea' | 'file'; -export function getPropertyControlType(property: ObjectPropertyInfo): ControlType { +export function getObjectPropertyType(property: ObjectPropertyInfo): ObjectPropertyType { const dataType = property.dataType?.toLowerCase(); if (dataType === 'boolean') { return 'checkbox'; } else if (property.validValues && property.validValues.length > 0) { - return 'combobox'; + return 'selector'; } else if (property.features.includes('href')) { return 'link'; } else if (dataType === 'string' && property.length === 'MULTILINE') { diff --git a/webapp/packages/core-sdk/src/index.ts b/webapp/packages/core-sdk/src/index.ts index 4f112e88e0..5fb8c0f342 100644 --- a/webapp/packages/core-sdk/src/index.ts +++ b/webapp/packages/core-sdk/src/index.ts @@ -6,6 +6,7 @@ export * from './DetailsError'; export * from './EnvironmentService'; export * from './EServerErrorCode'; export * from './getGQLResponse'; +export * from './getObjectPropertyType'; export * from './GQLError'; export * from './GQLErrorCatcher'; export * from './GraphQLService'; diff --git a/webapp/packages/core-sdk/src/queries/authentication/updateUserPreferences.gql b/webapp/packages/core-sdk/src/queries/authentication/updateUserPreferences.gql new file mode 100644 index 0000000000..7f3ac3afb9 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/authentication/updateUserPreferences.gql @@ -0,0 +1,18 @@ +mutation updateUserPreferences( + $preferences: Object! + $includeMetaParameters: Boolean! + $includeConfigurationParameters: Boolean! + $customIncludeOriginDetails: Boolean! +) { + user: setUserPreferences(preferences: $preferences) { + userId + displayName + authRole + linkedAuthProviders + metaParameters @include(if: $includeMetaParameters) + configurationParameters @include(if: $includeConfigurationParameters) + authTokens { + ...AuthToken + } + } +} diff --git a/webapp/packages/core-sdk/src/queries/settings/getProductSettings.gql b/webapp/packages/core-sdk/src/queries/settings/getProductSettings.gql new file mode 100644 index 0000000000..e091e1f2be --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/settings/getProductSettings.gql @@ -0,0 +1,12 @@ +query getProductSettings { + settings: productSettings { + groups { + id + displayName + } + settings { + ...ObjectPropertyInfo + scopes + } + } +} diff --git a/webapp/packages/core-server-localization/src/ServerLocalizationService.ts b/webapp/packages/core-server-localization/src/ServerLocalizationService.ts index dae6b7c79d..df230ed926 100644 --- a/webapp/packages/core-server-localization/src/ServerLocalizationService.ts +++ b/webapp/packages/core-server-localization/src/ServerLocalizationService.ts @@ -11,7 +11,10 @@ import { ServerConfigResource } from '@cloudbeaver/core-root'; @injectable() export class ServerLocalizationService extends Dependency { - constructor(private readonly serverConfigResource: ServerConfigResource, private readonly localizationService: LocalizationService) { + constructor( + private readonly serverConfigResource: ServerConfigResource, + private readonly localizationService: LocalizationService, + ) { super(); this.serverConfigResource.onDataUpdate.addHandler(this.updateSupportedLocalization.bind(this)); @@ -24,7 +27,8 @@ export class ServerLocalizationService extends Dependency { this.localizationService.setSupportedLanguages( supportedLanguages.map(lang => ({ isoCode: lang.isoCode, - displayName: lang.displayName ?? lang.nativeName ?? lang.isoCode, + name: lang.displayName ?? lang.isoCode, + nativeName: lang.nativeName, })), ); } diff --git a/webapp/packages/core-session-localization/package.json b/webapp/packages/core-session-localization/package.json index 3d2302f1f3..fb4f008afa 100644 --- a/webapp/packages/core-session-localization/package.json +++ b/webapp/packages/core-session-localization/package.json @@ -20,7 +20,8 @@ "dependencies": { "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0" + "@cloudbeaver/core-root": "~0.1.0", + "@cloudbeaver/core-settings-localization": "~0.1.0" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/core-session-localization/src/SessionLocalizationService.ts b/webapp/packages/core-session-localization/src/SessionLocalizationService.ts index 1f45ab207e..b0da9ba63f 100644 --- a/webapp/packages/core-session-localization/src/SessionLocalizationService.ts +++ b/webapp/packages/core-session-localization/src/SessionLocalizationService.ts @@ -8,19 +8,24 @@ import { Dependency, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; import { SessionResource } from '@cloudbeaver/core-root'; +import { SettingsLocalizationService } from '@cloudbeaver/core-settings-localization'; @injectable() export class SessionLocalizationService extends Dependency { - constructor(private readonly sessionResource: SessionResource, private readonly localizationService: LocalizationService) { + constructor( + private readonly sessionResource: SessionResource, + private readonly localizationService: LocalizationService, + private readonly settingsLocalizationService: SettingsLocalizationService, + ) { super(); - this.sessionResource.beforeLoad.addHandler(() => { - this.sessionResource.setDefaultLocale(this.localizationService.currentLanguage); - }); this.sessionResource.onDataUpdate.addHandler(this.syncLanguage.bind(this)); - this.localizationService.onChange.addHandler(key => { - this.sessionResource.setDefaultLocale(key); - this.sessionResource.changeLanguage(key); + this.settingsLocalizationService.settingsProvider.onChange.addHandler(data => { + if (data.key === 'core.localization.language') { + this.sessionResource.changeLanguage(data.value).catch(exception => { + console.error(exception); + }); + } }); } @@ -28,7 +33,7 @@ export class SessionLocalizationService extends Dependency { const session = this.sessionResource.data; if (session) { - await this.localizationService.setLocale(session.locale); + await this.localizationService.changeLocale(session.locale); } } } diff --git a/webapp/packages/core-session-localization/tsconfig.json b/webapp/packages/core-session-localization/tsconfig.json index 8207a55404..f2537f7ca5 100644 --- a/webapp/packages/core-session-localization/tsconfig.json +++ b/webapp/packages/core-session-localization/tsconfig.json @@ -14,6 +14,9 @@ }, { "path": "../core-root/tsconfig.json" + }, + { + "path": "../core-settings-localization/tsconfig.json" } ], "include": [ diff --git a/webapp/packages/core-settings-localization/package.json b/webapp/packages/core-settings-localization/package.json index 61c7fafc37..f9b51df6ce 100644 --- a/webapp/packages/core-settings-localization/package.json +++ b/webapp/packages/core-settings-localization/package.json @@ -20,18 +20,14 @@ "dependencies": { "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "mobx": "^6.12.0" }, "peerDependencies": {}, "devDependencies": { - "@cloudbeaver/core-browser": "~0.1.0", "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-product": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", diff --git a/webapp/packages/core-settings-localization/src/LocalizationSettingsManagerService.ts b/webapp/packages/core-settings-localization/src/LocalizationSettingsManagerService.ts index a430b6e3c4..76fbf66cae 100644 --- a/webapp/packages/core-settings-localization/src/LocalizationSettingsManagerService.ts +++ b/webapp/packages/core-settings-localization/src/LocalizationSettingsManagerService.ts @@ -7,7 +7,7 @@ */ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { LocalizationService } from '@cloudbeaver/core-localization'; -import { ESettingsValueType, INTERFACE_SETTINGS_GROUP, SettingsManagerService } from '@cloudbeaver/core-plugin'; +import { ESettingsValueType, INTERFACE_SETTINGS_GROUP, SettingsManagerService } from '@cloudbeaver/core-settings'; import { SettingsLocalizationService } from './SettingsLocalizationService'; @@ -26,16 +26,19 @@ export class LocalizationSettingsManagerService extends Bootstrap { } private registerSettings() { - this.settingsManagerService.registerSettings(this.settingsLocalizationService.pluginSettings, () => [ + this.settingsManagerService.registerSettings(this.settingsLocalizationService.settingsProvider, () => [ { group: INTERFACE_SETTINGS_GROUP, - key: 'defaultLanguage', - name: 'core_settings_localization_settings_default_locale_label', - description: 'core_settings_localization_settings_default_locale_description', + key: 'core.localization.language', + access: { + scope: ['server', 'client'], + }, + name: 'core_settings_localization_settings_locale_label', + description: 'core_settings_localization_settings_locale_description', type: ESettingsValueType.Select, options: this.localizationService.supportedLanguages.map(language => ({ - id: language.isoCode, - name: language.displayName, + value: language.isoCode, + name: language.nativeName ?? language.name, })), }, ]); diff --git a/webapp/packages/core-settings-localization/src/SettingsLocalizationBootstrap.ts b/webapp/packages/core-settings-localization/src/SettingsLocalizationBootstrap.ts new file mode 100644 index 0000000000..12b06d6eae --- /dev/null +++ b/webapp/packages/core-settings-localization/src/SettingsLocalizationBootstrap.ts @@ -0,0 +1,50 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { type IReactionDisposer, reaction } from 'mobx'; + +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; + +import { SettingsLocalizationService } from './SettingsLocalizationService'; + +@injectable() +export class SettingsLocalizationBootstrap extends Bootstrap { + private reactionDisposer: IReactionDisposer | null; + constructor( + private readonly settingsLocalizationService: SettingsLocalizationService, + private readonly localizationService: LocalizationService, + ) { + super(); + this.reactionDisposer = null; + this.changeLanguage = this.changeLanguage.bind(this); + } + + register(): void { + this.localizationService.onChange.addHandler(this.changeLanguage); + this.reactionDisposer = reaction( + () => this.settingsLocalizationService.language, + defaultLanguage => { + this.localizationService.setLanguage(defaultLanguage); + }, + { + fireImmediately: true, + }, + ); + } + + dispose(): void { + this.localizationService.onChange.removeHandler(this.changeLanguage); + if (this.reactionDisposer) { + this.reactionDisposer(); + } + } + + private async changeLanguage(language: string) { + await this.settingsLocalizationService.changeLanguage(language); + } +} diff --git a/webapp/packages/core-settings-localization/src/SettingsLocalizationService.test.ts b/webapp/packages/core-settings-localization/src/SettingsLocalizationService.test.ts index 2f21b81772..afda73df98 100644 --- a/webapp/packages/core-settings-localization/src/SettingsLocalizationService.test.ts +++ b/webapp/packages/core-settings-localization/src/SettingsLocalizationService.test.ts @@ -7,58 +7,48 @@ */ import '@testing-library/jest-dom'; -import { coreBrowserManifest } from '@cloudbeaver/core-browser'; import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; +import '@cloudbeaver/core-root/dist/__custom_mocks__/expectWebsocketClosedMessage'; import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; import { coreSDKManifest } from '@cloudbeaver/core-sdk'; import { coreSettingsManifest } from '@cloudbeaver/core-settings'; +import { + expectDeprecatedSettingMessage, + expectNoDeprecatedSettingMessage, +} from '@cloudbeaver/core-settings/dist/__custom_mocks__/expectDeprecatedSettingMessage'; import { createApp } from '@cloudbeaver/tests-runner'; import { coreSettingsLocalizationManifest } from './manifest'; -import { type ILocalizationSettings, SettingsLocalizationService } from './SettingsLocalizationService'; +import { SettingsLocalizationService } from './SettingsLocalizationService'; const endpoint = createGQLEndpoint(); +const server = mockGraphQL(...mockAppInit(endpoint)); const app = createApp( coreSettingsLocalizationManifest, - corePluginManifest, + coreClientActivityManifest, coreSettingsManifest, coreLocalizationManifest, - coreProductManifest, coreRootManifest, coreSDKManifest, - coreBrowserManifest, - coreClientActivityManifest, ); -const server = mockGraphQL(...mockAppInit(endpoint)); - -beforeAll(() => app.init()); - -const testValue = 'es'; -const testValueB = 'te'; +const deprecatedValue = 'es'; +const value = 'te'; const deprecatedSettings = { - core: { - user: { - defaultLanguage: testValueB, - } as ILocalizationSettings, - }, + 'core.user.defaultLanguage': deprecatedValue, + 'core.localization.defaultLanguage': deprecatedValue, + 'app.defaultLanguage': deprecatedValue, }; const newSettings = { - core: { - ...deprecatedSettings, - localization: { - defaultLanguage: testValue, - } as ILocalizationSettings, - }, + ...deprecatedSettings, + 'core.localization.language': value, }; test('New settings override deprecated settings', async () => { @@ -69,7 +59,8 @@ test('New settings override deprecated settings', async () => { await config.refresh(); - expect(settings.pluginSettings.getValue('defaultLanguage')).toBe(testValue); + expect(settings.language).toBe(value); + expectNoDeprecatedSettingMessage(); }); test('Deprecated settings are used if new settings are not defined', async () => { @@ -80,5 +71,6 @@ test('Deprecated settings are used if new settings are not defined', async () => await config.refresh(); - expect(settings.pluginSettings.getValue('defaultLanguage')).toBe(testValueB); + expect(settings.language).toBe(deprecatedValue); + expectDeprecatedSettingMessage(); }); diff --git a/webapp/packages/core-settings-localization/src/SettingsLocalizationService.ts b/webapp/packages/core-settings-localization/src/SettingsLocalizationService.ts index c109f83250..eddae4f353 100644 --- a/webapp/packages/core-settings-localization/src/SettingsLocalizationService.ts +++ b/webapp/packages/core-settings-localization/src/SettingsLocalizationService.ts @@ -5,53 +5,57 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { computed, makeObservable, reaction } from 'mobx'; +import { computed, makeObservable } from 'mobx'; import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { DEFAULT_LOCALE, LocalizationService } from '@cloudbeaver/core-localization'; -import { createSettingsAliasResolver, PluginManagerService, PluginSettings } from '@cloudbeaver/core-plugin'; -import { ServerSettingsResolverService, ServerSettingsService } from '@cloudbeaver/core-root'; +import { DEFAULT_LOCALE } from '@cloudbeaver/core-localization'; +import { + createSettingsAliasResolver, + ROOT_SETTINGS_LAYER, + SettingsProvider, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; import { schema } from '@cloudbeaver/core-utils'; const settingsSchema = schema.object({ - defaultLanguage: schema.string().default(DEFAULT_LOCALE.isoCode), + 'core.localization.language': schema.string().default(DEFAULT_LOCALE.isoCode), }); export type ILocalizationSettings = schema.infer; @injectable() export class SettingsLocalizationService extends Dependency { - get defaultLanguage(): string { - return this.pluginSettings.getValue('defaultLanguage'); + get language(): string { + return this.settingsProvider.getValue('core.localization.language'); } - readonly pluginSettings: PluginSettings; + readonly settingsProvider: SettingsProvider; constructor( - private readonly pluginManagerService: PluginManagerService, - private readonly localizationService: LocalizationService, - private readonly serverSettingsService: ServerSettingsService, - private readonly serverSettingsResolverService: ServerSettingsResolverService, + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsResolverService: SettingsResolverService, ) { super(); - this.pluginSettings = this.pluginManagerService.createSettings('localization', 'core', settingsSchema); + this.settingsProvider = this.settingsProviderService.createSettings(settingsSchema); - this.serverSettingsResolverService.addResolver( + this.settingsResolverService.addResolver( + ROOT_SETTINGS_LAYER, /** @deprecated Use settings instead, will be removed in 23.0.0 */ - createSettingsAliasResolver(this.serverSettingsService, this.pluginSettings, 'core.user'), - ); - reaction( - () => this.defaultLanguage, - defaultLanguage => { - this.localizationService.setDefaultLanguage(defaultLanguage); - }, - { - fireImmediately: true, - }, + createSettingsAliasResolver(this.settingsResolverService, this.settingsProvider, { 'core.localization.language': 'core.user.defaultLanguage' }), + createSettingsAliasResolver(this.settingsResolverService, this.settingsProvider, { + 'core.localization.language': 'core.localization.defaultLanguage', + }), + createSettingsAliasResolver(this.settingsResolverService, this.settingsProvider, { 'core.localization.language': 'app.defaultLanguage' }), ); makeObservable(this, { - defaultLanguage: computed, + language: computed, }); } + + async changeLanguage(language: string) { + this.settingsProvider.setValue('core.localization.language', language); + await this.settingsProvider.save(); + } } diff --git a/webapp/packages/core-settings-localization/src/index.ts b/webapp/packages/core-settings-localization/src/index.ts index ab5e0e1f96..7271bacf06 100644 --- a/webapp/packages/core-settings-localization/src/index.ts +++ b/webapp/packages/core-settings-localization/src/index.ts @@ -1 +1,2 @@ export * from './manifest'; +export * from './SettingsLocalizationService'; diff --git a/webapp/packages/core-settings-localization/src/locales/en.ts b/webapp/packages/core-settings-localization/src/locales/en.ts index 0a7deb7a66..2c9c01363c 100644 --- a/webapp/packages/core-settings-localization/src/locales/en.ts +++ b/webapp/packages/core-settings-localization/src/locales/en.ts @@ -1,4 +1,4 @@ export default [ - ['core_settings_localization_settings_default_locale_label', 'Default Language'], - ['core_settings_localization_settings_default_locale_description', 'Default language for the application'], + ['core_settings_localization_settings_locale_label', 'Language'], + ['core_settings_localization_settings_locale_description', 'Language for the application'], ]; diff --git a/webapp/packages/core-settings-localization/src/locales/it.ts b/webapp/packages/core-settings-localization/src/locales/it.ts index 0a7deb7a66..2c9c01363c 100644 --- a/webapp/packages/core-settings-localization/src/locales/it.ts +++ b/webapp/packages/core-settings-localization/src/locales/it.ts @@ -1,4 +1,4 @@ export default [ - ['core_settings_localization_settings_default_locale_label', 'Default Language'], - ['core_settings_localization_settings_default_locale_description', 'Default language for the application'], + ['core_settings_localization_settings_locale_label', 'Language'], + ['core_settings_localization_settings_locale_description', 'Language for the application'], ]; diff --git a/webapp/packages/core-settings-localization/src/locales/ru.ts b/webapp/packages/core-settings-localization/src/locales/ru.ts index 6a40fa6e16..0dccb56bbe 100644 --- a/webapp/packages/core-settings-localization/src/locales/ru.ts +++ b/webapp/packages/core-settings-localization/src/locales/ru.ts @@ -1,4 +1,4 @@ export default [ - ['core_settings_localization_settings_default_locale_label', 'Язык по умолчанию'], - ['core_settings_localization_settings_default_locale_description', 'Язык по умолчанию для приложения'], + ['core_settings_localization_settings_locale_label', 'Язык'], + ['core_settings_localization_settings_locale_description', 'Язык для приложения'], ]; diff --git a/webapp/packages/core-settings-localization/src/locales/zh.ts b/webapp/packages/core-settings-localization/src/locales/zh.ts index 0a7deb7a66..2c9c01363c 100644 --- a/webapp/packages/core-settings-localization/src/locales/zh.ts +++ b/webapp/packages/core-settings-localization/src/locales/zh.ts @@ -1,4 +1,4 @@ export default [ - ['core_settings_localization_settings_default_locale_label', 'Default Language'], - ['core_settings_localization_settings_default_locale_description', 'Default language for the application'], + ['core_settings_localization_settings_locale_label', 'Language'], + ['core_settings_localization_settings_locale_description', 'Language for the application'], ]; diff --git a/webapp/packages/core-settings-localization/src/manifest.ts b/webapp/packages/core-settings-localization/src/manifest.ts index b8bffa9de4..23b9686358 100644 --- a/webapp/packages/core-settings-localization/src/manifest.ts +++ b/webapp/packages/core-settings-localization/src/manifest.ts @@ -9,6 +9,7 @@ import type { PluginManifest } from '@cloudbeaver/core-di'; import { LocaleService } from './LocaleService'; import { LocalizationSettingsManagerService } from './LocalizationSettingsManagerService'; +import { SettingsLocalizationBootstrap } from './SettingsLocalizationBootstrap'; import { SettingsLocalizationService } from './SettingsLocalizationService'; export const coreSettingsLocalizationManifest: PluginManifest = { @@ -16,5 +17,5 @@ export const coreSettingsLocalizationManifest: PluginManifest = { name: 'Core Settings Localization', }, - providers: [SettingsLocalizationService, LocalizationSettingsManagerService, LocaleService], + providers: [SettingsLocalizationService, LocalizationSettingsManagerService, LocaleService, SettingsLocalizationBootstrap], }; diff --git a/webapp/packages/core-settings-localization/tsconfig.json b/webapp/packages/core-settings-localization/tsconfig.json index 2cce098093..b0d5cdab62 100644 --- a/webapp/packages/core-settings-localization/tsconfig.json +++ b/webapp/packages/core-settings-localization/tsconfig.json @@ -6,9 +6,6 @@ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "references": [ - { - "path": "../core-browser/tsconfig.json" - }, { "path": "../core-client-activity/tsconfig.json" }, @@ -21,18 +18,6 @@ { "path": "../core-localization/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-product/tsconfig.json" - }, - { - "path": "../core-root/tsconfig.json" - }, { "path": "../core-root/tsconfig.json" }, @@ -42,6 +27,9 @@ { "path": "../core-settings/tsconfig.json" }, + { + "path": "../core-settings/tsconfig.json" + }, { "path": "../core-utils/tsconfig.json" }, diff --git a/webapp/packages/core-plugin/.gitignore b/webapp/packages/core-settings-user/.gitignore similarity index 100% rename from webapp/packages/core-plugin/.gitignore rename to webapp/packages/core-settings-user/.gitignore diff --git a/webapp/packages/core-settings-user/package.json b/webapp/packages/core-settings-user/package.json new file mode 100644 index 0000000000..e6bc45f1dc --- /dev/null +++ b/webapp/packages/core-settings-user/package.json @@ -0,0 +1,34 @@ +{ + "name": "@cloudbeaver/core-settings-user", + "sideEffects": [ + "src/**/*.css", + "src/**/*.scss", + "public/**/*" + ], + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "main": "dist/index.js", + "scripts": { + "build": "tsc -b", + "lint": "eslint ./src/ --ext .ts,.tsx", + "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", + "test": "yarn build && core-cli-test", + "validate-dependencies": "core-cli-validate-dependencies", + "update-ts-references": "rimraf --glob dist && typescript-resolve-references" + }, + "dependencies": { + "@cloudbeaver/core-authentication": "~0.1.0", + "@cloudbeaver/core-di": "~0.1.0", + "@cloudbeaver/core-localization": "~0.1.0", + "@cloudbeaver/core-root": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", + "@cloudbeaver/core-storage": "~0.1.0", + "@cloudbeaver/core-utils": "~0.1.0", + "mobx": "^6.12.0" + }, + "peerDependencies": {}, + "devDependencies": { + "typescript": "^5.3.3" + } +} diff --git a/webapp/packages/core-settings-user/src/LocaleService.ts b/webapp/packages/core-settings-user/src/LocaleService.ts new file mode 100644 index 0000000000..edeb82d255 --- /dev/null +++ b/webapp/packages/core-settings-user/src/LocaleService.ts @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { 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/core-settings-user/src/SettingsUserBootstrap.ts b/webapp/packages/core-settings-user/src/SettingsUserBootstrap.ts new file mode 100644 index 0000000000..7529b2c094 --- /dev/null +++ b/webapp/packages/core-settings-user/src/SettingsUserBootstrap.ts @@ -0,0 +1,26 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { SESSION_SETTINGS_LAYER } from '@cloudbeaver/core-root'; +import { SettingsResolverService } from '@cloudbeaver/core-settings'; + +import { UserSettingsService } from './UserSettingsService'; + +@injectable() +export class SettingsUserBootstrap extends Bootstrap { + constructor( + private readonly settingsResolverService: SettingsResolverService, + private readonly userSettingsService: UserSettingsService, + ) { + super(); + } + + register(): void { + this.settingsResolverService.addResolver(SESSION_SETTINGS_LAYER, this.userSettingsService); + } +} diff --git a/webapp/packages/core-settings-user/src/UserSettingsService.ts b/webapp/packages/core-settings-user/src/UserSettingsService.ts new file mode 100644 index 0000000000..697dd3d5fa --- /dev/null +++ b/webapp/packages/core-settings-user/src/UserSettingsService.ts @@ -0,0 +1,117 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { makeObservable, observable } from 'mobx'; + +import { UserInfoResource } from '@cloudbeaver/core-authentication'; +import { injectable } from '@cloudbeaver/core-di'; +import { SettingsSource } from '@cloudbeaver/core-settings'; +import { StorageService } from '@cloudbeaver/core-storage'; + +@injectable() +export class UserSettingsService extends SettingsSource { + private readonly settings: Map; + private lastConfig: any; + private localSettings: Map; + + constructor( + private readonly userInfoResource: UserInfoResource, + private readonly storageService: StorageService, + ) { + super(); + this.lastConfig = null; + this.settings = new Map(); + this.localSettings = new Map(); + + this.userInfoResource.onDataUpdate.addHandler(this.refreshConfig.bind(this)); + this.userInfoResource.onUserChange.addHandler(this.refreshConfig.bind(this)); + + makeObservable(this, { + settings: observable.shallow, + localSettings: observable, + }); + + this.storageService.registerSettings( + 'user_settings', + this.localSettings, + () => new Map(), + undefined, + () => this.refreshConfig(), + ); + } + + has(key: any): boolean { + return this.getSource().has(key) || super.has(key); + } + + isReadOnly(key: any): boolean { + return false; + } + + getValue(key: any): any { + return this.getSource().get(key); + } + + clear(): void { + this.update(() => { + super.clear(); + this.settings.clear(); + }); + } + + async save() { + if (this.userInfoResource.data) { + await this.userInfoResource.updatePreferences(Object.fromEntries(this.changes)); + } else { + this.update(() => { + this.changes.forEach((value, key) => { + this.localSettings.set(key, value); + }); + this.changes.clear(); + }); + } + } + + resetChanges() { + super.clear(); + } + + protected getSnapshot() { + return Object.fromEntries(this.getSource()); + } + + private refreshConfig() { + this.update(() => { + if (!this.userInfoResource.data) { + this.clear(); + this.lastConfig = null; + return; + } + + if (this.userInfoResource.data.configurationParameters !== this.lastConfig) { + this.clear(); + this.localSettings.clear(); + this.lastConfig = this.userInfoResource.data.configurationParameters; + + if (this.lastConfig && typeof this.lastConfig === 'object') { + for (const [key, value] of Object.entries(this.lastConfig)) { + this.settings.set(key, value); + this.localSettings.set(key, value); + } + } + } + }); + } + + private getSource() { + if (this.userInfoResource.data) { + return this.settings; + } + + return this.localSettings; + } +} diff --git a/webapp/packages/core-settings-user/src/index.ts b/webapp/packages/core-settings-user/src/index.ts new file mode 100644 index 0000000000..0be39ee6ba --- /dev/null +++ b/webapp/packages/core-settings-user/src/index.ts @@ -0,0 +1,3 @@ +export * from './manifest'; + +export * from './UserSettingsService'; diff --git a/webapp/packages/core-settings-user/src/locales/en.ts b/webapp/packages/core-settings-user/src/locales/en.ts new file mode 100644 index 0000000000..d6d1738de6 --- /dev/null +++ b/webapp/packages/core-settings-user/src/locales/en.ts @@ -0,0 +1 @@ +export default []; diff --git a/webapp/packages/core-settings-user/src/locales/it.ts b/webapp/packages/core-settings-user/src/locales/it.ts new file mode 100644 index 0000000000..d6d1738de6 --- /dev/null +++ b/webapp/packages/core-settings-user/src/locales/it.ts @@ -0,0 +1 @@ +export default []; diff --git a/webapp/packages/core-settings-user/src/locales/ru.ts b/webapp/packages/core-settings-user/src/locales/ru.ts new file mode 100644 index 0000000000..d6d1738de6 --- /dev/null +++ b/webapp/packages/core-settings-user/src/locales/ru.ts @@ -0,0 +1 @@ +export default []; diff --git a/webapp/packages/core-settings-user/src/locales/zh.ts b/webapp/packages/core-settings-user/src/locales/zh.ts new file mode 100644 index 0000000000..d6d1738de6 --- /dev/null +++ b/webapp/packages/core-settings-user/src/locales/zh.ts @@ -0,0 +1 @@ +export default []; diff --git a/webapp/packages/core-plugin/src/manifest.ts b/webapp/packages/core-settings-user/src/manifest.ts similarity index 53% rename from webapp/packages/core-plugin/src/manifest.ts rename to webapp/packages/core-settings-user/src/manifest.ts index 6d9abe339b..98c4b2a92d 100644 --- a/webapp/packages/core-plugin/src/manifest.ts +++ b/webapp/packages/core-settings-user/src/manifest.ts @@ -8,13 +8,13 @@ import type { PluginManifest } from '@cloudbeaver/core-di'; import { LocaleService } from './LocaleService'; -import { PluginManagerService } from './PluginManagerService'; -import { SettingsManagerService } from './SettingsManager/SettingsManagerService'; +import { SettingsUserBootstrap } from './SettingsUserBootstrap'; +import { UserSettingsService } from './UserSettingsService'; -export const corePluginManifest: PluginManifest = { +export const coreSettingsUserManifest: PluginManifest = { info: { - name: 'Core Plugin', + name: 'Core User Settings', }, - providers: [PluginManagerService, SettingsManagerService, LocaleService], + providers: [LocaleService, UserSettingsService, SettingsUserBootstrap], }; diff --git a/webapp/packages/core-plugin/tsconfig.json b/webapp/packages/core-settings-user/tsconfig.json similarity index 79% rename from webapp/packages/core-plugin/tsconfig.json rename to webapp/packages/core-settings-user/tsconfig.json index 4b5a0a1eae..5c044419c0 100644 --- a/webapp/packages/core-plugin/tsconfig.json +++ b/webapp/packages/core-settings-user/tsconfig.json @@ -1,11 +1,19 @@ { "extends": "../../tsconfig.base.json", + "exclude": [ + "**/node_modules", + "lib/**/*", + "dist/**/*" + ], "compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "references": [ + { + "path": "../core-authentication/tsconfig.json" + }, { "path": "../core-di/tsconfig.json" }, @@ -13,11 +21,14 @@ "path": "../core-localization/tsconfig.json" }, { - "path": "../core-product/tsconfig.json" + "path": "../core-root/tsconfig.json" }, { "path": "../core-settings/tsconfig.json" }, + { + "path": "../core-storage/tsconfig.json" + }, { "path": "../core-utils/tsconfig.json" } @@ -28,10 +39,5 @@ "src/**/*.json", "src/**/*.css", "src/**/*.scss" - ], - "exclude": [ - "**/node_modules", - "lib/**/*", - "dist/**/*" ] } diff --git a/webapp/packages/core-settings/package.json b/webapp/packages/core-settings/package.json index 692e480dce..859f828324 100644 --- a/webapp/packages/core-settings/package.json +++ b/webapp/packages/core-settings/package.json @@ -13,17 +13,23 @@ "build": "tsc -b", "lint": "eslint ./src/ --ext .ts,.tsx", "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", + "test": "yarn build && core-cli-test", "validate-dependencies": "core-cli-validate-dependencies", "update-ts-references": "rimraf --glob dist && typescript-resolve-references" }, "dependencies": { - "@cloudbeaver/core-browser": "~0.1.0", "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-executor": "~0.1.0", + "@cloudbeaver/core-localization": "~0.1.0", + "@cloudbeaver/core-utils": "~0.1.0", "mobx": "^6.12.0" }, "peerDependencies": {}, "devDependencies": { + "@cloudbeaver/core-executor": "~0.1.0", + "@cloudbeaver/tests-runner": "~0.1.0", + "@testing-library/jest-dom": "^6.4.2", + "@types/jest": "^29.5.12", "typescript": "^5.3.3" } } diff --git a/webapp/packages/core-settings/src/ISettingsResolverSource.ts b/webapp/packages/core-settings/src/ISettingsResolverSource.ts index f25803b1b1..eb21ef7a21 100644 --- a/webapp/packages/core-settings/src/ISettingsResolverSource.ts +++ b/webapp/packages/core-settings/src/ISettingsResolverSource.ts @@ -6,10 +6,11 @@ * you may not use this file except in compliance with the License. */ import type { ISettingsSource } from './ISettingsSource'; +import type { ISettingsLayer } from './SettingsLayer'; export interface ISettingsResolverSource extends ISettingsSource { - hasResolver: (resolver: ISettingsSource) => boolean; - addResolver: (...resolvers: ISettingsSource[]) => void; - removeResolver: (resolver: ISettingsSource) => void; + hasResolver: (layer: ISettingsLayer, resolver: ISettingsSource) => boolean; + addResolver: (layer: ISettingsLayer, ...resolvers: ISettingsSource[]) => void; + removeResolver: (layer: ISettingsLayer, resolver: ISettingsSource) => void; clearResolvers: () => void; } diff --git a/webapp/packages/core-settings/src/ISettingsSource.ts b/webapp/packages/core-settings/src/ISettingsSource.ts index 072a4620a9..f81804d1b2 100644 --- a/webapp/packages/core-settings/src/ISettingsSource.ts +++ b/webapp/packages/core-settings/src/ISettingsSource.ts @@ -5,12 +5,21 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import type { ISyncExecutor } from '@cloudbeaver/core-executor'; + +export interface ISettingChangeData { + readonly key: T; + readonly value: any; +} export interface ISettingsSource { + readonly onChange: ISyncExecutor; has: (key: any) => boolean; + isEdited: (key?: any) => boolean; isReadOnly: (key: any) => boolean; getValue: (key: any) => any | undefined; - getDefaultValue: (key: any) => any | undefined; + getEditedValue: (key: any) => any | undefined; setValue: (key: any, value: any) => void; + save: () => Promise; clear: () => void; } diff --git a/webapp/packages/core-plugin/src/LocaleService.ts b/webapp/packages/core-settings/src/LocaleService.ts similarity index 100% rename from webapp/packages/core-plugin/src/LocaleService.ts rename to webapp/packages/core-settings/src/LocaleService.ts diff --git a/webapp/packages/core-settings/src/SettingsLayer.ts b/webapp/packages/core-settings/src/SettingsLayer.ts new file mode 100644 index 0000000000..1252b9a23b --- /dev/null +++ b/webapp/packages/core-settings/src/SettingsLayer.ts @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export interface ISettingsLayer { + parent: ISettingsLayer | null; + level: number; + name: string; +} + +class SettingsLayer implements ISettingsLayer { + parent: ISettingsLayer | null; + get level(): number { + if (!this.parent) { + return 0; + } + return this.parent.level + 1; + } + name: string; + + constructor(parent: ISettingsLayer | null, name: string) { + this.parent = parent; + this.name = name; + } +} + +export function createSettingsLayer(parent: ISettingsLayer, name: string): ISettingsLayer { + return new SettingsLayer(parent, name); +} + +export const ROOT_SETTINGS_LAYER = new SettingsLayer(null, 'root'); diff --git a/webapp/packages/core-plugin/src/SettingsManager/ESettingsValueType.ts b/webapp/packages/core-settings/src/SettingsManager/ESettingsValueType.ts similarity index 100% rename from webapp/packages/core-plugin/src/SettingsManager/ESettingsValueType.ts rename to webapp/packages/core-settings/src/SettingsManager/ESettingsValueType.ts diff --git a/webapp/packages/core-plugin/src/SettingsManager/ISettingDescription.ts b/webapp/packages/core-settings/src/SettingsManager/ISettingDescription.ts similarity index 70% rename from webapp/packages/core-plugin/src/SettingsManager/ISettingDescription.ts rename to webapp/packages/core-settings/src/SettingsManager/ISettingDescription.ts index 29a664a142..6bf0e1d42b 100644 --- a/webapp/packages/core-plugin/src/SettingsManager/ISettingDescription.ts +++ b/webapp/packages/core-settings/src/SettingsManager/ISettingDescription.ts @@ -6,18 +6,22 @@ * you may not use this file except in compliance with the License. */ import type { TLocalizationToken } from '@cloudbeaver/core-localization'; -import type { schema } from '@cloudbeaver/core-utils'; import type { ESettingsValueType } from './ESettingsValueType'; import type { SettingsGroup } from './SettingsGroup'; +export interface ISettingAccess { + scope: string[]; +} + export interface ISettingOptions { - id: string; + value: string; name: TLocalizationToken; } export interface ISettingDescription { key: keyof T; + access: ISettingAccess; type: ESettingsValueType; @@ -27,9 +31,5 @@ export interface ISettingDescription { options?: ISettingOptions[]; } -export interface ISettingDescriptionWithScope> extends ISettingDescription { - scope: string; - schema: schema.AnyZodObject; -} - -export type SettingsDescriptionGetter = () => ISettingDescription[]; +export type SettingDescriptionTransformer = (setting: ISettingDescription) => ISettingDescription | null; +export type SettingsDescriptionGetter = () => ReadonlyArray>; diff --git a/webapp/packages/core-settings/src/SettingsManager/SettingsGroup.ts b/webapp/packages/core-settings/src/SettingsManager/SettingsGroup.ts new file mode 100644 index 0000000000..15528e1f7a --- /dev/null +++ b/webapp/packages/core-settings/src/SettingsManager/SettingsGroup.ts @@ -0,0 +1,67 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { makeObservable, observable } from 'mobx'; + +import { uuid } from '@cloudbeaver/core-utils'; + +export class SettingsGroup { + get level(): number { + return (this.parent?.level ?? -1) + 1; + } + readonly id: string; + get subGroups(): ReadonlyArray { + return this.subGroupsData; + } + + protected groups: Map; + private subGroupsData: SettingsGroup[]; + + constructor( + readonly name: string, + readonly parent?: SettingsGroup, + ) { + this.id = uuid(); + this.subGroupsData = []; + this.groups = parent?.groups || new Map(); + this.groups.set(this.id, this); + + makeObservable(this, { + subGroupsData: observable.shallow, + groups: observable.shallow, + }); + } + + has(id: string): boolean { + return this.groups.has(id); + } + + get(id: string): SettingsGroup | undefined { + return this.groups.get(id); + } + + createSubGroup(name: string): SettingsGroup { + const subGroup = new SettingsGroup(name, this); + + this.subGroupsData.push(subGroup); + + return subGroup; + } + + deleteSubGroup(id: string): boolean { + const index = this.subGroupsData.findIndex(group => group.id === id); + + if (index === -1) { + return false; + } + + this.subGroupsData.splice(index, 1); + this.groups.delete(id); + + return true; + } +} diff --git a/webapp/packages/core-plugin/src/SettingsManager/SettingsGroups/INTERFACE_SETTINGS_GROUP.ts b/webapp/packages/core-settings/src/SettingsManager/SettingsGroups/INTERFACE_SETTINGS_GROUP.ts similarity index 100% rename from webapp/packages/core-plugin/src/SettingsManager/SettingsGroups/INTERFACE_SETTINGS_GROUP.ts rename to webapp/packages/core-settings/src/SettingsManager/SettingsGroups/INTERFACE_SETTINGS_GROUP.ts diff --git a/webapp/packages/core-plugin/src/SettingsManager/SettingsGroups/ROOT_SETTINGS_GROUP.ts b/webapp/packages/core-settings/src/SettingsManager/SettingsGroups/ROOT_SETTINGS_GROUP.ts similarity index 100% rename from webapp/packages/core-plugin/src/SettingsManager/SettingsGroups/ROOT_SETTINGS_GROUP.ts rename to webapp/packages/core-settings/src/SettingsManager/SettingsGroups/ROOT_SETTINGS_GROUP.ts diff --git a/webapp/packages/core-settings/src/SettingsManager/SettingsManagerService.ts b/webapp/packages/core-settings/src/SettingsManager/SettingsManagerService.ts new file mode 100644 index 0000000000..9912eaae84 --- /dev/null +++ b/webapp/packages/core-settings/src/SettingsManager/SettingsManagerService.ts @@ -0,0 +1,53 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { computed, makeObservable, observable } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import type { ILoadableState, schema } from '@cloudbeaver/core-utils'; + +import type { SettingsProvider } from '../SettingsProvider'; +import type { ISettingDescription, SettingsDescriptionGetter } from './ISettingDescription'; + +interface SettingsProviderItem { + settingsGetter: SettingsDescriptionGetter>; + loaders?: ReadonlyArray; +} + +@injectable() +export class SettingsManagerService { + get activeSettings(): ReadonlyArray> { + return this.settings.reduce[]>((acc, setting) => [...acc, ...setting.settingsGetter()], []); + } + + get loaders(): ReadonlyArray { + return this.settings.flatMap(setting => setting.loaders || []); + } + + private settings: SettingsProviderItem[]; + + constructor() { + this.settings = []; + + makeObservable(this, { + activeSettings: computed, + loaders: computed, + settings: observable.shallow, + }); + } + + registerSettings( + provider: SettingsProvider, + settingsGetter: SettingsDescriptionGetter>, + loaders?: ReadonlyArray, + ) { + this.settings.push({ + settingsGetter, + loaders, + }); + } +} diff --git a/webapp/packages/core-settings/src/SettingsProvider.ts b/webapp/packages/core-settings/src/SettingsProvider.ts new file mode 100644 index 0000000000..f33028460e --- /dev/null +++ b/webapp/packages/core-settings/src/SettingsProvider.ts @@ -0,0 +1,79 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import type { schema } from '@cloudbeaver/core-utils'; + +import type { ISettingChangeData, ISettingsSource } from './ISettingsSource'; + +export class SettingsProvider implements ISettingsSource { + readonly onChange: ISyncExecutor>>; + constructor( + private readonly source: ISettingsSource, + readonly schema: TSchema, + ) { + this.onChange = new SyncExecutor(); + source.onChange.next( + this.onChange, + data => data, + data => data.key in this.schema.shape, + ); + } + + isReadOnly>(key: TKey): boolean { + return this.source.isReadOnly(key) || false; + } + + isEdited(key?: any): boolean { + return this.source.isEdited(key); + } + + has>(key: TKey): boolean { + return true; + } + + isValueDefault>(key: TKey): boolean { + return !this.source.has(key); + } + + getEditedValue>(key: TKey): schema.infer[TKey] { + return this.getValue(key); + } + + getValue>(key: TKey): schema.infer[TKey] { + let value = undefined; + + if (this.source.has(key)) { + value = this.source.getValue(key); + } + + if (key in this.schema.shape) { + const schema = this.schema.shape[key as any]; + const result = schema.safeParse(value); + + if (result.success) { + return result.data; + } + + return schema.parse(undefined); + } + + return value; + } + + setValue>(key: TKey, value: schema.infer[TKey]): void { + this.source.setValue(key, value); + } + + clear(): void { + this.source.clear(); + } + + async save(): Promise { + await this.source.save(); + } +} diff --git a/webapp/packages/core-settings/src/SettingsProviderService.ts b/webapp/packages/core-settings/src/SettingsProviderService.ts new file mode 100644 index 0000000000..0d21eadf53 --- /dev/null +++ b/webapp/packages/core-settings/src/SettingsProviderService.ts @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { makeObservable, observable } from 'mobx'; + +import { injectable } from '@cloudbeaver/core-di'; +import { schema } from '@cloudbeaver/core-utils'; + +import { SettingsProvider } from './SettingsProvider'; +import { SettingsResolverService } from './SettingsResolverService'; + +@injectable() +export class SettingsProviderService { + get schema(): schema.AnyZodObject { + return this.mergedSchema; + } + private mergedSchema: schema.AnyZodObject; + constructor(private readonly settingsResolverService: SettingsResolverService) { + this.mergedSchema = schema.object({}); + + makeObservable(this, { + mergedSchema: observable.ref, + }); + } + + createSettings(schema: TSchema) { + this.mergedSchema = this.mergedSchema.merge(schema); + const settings = new SettingsProvider(this.settingsResolverService, schema); + return settings; + } +} diff --git a/webapp/packages/core-settings/src/SettingsResolverSource.ts b/webapp/packages/core-settings/src/SettingsResolverSource.ts index b5569d4b6c..acfad55cc8 100644 --- a/webapp/packages/core-settings/src/SettingsResolverSource.ts +++ b/webapp/packages/core-settings/src/SettingsResolverSource.ts @@ -5,75 +5,160 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { makeObservable, observable } from 'mobx'; +import { computed, makeObservable, observable } from 'mobx'; + +import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import type { ISettingsResolverSource } from './ISettingsResolverSource'; -import type { ISettingsSource } from './ISettingsSource'; +import type { ISettingChangeData, ISettingsSource } from './ISettingsSource'; +import type { ISettingsLayer } from './SettingsLayer'; -export abstract class SettingsResolverSource implements ISettingsResolverSource { - protected resolvers: ISettingsSource[]; +interface ISettingsSourcesLayer { + layer: ISettingsLayer; + sources: ISettingsSource[]; +} + +export class SettingsResolverSource implements ISettingsResolverSource { + readonly onChange: ISyncExecutor; + protected get sources(): ISettingsSource[] { + return this.layers + .slice() + .sort((a, b) => a.layer.level - b.layer.level) + .flatMap(layer => layer.sources) + .reverse(); + } + protected layers: ISettingsSourcesLayer[]; constructor() { - this.resolvers = []; - makeObservable(this, { - resolvers: observable.shallow, + this.onChange = new SyncExecutor(); + this.layers = []; + makeObservable(this, { + layers: observable.shallow, + sources: computed, }); } - hasResolver(resolver: ISettingsSource): boolean { - return this.resolvers.includes(resolver); + hasResolver(layer: ISettingsLayer, resolver: ISettingsSource): boolean { + return this.tryGetLayerSources(layer)?.sources.includes(resolver) || false; } - removeResolver(resolver: ISettingsSource): void { - const index = this.resolvers.indexOf(resolver); + removeResolver(layer: ISettingsLayer, resolver: ISettingsSource): void { + const layerSources = this.getLayerSources(layer); + + const index = layerSources.sources.indexOf(resolver); if (index !== -1) { - this.resolvers.splice(index, 1); + layerSources.sources.splice(index, 1); + resolver.onChange.removeNext(this.onChange); } } - addResolver(...resolvers: ISettingsSource[]): void { - if (resolvers.some(this.hasResolver.bind(this))) { + addResolver(layer: ISettingsLayer, ...resolvers: ISettingsSource[]): void { + if (resolvers.some(this.hasResolver.bind(this, layer))) { return; } - this.resolvers.push(...resolvers); + const layerSources = this.getOrCreateLayerSources(layer); + + layerSources.sources.push(...resolvers); + + for (const resolver of resolvers) { + resolver.onChange.next( + this.onChange, + data => { + if (resolver.has(data.key)) { + return data; + } + return { ...data, value: this.getValue(data.key) }; + }, + data => !resolver.has(data.key) || this.sources.find(r => r.has(data.key)) === resolver, + ); + } } clearResolvers(): void { - this.resolvers = []; + this.layers = []; + } + + isEdited(key?: any): boolean { + return this.sources.find(r => r.has(key))?.isEdited(key) || false; } isReadOnly(key: any): boolean { - return this.resolvers.every(r => r.isReadOnly(key)); + for (const source of this.sources) { + if (!source.isReadOnly(key)) { + return false; + } + + if (source.has(key)) { + return true; + } + } + return true; } has(key: any): boolean { - return this.resolvers.some(r => r.has(key)); + return this.sources.some(r => r.has(key)); } - getDefaultValue(key: any): any { - return this.resolvers.find(r => r.getDefaultValue(key) !== undefined)?.getDefaultValue(key); + getEditedValue(key: any): any { + return this.sources.find(r => r.has(key))?.getEditedValue(key); } getValue(key: any): any { - return this.resolvers.find(r => r.has(key))?.getValue(key); + return this.sources.find(r => r.has(key))?.getValue(key); } setValue(key: any, value: any): void { - for (const resolver of this.resolvers) { - if (!resolver.isReadOnly(key)) { - resolver.setValue(key, value); + for (const source of this.sources) { + const readonly = source.isReadOnly(key); + + if (source.has(key) && readonly) { + throw new Error(`Can't set value for key ${key}`); + } + + if (!readonly) { + source.setValue(key, value); return; } } + } - throw new Error(`Can't set value for key ${key}`); + async save(): Promise { + for (const source of this.sources) { + if (source.isEdited()) { + await source.save(); + } + } } clear(): void { - for (const resolver of this.resolvers) { + for (const resolver of this.sources) { resolver.clear(); } } + + protected getOrCreateLayerSources(layer: ISettingsLayer): ISettingsSourcesLayer { + if (!this.tryGetLayerSources(layer)) { + this.layers.push(observable({ layer, sources: [] }, { sources: observable.shallow })); + } + + return this.getLayerSources(layer); + } + + protected tryGetLayerSources(layer: ISettingsLayer): ISettingsSourcesLayer | undefined { + const layerSources = this.layers.find(layerSources => layerSources.layer === layer); + + return layerSources; + } + + protected getLayerSources(layer: ISettingsLayer): ISettingsSourcesLayer { + const layerSources = this.tryGetLayerSources(layer); + + if (!layerSources) { + throw new Error('Resolver not found'); + } + + return layerSources; + } } diff --git a/webapp/packages/core-settings/src/SettingsSource.ts b/webapp/packages/core-settings/src/SettingsSource.ts new file mode 100644 index 0000000000..a65ef7b7a9 --- /dev/null +++ b/webapp/packages/core-settings/src/SettingsSource.ts @@ -0,0 +1,94 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { action, makeObservable, observable } from 'mobx'; + +import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { isNotNullDefined } from '@cloudbeaver/core-utils'; + +import type { ISettingChangeData, ISettingsSource } from './ISettingsSource'; + +export abstract class SettingsSource implements ISettingsSource { + readonly onChange: ISyncExecutor; + private updating: boolean; + protected readonly changes: Map; + constructor() { + this.onChange = new SyncExecutor(); + this.updating = false; + this.changes = new Map(); + + makeObservable(this, { + changes: observable.shallow, + update: action, + }); + } + + has(key: any): boolean { + return this.changes.has(key); + } + + isEdited(key?: any): boolean { + if (isNotNullDefined(key)) { + return this.changes.has(key); + } + + return this.changes.size > 0; + } + + protected abstract getSnapshot(): Record; + abstract isReadOnly(key: any): boolean; + abstract getValue(key: any): any; + abstract save(): Promise; + + clear(): void { + this.changes.clear(); + } + + getEditedValue(key: any): any { + if (this.changes.has(key)) { + return this.changes.get(key); + } + + return this.getValue(key); + } + + setValue(key: any, value: any): void { + if (this.getValue(key) === value) { + this.changes.delete(key); + } else { + this.changes.set(key, value); + } + } + + protected update(action: () => void) { + if (this.updating) { + action(); + return; + } + + this.updating = true; + try { + const snapshot = this.getSnapshot(); + action(); + const newSnapshot = this.getSnapshot(); + + for (const [key, value] of Object.entries(newSnapshot)) { + if (snapshot[key] !== value) { + this.onChange.execute({ key, value }); + } + } + + for (const key of Object.keys(snapshot)) { + if (!(key in newSnapshot)) { + this.onChange.execute({ key, value: undefined }); + } + } + } finally { + this.updating = false; + } + } +} diff --git a/webapp/packages/core-settings/src/__custom_mocks__/expectDeprecatedSettingMessage.ts b/webapp/packages/core-settings/src/__custom_mocks__/expectDeprecatedSettingMessage.ts new file mode 100644 index 0000000000..0ae2cf2a0b --- /dev/null +++ b/webapp/packages/core-settings/src/__custom_mocks__/expectDeprecatedSettingMessage.ts @@ -0,0 +1,26 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { addKnownWarn, consoleSpy } from '@cloudbeaver/tests-runner'; + +const DEPRECATED_SETTING_MESSAGE_REGEX = /You are using deprecated settings.*/; + +beforeAll(async () => { + addKnownWarn(DEPRECATED_SETTING_MESSAGE_REGEX); +}); + +export function expectDeprecatedSettingMessage(deprecated?: string, key?: string) { + if (deprecated && key) { + expect(consoleSpy.warn).toHaveBeenCalledWith(expect.stringMatching(`You are using deprecated settings: "${deprecated}". Use "${key}" instead.`)); + } else { + expect(consoleSpy.warn).toHaveBeenCalledWith(expect.stringMatching(DEPRECATED_SETTING_MESSAGE_REGEX)); + } +} + +export function expectNoDeprecatedSettingMessage() { + expect(consoleSpy.warn).not.toHaveBeenCalledWith(expect.stringMatching(DEPRECATED_SETTING_MESSAGE_REGEX)); +} diff --git a/webapp/packages/core-settings/src/createSettingsAliasResolver.test.ts b/webapp/packages/core-settings/src/createSettingsAliasResolver.test.ts new file mode 100644 index 0000000000..ff9a4d21c7 --- /dev/null +++ b/webapp/packages/core-settings/src/createSettingsAliasResolver.test.ts @@ -0,0 +1,61 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import '@testing-library/jest-dom'; + +import { SyncExecutor } from '@cloudbeaver/core-executor'; + +import { expectDeprecatedSettingMessage } from './__custom_mocks__/expectDeprecatedSettingMessage'; +import { createSettingsAliasResolver } from './createSettingsAliasResolver'; +import type { ISettingsSource } from './ISettingsSource'; + +const deprecatedSettings = { + deprecated: 'deprecatedValue', +}; + +const newSettings = { + ...deprecatedSettings, + value: 'value', +}; + +function createSource(settings: Record): ISettingsSource { + return { + onChange: new SyncExecutor(), + has(key: any): boolean { + return key in settings; + }, + isEdited(key?: any): boolean { + return false; + }, + isReadOnly(key: any): boolean { + return false; + }, + getValue(key: any): any | undefined { + return settings[key]; + }, + getEditedValue(key: any): any | undefined { + return undefined; + }, + setValue(key: any, value: any): void {}, + async save(): Promise {}, + clear(): void {}, + }; +} + +function createResolver(settings: Record) { + return createSettingsAliasResolver(createSource(settings), null as any, { + value: 'deprecated', + }); +} + +test('Deprecated setting extracted', async () => { + const resolver = createResolver(newSettings); + + expect(resolver.has('value')).toBe(true); + expect(resolver.getValue('value')).toBe('deprecatedValue'); + expectDeprecatedSettingMessage('deprecated', 'value'); +}); diff --git a/webapp/packages/core-settings/src/createSettingsAliasResolver.ts b/webapp/packages/core-settings/src/createSettingsAliasResolver.ts new file mode 100644 index 0000000000..fcd10580ac --- /dev/null +++ b/webapp/packages/core-settings/src/createSettingsAliasResolver.ts @@ -0,0 +1,117 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { invertObject, type schema } from '@cloudbeaver/core-utils'; + +import type { ISettingChangeData, ISettingsSource } from './ISettingsSource'; +import type { SettingsProvider } from './SettingsProvider'; + +const DEPRECATED_SETTINGS = new Set(); + +type SettingsMapping = Partial<{ + [key in keyof TTarget]: string; +}>; + +export function createSettingsAliasResolver( + source: ISettingsSource, + target: SettingsProvider, + mappings: SettingsMapping>, +): ISettingsSource { + type targetSchema = schema.infer; + const reversed = invertObject(mappings); + + function reverseMapKey(key: string): keyof targetSchema { + return (reversed[key] as any) || (key as any); + } + + function mapKey(key: keyof targetSchema): string { + return mappings[key] || (key as any); + } + const onChange: ISyncExecutor> = new SyncExecutor(); + + source.onChange.next( + onChange, + data => ({ ...data, key: reverseMapKey(data.key) }), + data => data.key in reversed, + ); + + let resolverLock = false; + + function withLock(fn: (locked: boolean) => T): T { + if (resolverLock) { + return fn(true); + } + try { + resolverLock = true; + return fn(false); + } finally { + resolverLock = false; + } + } + + return { + onChange, + has(key) { + if (!(key in mappings)) { + return false; + } + + // check that there is no actual key in source + if (withLock(locked => locked || source.has(key))) { + return false; + } + + const oldKey = mapKey(key); + const has = source.has(oldKey); + + if (!DEPRECATED_SETTINGS.has(oldKey)) { + console.warn(`You have deprecated settings: "${String(oldKey)}". Use "${key}" instead.`); + DEPRECATED_SETTINGS.add(oldKey); + } + + return has; + }, + isEdited(key) { + if (!(key in mappings)) { + return false; + } + return source.isEdited(mapKey(key)); + }, + isReadOnly(key) { + if (!(key in mappings)) { + return true; + } + return source.isReadOnly(mapKey(key)); + }, + getEditedValue(key) { + if (!(key in mappings)) { + return undefined; + } + return source.getEditedValue(mapKey(key)); + }, + getValue(key) { + if (!(key in mappings)) { + return undefined; + } + + return source.getValue(mapKey(key)); + }, + setValue(key, value) { + if (!(key in mappings)) { + return; + } + withLock(locked => { + if (!locked) { + source.setValue(key, value); + } + }); + }, + async save() {}, + clear() {}, + }; +} diff --git a/webapp/packages/core-settings/src/index.ts b/webapp/packages/core-settings/src/index.ts index 82521c14e3..fd1335b3d8 100644 --- a/webapp/packages/core-settings/src/index.ts +++ b/webapp/packages/core-settings/src/index.ts @@ -1,7 +1,17 @@ export * from './manifest'; +export * from './createSettingsAliasResolver'; export * from './SettingsScopeType'; -export * from './SettingsService'; export * from './SettingsResolverSource'; export * from './ISettingsResolverSource'; export * from './ISettingsSource'; +export * from './SettingsLayer'; export * from './SettingsResolverService'; +export * from './SettingsProvider'; +export * from './SettingsProviderService'; +export * from './SettingsSource'; +export * from './SettingsManager/ESettingsValueType'; +export * from './SettingsManager/ISettingDescription'; +export * from './SettingsManager/SettingsGroup'; +export * from './SettingsManager/SettingsManagerService'; +export * from './SettingsManager/SettingsGroups/INTERFACE_SETTINGS_GROUP'; +export * from './SettingsManager/SettingsGroups/ROOT_SETTINGS_GROUP'; diff --git a/webapp/packages/core-plugin/src/locales/en.ts b/webapp/packages/core-settings/src/locales/en.ts similarity index 100% rename from webapp/packages/core-plugin/src/locales/en.ts rename to webapp/packages/core-settings/src/locales/en.ts diff --git a/webapp/packages/core-plugin/src/locales/it.ts b/webapp/packages/core-settings/src/locales/it.ts similarity index 100% rename from webapp/packages/core-plugin/src/locales/it.ts rename to webapp/packages/core-settings/src/locales/it.ts diff --git a/webapp/packages/core-plugin/src/locales/ru.ts b/webapp/packages/core-settings/src/locales/ru.ts similarity index 100% rename from webapp/packages/core-plugin/src/locales/ru.ts rename to webapp/packages/core-settings/src/locales/ru.ts diff --git a/webapp/packages/core-plugin/src/locales/zh.ts b/webapp/packages/core-settings/src/locales/zh.ts similarity index 100% rename from webapp/packages/core-plugin/src/locales/zh.ts rename to webapp/packages/core-settings/src/locales/zh.ts diff --git a/webapp/packages/core-settings/src/manifest.ts b/webapp/packages/core-settings/src/manifest.ts index 7537479ab5..a20a4d432b 100644 --- a/webapp/packages/core-settings/src/manifest.ts +++ b/webapp/packages/core-settings/src/manifest.ts @@ -7,13 +7,15 @@ */ import type { PluginManifest } from '@cloudbeaver/core-di'; +import { LocaleService } from './LocaleService'; +import { SettingsManagerService } from './SettingsManager/SettingsManagerService'; +import { SettingsProviderService } from './SettingsProviderService'; import { SettingsResolverService } from './SettingsResolverService'; -import { SettingsService } from './SettingsService'; export const coreSettingsManifest: PluginManifest = { info: { name: 'Core Settings', }, - providers: [SettingsService, SettingsResolverService], + providers: [SettingsManagerService, SettingsResolverService, LocaleService, SettingsProviderService], }; diff --git a/webapp/packages/core-settings/tsconfig.json b/webapp/packages/core-settings/tsconfig.json index 3ce1bf6725..e153e2d403 100644 --- a/webapp/packages/core-settings/tsconfig.json +++ b/webapp/packages/core-settings/tsconfig.json @@ -6,14 +6,23 @@ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "references": [ - { - "path": "../core-browser/tsconfig.json" - }, { "path": "../core-di/tsconfig.json" }, { "path": "../core-executor/tsconfig.json" + }, + { + "path": "../core-executor/tsconfig.json" + }, + { + "path": "../core-localization/tsconfig.json" + }, + { + "path": "../core-utils/tsconfig.json" + }, + { + "path": "../tests-runner/tsconfig.json" } ], "include": [ diff --git a/webapp/packages/core-storage/.gitignore b/webapp/packages/core-storage/.gitignore new file mode 100644 index 0000000000..99902db863 --- /dev/null +++ b/webapp/packages/core-storage/.gitignore @@ -0,0 +1,21 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/core-plugin/package.json b/webapp/packages/core-storage/package.json similarity index 74% rename from webapp/packages/core-plugin/package.json rename to webapp/packages/core-storage/package.json index ff80fb0f04..234b638b60 100644 --- a/webapp/packages/core-plugin/package.json +++ b/webapp/packages/core-storage/package.json @@ -1,5 +1,5 @@ { - "name": "@cloudbeaver/core-plugin", + "name": "@cloudbeaver/core-storage", "sideEffects": [ "src/**/*.css", "src/**/*.scss", @@ -17,11 +17,9 @@ "update-ts-references": "rimraf --glob dist && typescript-resolve-references" }, "dependencies": { + "@cloudbeaver/core-browser": "~0.1.0", "@cloudbeaver/core-di": "~0.1.0", - "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-product": "~0.1.0", - "@cloudbeaver/core-settings": "~0.1.0", - "@cloudbeaver/core-utils": "~0.1.0" + "@cloudbeaver/core-executor": "~0.1.0" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/core-settings/src/SettingsService.ts b/webapp/packages/core-storage/src/StorageService.ts similarity index 97% rename from webapp/packages/core-settings/src/SettingsService.ts rename to webapp/packages/core-storage/src/StorageService.ts index cf71b17042..94fb1ad883 100644 --- a/webapp/packages/core-settings/src/SettingsService.ts +++ b/webapp/packages/core-storage/src/StorageService.ts @@ -10,7 +10,7 @@ import { injectable } from '@cloudbeaver/core-di'; import type { ISyncExecutor } from '@cloudbeaver/core-executor'; @injectable() -export class SettingsService { +export class StorageService { readonly onStorageChange: ISyncExecutor; constructor(private readonly localStorageSaveService: LocalStorageSaveService) { this.onStorageChange = this.localStorageSaveService.onStorageChange; diff --git a/webapp/packages/core-storage/src/index.ts b/webapp/packages/core-storage/src/index.ts new file mode 100644 index 0000000000..4471676e7a --- /dev/null +++ b/webapp/packages/core-storage/src/index.ts @@ -0,0 +1,2 @@ +export * from './manifest'; +export * from './StorageService'; diff --git a/webapp/packages/core-storage/src/manifest.ts b/webapp/packages/core-storage/src/manifest.ts new file mode 100644 index 0000000000..704fa753d1 --- /dev/null +++ b/webapp/packages/core-storage/src/manifest.ts @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { PluginManifest } from '@cloudbeaver/core-di'; + +import { StorageService } from './StorageService'; + +export const coreStorageManifest: PluginManifest = { + info: { + name: 'Core Storage', + }, + + providers: [StorageService], +}; diff --git a/webapp/packages/core-storage/tsconfig.json b/webapp/packages/core-storage/tsconfig.json new file mode 100644 index 0000000000..3ce1bf6725 --- /dev/null +++ b/webapp/packages/core-storage/tsconfig.json @@ -0,0 +1,31 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + }, + "references": [ + { + "path": "../core-browser/tsconfig.json" + }, + { + "path": "../core-di/tsconfig.json" + }, + { + "path": "../core-executor/tsconfig.json" + } + ], + "include": [ + "__custom_mocks__/**/*", + "src/**/*", + "src/**/*.json", + "src/**/*.css", + "src/**/*.scss" + ], + "exclude": [ + "**/node_modules", + "lib/**/*", + "dist/**/*" + ] +} diff --git a/webapp/packages/core-theming/package.json b/webapp/packages/core-theming/package.json index a7ac60e6d8..8c2d128a84 100644 --- a/webapp/packages/core-theming/package.json +++ b/webapp/packages/core-theming/package.json @@ -22,20 +22,15 @@ "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-executor": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "mobx": "^6.12.0" }, "peerDependencies": {}, "devDependencies": { - "@cloudbeaver/core-browser": "~0.1.0", "@cloudbeaver/core-client-activity": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-product": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", diff --git a/webapp/packages/core-theming/src/ThemeService.ts b/webapp/packages/core-theming/src/ThemeService.ts index 40ed690ace..110258d450 100644 --- a/webapp/packages/core-theming/src/ThemeService.ts +++ b/webapp/packages/core-theming/src/ThemeService.ts @@ -5,13 +5,11 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action, computed, makeObservable, observable } from 'mobx'; +import { computed, IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { NotificationService, UIError } from '@cloudbeaver/core-events'; import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; -import { ServerConfigResource } from '@cloudbeaver/core-root'; -import { SettingsService } from '@cloudbeaver/core-settings'; import type { Style } from './ComponentStyle'; import './styles/main/base.pure.css'; @@ -25,7 +23,6 @@ import { ThemeSettingsService } from './ThemeSettingsService'; import type { ClassCollection } from './themeUtils'; const COMMON_STYLES: any[] = []; -const THEME_SETTINGS_KEY = 'themeSettings'; export interface ITheme { name: string; @@ -34,10 +31,6 @@ export interface ITheme { loader: () => Promise; } -interface ISettings { - currentThemeId: string; -} - export interface IStyleRegistry { mode: 'replace' | 'append'; styles: Style[]; @@ -49,16 +42,12 @@ export class ThemeService extends Bootstrap { return Array.from(this.themeMap.values()); } - get defaultThemeId(): string { - return this.themeSettingsService.settings.getValue('defaultTheme'); - } - - get currentThemeId(): string { - return this.settings.currentThemeId; + get themeId(): string { + return this.themeSettingsService.theme; } get currentTheme(): ITheme { - let theme = this.themeMap.get(this.currentThemeId); + let theme = this.themeMap.get(this.themeId); if (!theme) { theme = this.themeMap.get(DEFAULT_THEME_ID)!; @@ -71,31 +60,40 @@ export class ThemeService extends Bootstrap { private readonly stylesRegistry: Map = new Map(); private readonly themeMap: Map = new Map(); - private readonly settings: ISettings; + private reactionDisposer: IReactionDisposer | null; constructor( - private readonly serverConfigResource: ServerConfigResource, private readonly notificationService: NotificationService, - private readonly settingsService: SettingsService, private readonly themeSettingsService: ThemeSettingsService, ) { super(); - this.settings = getDefaultThemeSettings(); + this.reactionDisposer = null; this.onChange = new SyncExecutor(); - makeObservable(this, { + makeObservable(this, { themes: computed, currentTheme: computed, - defaultThemeId: computed, + themeId: computed, themeMap: observable.shallow, - settings: observable, - setCurrentThemeId: action, }); } register(): void { this.loadAllThemes(); + this.reactionDisposer = reaction( + () => this.currentTheme, + theme => this.loadTheme(theme.id), + { + fireImmediately: true, + }, + ); + } + + dispose(): void { + if (this.reactionDisposer) { + this.reactionDisposer(); + } } addStyleRegistry>(style: Style, mode: 'replace' | 'append', styles: Style[]): void { @@ -130,12 +128,7 @@ export class ThemeService extends Bootstrap { } async load(): Promise { - await this.serverConfigResource.load(); - this.setCurrentThemeId(this.defaultThemeId); // set default app theme - this.settingsService.registerSettings(THEME_SETTINGS_KEY, this.settings, getDefaultThemeSettings, undefined, () => - this.setTheme(this.currentThemeId), - ); // load user state theme - await this.setTheme(this.currentThemeId); + await this.loadTheme(this.themeId); } getThemeStyles(themeId: string): ClassCollection[] { @@ -149,32 +142,30 @@ export class ThemeService extends Bootstrap { } async changeTheme(themeId: string): Promise { - if (themeId === this.currentThemeId) { + if (themeId === this.themeId) { return; } await this.setTheme(themeId); this.onChange.execute(this.currentTheme); } - async setTheme(themeId: string): Promise { - await this.tryChangeTheme(themeId); + private async setTheme(themeId: string): Promise { + themeId = await this.loadTheme(themeId); + + this.themeSettingsService.settings.setValue('core.theming.theme', themeId); + await this.themeSettingsService.settings.save(); } - private async tryChangeTheme(themeId: string): Promise { + private async loadTheme(themeId: string): Promise { try { await this.loadThemeStylesAsync(themeId); + return themeId; } catch (e: any) { if (themeId !== DEFAULT_THEME_ID) { - return this.tryChangeTheme(DEFAULT_THEME_ID); // try to fallback to default theme + return this.loadTheme(DEFAULT_THEME_ID); // try to fallback to default theme } throw e; } - - this.setCurrentThemeId(themeId); - } - - private setCurrentThemeId(themeId: string) { - this.settings.currentThemeId = themeId; } private loadAllThemes(): void { @@ -194,9 +185,3 @@ export class ThemeService extends Bootstrap { } } } - -function getDefaultThemeSettings(): ISettings { - return { - currentThemeId: DEFAULT_THEME_ID, - }; -} diff --git a/webapp/packages/core-theming/src/ThemeSettingsManagementService.ts b/webapp/packages/core-theming/src/ThemeSettingsManagementService.ts index 8daf49cc09..70e967cd43 100644 --- a/webapp/packages/core-theming/src/ThemeSettingsManagementService.ts +++ b/webapp/packages/core-theming/src/ThemeSettingsManagementService.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { SettingsManagerService } from '@cloudbeaver/core-plugin'; +import { ESettingsValueType, INTERFACE_SETTINGS_GROUP, SettingsManagerService } from '@cloudbeaver/core-settings'; import { ThemeService } from './ThemeService'; import { ThemeSettingsService } from './ThemeSettingsService'; @@ -16,14 +16,17 @@ export class ThemeSettingsManagementService extends Dependency { constructor(themeSettingsService: ThemeSettingsService, themeService: ThemeService, settingsManagerService: SettingsManagerService) { super(); settingsManagerService.registerSettings(themeSettingsService.settings, () => [ - // { - // key: 'defaultTheme', - // type: ESettingsValueType.Select, - // name: 'core_theming_settings_default_theme_name', - // description: 'core_theming_settings_default_theme_description', - // options: themeService.themes.map(theme => ({ id: theme.id, name: theme.name })), - // group: INTERFACE_SETTINGS_GROUP, - // }, + { + key: 'core.theming.theme', + access: { + scope: ['server', 'client'], + }, + type: ESettingsValueType.Select, + name: 'core_theming_settings_theme_name', + description: 'core_theming_settings_theme_description', + options: themeService.themes.map(theme => ({ value: theme.id, name: theme.name })), + group: INTERFACE_SETTINGS_GROUP, + }, ]); } } diff --git a/webapp/packages/core-theming/src/ThemeSettingsService.test.ts b/webapp/packages/core-theming/src/ThemeSettingsService.test.ts index 83eeda9986..d4d34c3f94 100644 --- a/webapp/packages/core-theming/src/ThemeSettingsService.test.ts +++ b/webapp/packages/core-theming/src/ThemeSettingsService.test.ts @@ -7,58 +7,50 @@ */ import '@testing-library/jest-dom'; -import { coreBrowserManifest } from '@cloudbeaver/core-browser'; import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; +import '@cloudbeaver/core-root/dist/__custom_mocks__/expectWebsocketClosedMessage'; import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; import { coreSDKManifest } from '@cloudbeaver/core-sdk'; import { coreSettingsManifest } from '@cloudbeaver/core-settings'; +import { + expectDeprecatedSettingMessage, + expectNoDeprecatedSettingMessage, +} from '@cloudbeaver/core-settings/dist/__custom_mocks__/expectDeprecatedSettingMessage'; import { createApp } from '@cloudbeaver/tests-runner'; import { coreThemingManifest } from './manifest'; -import { IThemeSettings, ThemeSettingsService } from './ThemeSettingsService'; +import { ThemeSettingsService } from './ThemeSettingsService'; const endpoint = createGQLEndpoint(); +const server = mockGraphQL(...mockAppInit(endpoint)); const app = createApp( coreThemingManifest, coreEventsManifest, - corePluginManifest, - coreProductManifest, coreRootManifest, coreSDKManifest, coreSettingsManifest, - coreBrowserManifest, coreLocalizationManifest, coreClientActivityManifest, ); -const server = mockGraphQL(...mockAppInit(endpoint)); - -beforeAll(() => app.init()); - -const testValueA = 'light'; -const testValueB = 'dark'; +const deprecatedValue = 'light'; +const value = 'dark'; const deprecatedSettings = { - 'core.user': { - defaultTheme: testValueA, - } as IThemeSettings, + 'core.user.defaultTheme': deprecatedValue, + 'core.theming.defaultTheme': deprecatedValue, + 'app.defaultTheme': deprecatedValue, }; const newSettings = { ...deprecatedSettings, - core: { - theming: { - defaultTheme: testValueB, - } as IThemeSettings, - }, + 'core.theming.theme': value, }; test('New Settings override deprecated settings', async () => { @@ -69,7 +61,8 @@ test('New Settings override deprecated settings', async () => { await config.refresh(); - expect(settings.settings.getValue('defaultTheme')).toBe(testValueB); + expect(settings.theme).toBe(value); + expectNoDeprecatedSettingMessage(); }); test('Deprecated settings are used if new settings are not defined', async () => { @@ -80,5 +73,6 @@ test('Deprecated settings are used if new settings are not defined', async () => await config.refresh(); - expect(settings.settings.getValue('defaultTheme')).toBe(testValueA); + expect(settings.theme).toBe(deprecatedValue); + expectDeprecatedSettingMessage(); }); diff --git a/webapp/packages/core-theming/src/ThemeSettingsService.ts b/webapp/packages/core-theming/src/ThemeSettingsService.ts index 4e8048524a..dd91fb79aa 100644 --- a/webapp/packages/core-theming/src/ThemeSettingsService.ts +++ b/webapp/packages/core-theming/src/ThemeSettingsService.ts @@ -6,31 +6,44 @@ * you may not use this file except in compliance with the License. */ import { injectable } from '@cloudbeaver/core-di'; -import { createSettingsAliasResolver, PluginManagerService, PluginSettings } from '@cloudbeaver/core-plugin'; -import { ServerSettingsResolverService, ServerSettingsService } from '@cloudbeaver/core-root'; +import { + createSettingsAliasResolver, + ROOT_SETTINGS_LAYER, + SettingsProvider, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; import { schema } from '@cloudbeaver/core-utils'; import { DEFAULT_THEME_ID } from './themes'; const settingsSchema = schema.object({ - defaultTheme: schema.string().default(DEFAULT_THEME_ID), + 'core.theming.theme': schema.string().default(DEFAULT_THEME_ID), }); export type IThemeSettings = schema.infer; @injectable() export class ThemeSettingsService { - readonly settings: PluginSettings; + get theme(): string { + return this.settings.getValue('core.theming.theme'); + } + readonly settings: SettingsProvider; constructor( - private readonly pluginManagerService: PluginManagerService, - private readonly serverSettingsService: ServerSettingsService, - private readonly serverSettingsResolverService: ServerSettingsResolverService, + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsResolverService: SettingsResolverService, ) { - this.settings = this.pluginManagerService.createSettings('theming', 'core', settingsSchema); - this.serverSettingsResolverService.addResolver( + this.settings = this.settingsProviderService.createSettings(settingsSchema); + + this.settingsResolverService.addResolver( + ROOT_SETTINGS_LAYER, /** @deprecated Use settings instead, will be removed in 23.0.0 */ - createSettingsAliasResolver(this.serverSettingsService, this.settings, 'core.user'), + createSettingsAliasResolver(this.settingsResolverService, this.settings, { 'core.theming.theme': 'core.user.defaultTheme' }), + createSettingsAliasResolver(this.settingsResolverService, this.settings, { + 'core.theming.theme': 'core.localization.defaultTheme', + }), + createSettingsAliasResolver(this.settingsResolverService, this.settings, { 'core.theming.theme': 'app.defaultTheme' }), ); } } diff --git a/webapp/packages/core-theming/src/locales/en.ts b/webapp/packages/core-theming/src/locales/en.ts index 907b500a08..7665aab62f 100644 --- a/webapp/packages/core-theming/src/locales/en.ts +++ b/webapp/packages/core-theming/src/locales/en.ts @@ -1,4 +1,4 @@ export default [ - ['core_theming_settings_default_theme_name', 'Default Theme'], - ['core_theming_settings_default_theme_description', 'Default Theme'], + ['core_theming_settings_theme_name', 'Theme'], + ['core_theming_settings_theme_description', 'Application color theme'], ]; diff --git a/webapp/packages/core-theming/src/locales/it.ts b/webapp/packages/core-theming/src/locales/it.ts index 907b500a08..7665aab62f 100644 --- a/webapp/packages/core-theming/src/locales/it.ts +++ b/webapp/packages/core-theming/src/locales/it.ts @@ -1,4 +1,4 @@ export default [ - ['core_theming_settings_default_theme_name', 'Default Theme'], - ['core_theming_settings_default_theme_description', 'Default Theme'], + ['core_theming_settings_theme_name', 'Theme'], + ['core_theming_settings_theme_description', 'Application color theme'], ]; diff --git a/webapp/packages/core-theming/src/locales/ru.ts b/webapp/packages/core-theming/src/locales/ru.ts index 77a31e5689..40c2d3423c 100644 --- a/webapp/packages/core-theming/src/locales/ru.ts +++ b/webapp/packages/core-theming/src/locales/ru.ts @@ -1,4 +1,4 @@ export default [ - ['core_theming_settings_default_theme_name', 'Тема по умолчанию'], - ['core_theming_settings_default_theme_description', 'Тема по умолчанию'], + ['core_theming_settings_theme_name', 'Тема'], + ['core_theming_settings_theme_description', 'Цветовая тема приложения'], ]; diff --git a/webapp/packages/core-theming/src/locales/zh.ts b/webapp/packages/core-theming/src/locales/zh.ts index 907b500a08..7665aab62f 100644 --- a/webapp/packages/core-theming/src/locales/zh.ts +++ b/webapp/packages/core-theming/src/locales/zh.ts @@ -1,4 +1,4 @@ export default [ - ['core_theming_settings_default_theme_name', 'Default Theme'], - ['core_theming_settings_default_theme_description', 'Default Theme'], + ['core_theming_settings_theme_name', 'Theme'], + ['core_theming_settings_theme_description', 'Application color theme'], ]; diff --git a/webapp/packages/core-theming/tsconfig.json b/webapp/packages/core-theming/tsconfig.json index 0f4fc71597..e1c1723121 100644 --- a/webapp/packages/core-theming/tsconfig.json +++ b/webapp/packages/core-theming/tsconfig.json @@ -6,9 +6,6 @@ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "references": [ - { - "path": "../core-browser/tsconfig.json" - }, { "path": "../core-client-activity/tsconfig.json" }, @@ -30,18 +27,6 @@ { "path": "../core-localization/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-product/tsconfig.json" - }, - { - "path": "../core-root/tsconfig.json" - }, { "path": "../core-root/tsconfig.json" }, diff --git a/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx b/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx index ae189ac943..d89f4e29a9 100644 --- a/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx @@ -6,33 +6,14 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { ButtonHTMLAttributes, forwardRef, useRef, useState } from 'react'; -import type { MenuInitialState } from 'reakit/Menu'; +import { forwardRef, useRef, useState } from 'react'; -import { getComputed, IMenuState, IMouseContextMenu, Menu, useAutoLoad, useObjectRef, useTranslate } from '@cloudbeaver/core-blocks'; -import { IMenuData, MenuActionItem } from '@cloudbeaver/core-view'; +import { getComputed, IMenuState, Menu, useAutoLoad, useObjectRef, useTranslate } from '@cloudbeaver/core-blocks'; +import { MenuActionItem } from '@cloudbeaver/core-view'; +import type { IContextMenuProps } from './IContextMenuProps'; import { MenuItemRenderer } from './MenuItemRenderer'; -interface IMenuProps extends React.PropsWithChildren { - loading: boolean; - disabled: boolean; -} - -type ContextMenuRenderingChildren = (props: IMenuProps) => React.ReactNode | React.ReactElement; - -interface IContextMenuProps extends Omit, 'children'> { - mouseContextMenu?: IMouseContextMenu; - menu: IMenuData; - disclosure?: boolean; - placement?: MenuInitialState['placement']; - modal?: boolean; - visible?: boolean; - rtl?: boolean; - children?: React.ReactNode | ContextMenuRenderingChildren; - onVisibleSwitch?: (visible: boolean) => void; -} - // TODO the click doesn't work for React components as children export const ContextMenu = observer( forwardRef(function ContextMenu( diff --git a/webapp/packages/core-ui/src/ContextMenu/ContextMenuLazy.ts b/webapp/packages/core-ui/src/ContextMenu/ContextMenuLazy.ts new file mode 100644 index 0000000000..81d8328629 --- /dev/null +++ b/webapp/packages/core-ui/src/ContextMenu/ContextMenuLazy.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +export const ContextMenu = importLazyComponent(() => import('./ContextMenu').then(m => m.ContextMenu)); diff --git a/webapp/packages/core-ui/src/ContextMenu/IContextMenuProps.ts b/webapp/packages/core-ui/src/ContextMenu/IContextMenuProps.ts new file mode 100644 index 0000000000..a6bd3f940d --- /dev/null +++ b/webapp/packages/core-ui/src/ContextMenu/IContextMenuProps.ts @@ -0,0 +1,31 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { ButtonHTMLAttributes } from 'react'; +import type { MenuInitialState } from 'reakit'; + +import type { IMouseContextMenu } from '@cloudbeaver/core-blocks'; +import type { IMenuData } from '@cloudbeaver/core-view'; + +export interface IContextMenuBaseProps extends React.PropsWithChildren { + loading: boolean; + disabled: boolean; +} + +export type ContextMenuRenderingChildren = (props: IContextMenuBaseProps) => React.ReactNode | React.ReactElement; + +export interface IContextMenuProps extends Omit, 'children'> { + mouseContextMenu?: IMouseContextMenu; + menu: IMenuData; + disclosure?: boolean; + placement?: MenuInitialState['placement']; + modal?: boolean; + visible?: boolean; + rtl?: boolean; + children?: React.ReactNode | ContextMenuRenderingChildren; + onVisibleSwitch?: (visible: boolean) => void; +} diff --git a/webapp/packages/core-ui/src/ContextMenu/MenuBar/IMenuBarProps.ts b/webapp/packages/core-ui/src/ContextMenu/MenuBar/IMenuBarProps.ts new file mode 100644 index 0000000000..495f7c66bd --- /dev/null +++ b/webapp/packages/core-ui/src/ContextMenu/MenuBar/IMenuBarProps.ts @@ -0,0 +1,20 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { MenuInitialState } from 'reakit'; + +import type { IMenuData } from '@cloudbeaver/core-view'; + +export interface IMenuBarNestedMenuSettings extends MenuInitialState { + onVisibleSwitch?: (visible: boolean) => void; +} + +export interface IMenuBarProps extends React.HTMLAttributes { + menu: IMenuData; + nestedMenuSettings?: IMenuBarNestedMenuSettings; + rtl?: boolean; +} diff --git a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx index ad2a8d49aa..6abbc2dd48 100644 --- a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBar.tsx @@ -7,7 +7,6 @@ */ import { observer } from 'mobx-react-lite'; import { forwardRef, useCallback } from 'react'; -import type { MenuInitialState } from 'reakit'; import { getComputed, MenuSeparator, MenuSeparatorStyles, s, SContext, StyleRegistry, useAutoLoad, useS } from '@cloudbeaver/core-blocks'; import { @@ -24,19 +23,10 @@ import { } from '@cloudbeaver/core-view'; import { ContextMenu } from '../ContextMenu'; +import type { IMenuBarNestedMenuSettings, IMenuBarProps } from './IMenuBarProps'; import style from './MenuBar.m.css'; import { MenuBarItem } from './MenuBarItem'; -interface INestedMenuSettings extends MenuInitialState { - onVisibleSwitch?: (visible: boolean) => void; -} - -interface IMenuBarProps extends React.HTMLAttributes { - menu: IMenuData; - nestedMenuSettings?: INestedMenuSettings; - rtl?: boolean; -} - const registry: StyleRegistry = [ [ MenuSeparatorStyles, @@ -72,7 +62,7 @@ export const MenuBar = observer( interface IMenuBarElementProps { item: IMenuItem; menuData: IMenuData; - nestedMenuSettings?: INestedMenuSettings; + nestedMenuSettings?: IMenuBarNestedMenuSettings; className?: string; rtl?: boolean; } @@ -155,7 +145,7 @@ const MenuBarAction = observer(function MenuBarAction({ ite interface ISubMenuItemProps { item: MenuSubMenuItem; menuData: IMenuData; - nestedMenuSettings?: INestedMenuSettings; + nestedMenuSettings?: IMenuBarNestedMenuSettings; className?: string; rtl?: boolean; } diff --git a/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBarLazy.ts b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBarLazy.ts new file mode 100644 index 0000000000..4a7d8280fb --- /dev/null +++ b/webapp/packages/core-ui/src/ContextMenu/MenuBar/MenuBarLazy.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +export const MenuBar = importLazyComponent(() => import('./MenuBar').then(m => m.MenuBar)); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserForm.m.css b/webapp/packages/core-ui/src/Form/Components/BaseForm.m.css similarity index 50% rename from webapp/packages/plugin-user-profile/src/UserProfileForm/UserForm.m.css rename to webapp/packages/core-ui/src/Form/Components/BaseForm.m.css index 4582cbe5f8..3f7d709ce4 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserForm.m.css +++ b/webapp/packages/core-ui/src/Form/Components/BaseForm.m.css @@ -1,14 +1,16 @@ -.tabList { - position: relative; - flex-shrink: 0; - align-items: center; -} +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ -.topBar { +.bar { composes: theme-border-color-background theme-background-secondary theme-text-on-secondary from global; position: relative; display: flex; - padding-top: 16px; + margin-bottom: 16px; &:before { content: ''; @@ -20,48 +22,24 @@ } } -.topBarTabs { - flex: 1; -} - -.topBarActions { +.tabList { display: flex; + position: relative; + flex-shrink: 0; align-items: center; - padding: 0 24px; - gap: 16px; -} - -/*Button:not(:first-child) { - margin-right: 24px; - }*/ - -.statusMessage { - composes: theme-typography--caption from global; - height: 24px; - padding: 0 16px; - display: flex; - align-items: center; - gap: 8px; - - & .iconOrImage { - height: 24px; - width: 24px; - } -} - -.box { - display: flex; - flex-direction: column; - flex: 1; - height: 100%; - overflow: auto; } .contentBox { - composes: theme-background-secondary theme-border-color-background from global; - position: relative; + composes: theme-background-secondary theme-text-on-secondary theme-border-color-background from global; display: flex; flex: 1; flex-direction: column; overflow: auto; } + +.submittingForm { + flex: 1; + overflow: auto; + display: flex; + flex-direction: column; +} diff --git a/webapp/packages/core-ui/src/Form/Components/BaseForm.tsx b/webapp/packages/core-ui/src/Form/Components/BaseForm.tsx new file mode 100644 index 0000000000..0060b0c6b1 --- /dev/null +++ b/webapp/packages/core-ui/src/Form/Components/BaseForm.tsx @@ -0,0 +1,71 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { Button, Container, Form, s, SContext, StatusMessage, useAutoLoad, useForm, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { getFirstException } from '@cloudbeaver/core-utils'; + +import { TabBigUnderlineStyleRegistry } from '../../Tabs/Tab/TabStyleRegistries'; +import { TabList } from '../../Tabs/TabList'; +import { TabPanelList } from '../../Tabs/TabPanelList'; +import { TabsState } from '../../Tabs/TabsState'; +import { FormMode } from '../FormMode'; +import style from './BaseForm.m.css'; +import type { IBaseFormProps } from './IBaseFormProps'; + +export const BaseForm = observer>(function BaseForm({ service, state, onClose, onSubmit }) { + const styles = useS(style); + const translate = useTranslate(); + + const editing = state.mode === FormMode.Edit; + const changed = state.isChanged(); + + const form = useForm({ + async onSubmit() { + const mode = state.mode; + const success = await state.save(); + + onSubmit?.({ + success, + creating: mode === FormMode.Create, + }); + }, + }); + + useAutoLoad(BaseForm, state); + + return ( +
+ + + + + + + + + + + {onClose && ( + + )} + + + + + + + + +
+ ); +}); diff --git a/webapp/packages/core-ui/src/Form/Components/BaseFormLazy.ts b/webapp/packages/core-ui/src/Form/Components/BaseFormLazy.ts new file mode 100644 index 0000000000..c6d75d4b59 --- /dev/null +++ b/webapp/packages/core-ui/src/Form/Components/BaseFormLazy.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +export const BaseForm = importLazyComponent(() => import('./BaseForm').then(m => m.BaseForm)); diff --git a/webapp/packages/core-ui/src/Form/Components/IBaseFormProps.ts b/webapp/packages/core-ui/src/Form/Components/IBaseFormProps.ts new file mode 100644 index 0000000000..d3b88797b2 --- /dev/null +++ b/webapp/packages/core-ui/src/Form/Components/IBaseFormProps.ts @@ -0,0 +1,21 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { FormBaseService } from '../FormBaseService'; +import type { IFormState } from '../IFormState'; + +export interface IBaseFormSubmitInfo { + success: boolean; + creating: boolean; +} + +export interface IBaseFormProps { + service: FormBaseService; + state: IFormState; + onSubmit?: (info: IBaseFormSubmitInfo) => void; + onClose?: () => void; +} diff --git a/webapp/packages/core-ui/src/Form/FormState.ts b/webapp/packages/core-ui/src/Form/FormState.ts index 1231bb7a56..9bcfc1ce3f 100644 --- a/webapp/packages/core-ui/src/Form/FormState.ts +++ b/webapp/packages/core-ui/src/Form/FormState.ts @@ -118,6 +118,10 @@ export class FormState implements IFormState { return this.dataContext.get(DATA_CONTEXT_LOADABLE_STATE).loaders.some(loader => loader.isCancelled?.() === true); } + isChanged(): boolean { + return Array.from(this.parts.values()).some(part => part.isChanged()); + } + async load(refresh?: boolean): Promise { if (this.promise !== null) { return this.promise; diff --git a/webapp/packages/core-ui/src/Form/IFormState.ts b/webapp/packages/core-ui/src/Form/IFormState.ts index 7bb85072b4..4b469260c8 100644 --- a/webapp/packages/core-ui/src/Form/IFormState.ts +++ b/webapp/packages/core-ui/src/Form/IFormState.ts @@ -46,6 +46,7 @@ export interface IFormState extends ILoadableState { isError(): boolean; isOutdated(): boolean; isCancelled(): boolean; + isChanged(): boolean; load(): Promise; reload(): Promise; diff --git a/webapp/packages/core-ui/src/Screens/AppScreen/OptionsPanelService.ts b/webapp/packages/core-ui/src/Screens/AppScreen/OptionsPanelService.ts index 305a7722ee..8ffec0fbd1 100644 --- a/webapp/packages/core-ui/src/Screens/AppScreen/OptionsPanelService.ts +++ b/webapp/packages/core-ui/src/Screens/AppScreen/OptionsPanelService.ts @@ -8,7 +8,7 @@ import { makeObservable, observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; -import { Executor, ExecutorInterrupter, IExecutor, IExecutorHandler } from '@cloudbeaver/core-executor'; +import { Executor, ExecutorInterrupter, IExecutionContext, IExecutor, IExecutorHandler } from '@cloudbeaver/core-executor'; import { NavigationService } from './NavigationService'; @@ -55,12 +55,12 @@ export class OptionsPanelService { return true; } - async close(): Promise { + async close(context?: IExecutionContext): Promise { if (this.panelComponent === null) { return true; } - const contexts = await this.closeTask.execute(); + const contexts = await this.closeTask.execute(undefined, context); const interrupted = contexts.getContext(ExecutorInterrupter.interruptContext); diff --git a/webapp/packages/core-ui/src/index.ts b/webapp/packages/core-ui/src/index.ts index bbcdda809c..cbcbf9b2dc 100644 --- a/webapp/packages/core-ui/src/index.ts +++ b/webapp/packages/core-ui/src/index.ts @@ -1,9 +1,9 @@ export * from './Clipboard/ClipboardBootstrap'; export * from './Clipboard/ClipboardService'; -export * from './ContextMenu/ContextMenu'; +export * from './ContextMenu/ContextMenuLazy'; export * from './ContextMenu/IContextMenuItemProps'; -export * from './ContextMenu/MenuBar/MenuBar'; +export * from './ContextMenu/MenuBar/MenuBarLazy'; export { default as MenuBarStyles } from './ContextMenu/MenuBar/MenuBar.m.css'; export { default as MenuBarItemStyles } from './ContextMenu/MenuBar/MenuBarItem.m.css'; @@ -15,6 +15,8 @@ export * from './DragAndDrop/DNDProvider'; export * from './DragAndDrop/useDNDBox'; export * from './DragAndDrop/useDNDData'; +export * from './Form/Components/IBaseFormProps'; +export * from './Form/Components/BaseFormLazy'; export * from './Form/DATA_CONTEXT_FORM_STATE'; export * from './Form/FormBaseService'; export * from './Form/FormMode'; diff --git a/webapp/packages/core-utils/src/parseJSONFlat.test.ts b/webapp/packages/core-utils/src/parseJSONFlat.test.ts index 5616cb6941..8333351e0c 100644 --- a/webapp/packages/core-utils/src/parseJSONFlat.test.ts +++ b/webapp/packages/core-utils/src/parseJSONFlat.test.ts @@ -1,6 +1,24 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ import { parseJSONFlat } from './parseJSONFlat'; describe('parseJSONFlat', () => { + it('should parse null values', () => { + const object = { + key: null, + }; + const setValue = jest.fn(); + + parseJSONFlat(object, setValue); + + expect(setValue).toHaveBeenCalledWith('key', null); + }); + it('should parse empty object', () => { const object = {}; const setValue = jest.fn(); diff --git a/webapp/packages/core-utils/src/parseJSONFlat.ts b/webapp/packages/core-utils/src/parseJSONFlat.ts index 9a113a569e..7b34f4033f 100644 --- a/webapp/packages/core-utils/src/parseJSONFlat.ts +++ b/webapp/packages/core-utils/src/parseJSONFlat.ts @@ -7,7 +7,7 @@ */ export function parseJSONFlat(object: any, setValue: (key: string, value: any) => void, scope?: string) { - if (typeof object === 'object') { + if (typeof object === 'object' && object !== null) { if (Array.isArray(object)) { if (scope) { setValue(scope, object); diff --git a/webapp/packages/core-utils/src/schema.ts b/webapp/packages/core-utils/src/schema.ts index 5afa14ae4d..1e04b848a2 100644 --- a/webapp/packages/core-utils/src/schema.ts +++ b/webapp/packages/core-utils/src/schema.ts @@ -1,3 +1,8 @@ -import z from 'zod'; +import * as z from 'zod'; +export const schemaExtra = { + stringedBoolean() { + return z.union([z.enum(['false', '0']).transform(() => false), z.boolean(), z.string(), z.number()]).pipe(z.coerce.boolean()); + }, +}; export { z as schema }; diff --git a/webapp/packages/core-utils/src/underscore.ts b/webapp/packages/core-utils/src/underscore.ts index 0007c85865..bb2e4c63f8 100644 --- a/webapp/packages/core-utils/src/underscore.ts +++ b/webapp/packages/core-utils/src/underscore.ts @@ -6,4 +6,4 @@ * you may not use this file except in compliance with the License. */ -export { isNull, isUndefined, isObject, uniq, groupBy, iteratee, values as objectValues, compose } from 'underscore'; +export { isNull, isUndefined, isObject, uniq, groupBy, iteratee, values as objectValues, compose, invert as invertObject } from 'underscore'; diff --git a/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/AuthenticationProviders.tsx b/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/AuthenticationProviders.tsx index 841c2423dc..7ce9f7758c 100644 --- a/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/AuthenticationProviders.tsx +++ b/webapp/packages/plugin-authentication-administration/src/Administration/ServerConfiguration/AuthenticationProviders.tsx @@ -50,7 +50,7 @@ export const AuthenticationProviders: PlaceholderComponent import('./DriverSelector/DriverSelectorDialog').then(m => m.DriverSelectorDialog)); @injectable() export class CustomConnectionPluginBootstrap extends Bootstrap { @@ -45,14 +47,12 @@ export class CustomConnectionPluginBootstrap extends Bootstrap { } if (action === ACTION_CONNECTION_CUSTOM) { - return this.customConnectionSettingsService.settings.getValue('disabled'); + return this.customConnectionSettingsService.disabled; } return false; }, - getLoader: (context, action) => { - return getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey); - }, + getLoader: (context, action) => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), handler: async (context, action) => { switch (action) { case ACTION_CONNECTION_CUSTOM: { diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnectionSettingsService.ts b/webapp/packages/plugin-connection-custom/src/CustomConnectionSettingsService.ts index da7fc59639..5bc5fc3bc8 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnectionSettingsService.ts +++ b/webapp/packages/plugin-connection-custom/src/CustomConnectionSettingsService.ts @@ -6,20 +6,26 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; -import { schema } from '@cloudbeaver/core-utils'; +import { SettingsManagerService, SettingsProvider, SettingsProviderService } from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; const settings = schema.object({ - disabled: schema.coerce.boolean().default(false), + 'plugin.connection-custom.disabled': schemaExtra.stringedBoolean().default(false), }); @injectable() export class CustomConnectionSettingsService extends Dependency { - readonly settings: PluginSettings; + get disabled(): boolean { + return this.settings.getValue('plugin.connection-custom.disabled'); + } + readonly settings: SettingsProvider; - constructor(private readonly pluginManagerService: PluginManagerService, private readonly settingsManagerService: SettingsManagerService) { + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + ) { super(); - this.settings = this.pluginManagerService.createSettings('connection-custom', 'plugin', settings); + this.settings = this.settingsProviderService.createSettings(settings); this.registerSettings(); } diff --git a/webapp/packages/plugin-connection-custom/tsconfig.json b/webapp/packages/plugin-connection-custom/tsconfig.json index d97ec04fab..0ae63b1262 100644 --- a/webapp/packages/plugin-connection-custom/tsconfig.json +++ b/webapp/packages/plugin-connection-custom/tsconfig.json @@ -24,15 +24,15 @@ { "path": "../core-localization/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, { "path": "../core-projects/tsconfig.json" }, { "path": "../core-resource/tsconfig.json" }, + { + "path": "../core-settings/tsconfig.json" + }, { "path": "../core-utils/tsconfig.json" }, diff --git a/webapp/packages/plugin-connection-search/package.json b/webapp/packages/plugin-connection-search/package.json index c47e2ea8c8..a7fa2ef6ab 100644 --- a/webapp/packages/plugin-connection-search/package.json +++ b/webapp/packages/plugin-connection-search/package.json @@ -24,11 +24,11 @@ "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-executor": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", diff --git a/webapp/packages/plugin-connection-search/src/ConnectionSearchSettingsService.ts b/webapp/packages/plugin-connection-search/src/ConnectionSearchSettingsService.ts index 961a5f2db7..5fb8ef495f 100644 --- a/webapp/packages/plugin-connection-search/src/ConnectionSearchSettingsService.ts +++ b/webapp/packages/plugin-connection-search/src/ConnectionSearchSettingsService.ts @@ -6,20 +6,26 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; -import { schema } from '@cloudbeaver/core-utils'; +import { SettingsManagerService, SettingsProvider, SettingsProviderService } from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; const settings = schema.object({ - disabled: schema.coerce.boolean().default(false), + 'plugin.connection-search.disabled': schemaExtra.stringedBoolean().default(false), }); @injectable() export class ConnectionSearchSettingsService extends Dependency { - readonly settings: PluginSettings; + get disabled(): boolean { + return this.settings.getValue('plugin.connection-search.disabled'); + } + readonly settings: SettingsProvider; - constructor(private readonly pluginManagerService: PluginManagerService, private readonly settingsManagerService: SettingsManagerService) { + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + ) { super(); - this.settings = this.pluginManagerService.createSettings('connection-search', 'plugin', settings); + this.settings = this.settingsProviderService.createSettings(settings); this.registerSettings(); } diff --git a/webapp/packages/plugin-connection-search/src/SearchConnectionPluginBootstrap.ts b/webapp/packages/plugin-connection-search/src/SearchConnectionPluginBootstrap.ts index 70c9eaa64d..8365144fdf 100644 --- a/webapp/packages/plugin-connection-search/src/SearchConnectionPluginBootstrap.ts +++ b/webapp/packages/plugin-connection-search/src/SearchConnectionPluginBootstrap.ts @@ -46,7 +46,7 @@ export class SearchConnectionPluginBootstrap extends Bootstrap { } if (action === ACTION_CONNECTION_SEARCH) { - return this.connectionSearchSettingsService.settings.getValue('disabled'); + return this.connectionSearchSettingsService.disabled; } return false; diff --git a/webapp/packages/plugin-connection-search/tsconfig.json b/webapp/packages/plugin-connection-search/tsconfig.json index 06a68f6d37..8ce25c5e66 100644 --- a/webapp/packages/plugin-connection-search/tsconfig.json +++ b/webapp/packages/plugin-connection-search/tsconfig.json @@ -27,9 +27,6 @@ { "path": "../core-localization/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, { "path": "../core-projects/tsconfig.json" }, @@ -42,6 +39,9 @@ { "path": "../core-sdk/tsconfig.json" }, + { + "path": "../core-settings/tsconfig.json" + }, { "path": "../core-ui/tsconfig.json" }, diff --git a/webapp/packages/plugin-connections/package.json b/webapp/packages/plugin-connections/package.json index a22c87712b..05eb59f0c3 100644 --- a/webapp/packages/plugin-connections/package.json +++ b/webapp/packages/plugin-connections/package.json @@ -27,11 +27,11 @@ "@cloudbeaver/core-executor": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", diff --git a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProviderPropertiesForm.tsx b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProviderPropertiesForm.tsx index 5b612dc04d..e00cedbe10 100644 --- a/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProviderPropertiesForm.tsx +++ b/webapp/packages/plugin-connections/src/ConnectionForm/Options/ProviderPropertiesForm.tsx @@ -10,7 +10,6 @@ import { observer } from 'mobx-react-lite'; import { Container, Expandable, - getPropertyControlType, Group, GroupTitle, InputField, @@ -18,7 +17,7 @@ import { useObjectPropertyCategories, useTranslate, } from '@cloudbeaver/core-blocks'; -import type { ConnectionConfig, DriverProviderPropertyInfoFragment } from '@cloudbeaver/core-sdk'; +import { type ConnectionConfig, type DriverProviderPropertyInfoFragment, getObjectPropertyType } from '@cloudbeaver/core-sdk'; type DriverProviderPropertyInfo = DriverProviderPropertyInfoFragment; @@ -104,7 +103,7 @@ export const ProviderPropertiesForm = observer(function ProviderPropertie category={category} disabled={disabled} readOnly={readonly} - geLayoutSize={property => (getPropertyControlType(property) === 'checkbox' ? { maximum: true } : { small: true, noGrow: true })} + geLayoutSize={property => (getObjectPropertyType(property) === 'checkbox' ? { maximum: true } : { small: true, noGrow: true })} hideEmptyPlaceholder /> diff --git a/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts b/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts index babbe9bfbf..9a6179ae45 100644 --- a/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts +++ b/webapp/packages/plugin-connections/src/ContextMenu/ConnectionMenuBootstrap.ts @@ -63,10 +63,7 @@ export class ConnectionMenuBootstrap extends Bootstrap { this.menuService.addCreator({ isApplicable: context => { - if ( - this.pluginConnectionsSettingsService.settings.getValue('hideConnectionViewForUsers') && - !this.permissionsService.has(EAdminPermission.admin) - ) { + if (this.pluginConnectionsSettingsService.hideConnectionViewForUsers && !this.permissionsService.has(EAdminPermission.admin)) { return false; } @@ -273,8 +270,7 @@ export class ConnectionMenuBootstrap extends Bootstrap { this.menuService.setHandler({ id: 'connections-menu-base', isApplicable: context => context.tryGet(DATA_CONTEXT_MENU) === MENU_CONNECTIONS, - isHidden: () => - this.connectionsManagerService.createConnectionProjects.length === 0 || this.connectionsSettingsService.settings.getValue('disabled'), + isHidden: () => this.connectionsManagerService.createConnectionProjects.length === 0 || this.connectionsSettingsService.disabled, isLabelVisible: () => false, }); } diff --git a/webapp/packages/plugin-connections/src/PluginConnectionsSettingsService.ts b/webapp/packages/plugin-connections/src/PluginConnectionsSettingsService.ts index cb37406bd9..2ea00ee24b 100644 --- a/webapp/packages/plugin-connections/src/PluginConnectionsSettingsService.ts +++ b/webapp/packages/plugin-connections/src/PluginConnectionsSettingsService.ts @@ -6,22 +6,28 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; -import { schema } from '@cloudbeaver/core-utils'; +import { SettingsManagerService, SettingsProvider, SettingsProviderService } from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; const defaultSettings = schema.object({ - hideConnectionViewForUsers: schema.coerce.boolean().default(false), + 'plugin.connections.hideConnectionViewForUsers': schemaExtra.stringedBoolean().default(false), }); export type PluginConnectionsSettings = schema.infer; @injectable() export class PluginConnectionsSettingsService extends Dependency { - readonly settings: PluginSettings; + get hideConnectionViewForUsers(): boolean { + return this.settings.getValue('plugin.connections.hideConnectionViewForUsers'); + } + readonly settings: SettingsProvider; - constructor(private readonly pluginManagerService: PluginManagerService, private readonly settingsManagerService: SettingsManagerService) { + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + ) { super(); - this.settings = this.pluginManagerService.createSettings('connections', 'plugin', defaultSettings); + this.settings = this.settingsProviderService.createSettings(defaultSettings); this.registerSettings(); } diff --git a/webapp/packages/plugin-connections/tsconfig.json b/webapp/packages/plugin-connections/tsconfig.json index e1713029a0..f680bb9930 100644 --- a/webapp/packages/plugin-connections/tsconfig.json +++ b/webapp/packages/plugin-connections/tsconfig.json @@ -36,9 +36,6 @@ { "path": "../core-navigation-tree/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, { "path": "../core-projects/tsconfig.json" }, @@ -51,6 +48,9 @@ { "path": "../core-sdk/tsconfig.json" }, + { + "path": "../core-settings/tsconfig.json" + }, { "path": "../core-ui/tsconfig.json" }, diff --git a/webapp/packages/plugin-data-export/package.json b/webapp/packages/plugin-data-export/package.json index 7b312d55fd..977a609e6e 100644 --- a/webapp/packages/plugin-data-export/package.json +++ b/webapp/packages/plugin-data-export/package.json @@ -25,10 +25,10 @@ "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", @@ -50,17 +50,14 @@ "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-product": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-routing": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", - "@cloudbeaver/core-theming": "~0.1.0", + "@cloudbeaver/core-storage": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", - "@cloudbeaver/plugin-data-viewer": "~0.1.0", "@cloudbeaver/plugin-datasource-context-switch": "~0.1.0", "@cloudbeaver/plugin-navigation-tabs": "~0.1.0", "@cloudbeaver/plugin-navigation-tree": "~0.1.0", diff --git a/webapp/packages/plugin-data-export/src/DATA_EXPORT_SETTINGS_GROUP.ts b/webapp/packages/plugin-data-export/src/DATA_EXPORT_SETTINGS_GROUP.ts index 526764c045..991978c9df 100644 --- a/webapp/packages/plugin-data-export/src/DATA_EXPORT_SETTINGS_GROUP.ts +++ b/webapp/packages/plugin-data-export/src/DATA_EXPORT_SETTINGS_GROUP.ts @@ -5,6 +5,6 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-plugin'; +import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-settings'; export const DATA_EXPORT_SETTINGS_GROUP = ROOT_SETTINGS_GROUP.createSubGroup('plugin_data_export_data_export_settings_group'); diff --git a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts index 3cd8d03a4d..83e696221d 100644 --- a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts +++ b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts @@ -146,6 +146,6 @@ export class DataExportMenuService { return false; } - return this.dataExportSettingsService.settings.getValue('disabled'); + return this.dataExportSettingsService.disabled; } } diff --git a/webapp/packages/plugin-data-export/src/DataExportSettingsService.test.ts b/webapp/packages/plugin-data-export/src/DataExportSettingsService.test.ts index a312548387..b8bbf3fcfb 100644 --- a/webapp/packages/plugin-data-export/src/DataExportSettingsService.test.ts +++ b/webapp/packages/plugin-data-export/src/DataExportSettingsService.test.ts @@ -18,41 +18,43 @@ import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; import { coreProjectsManifest } from '@cloudbeaver/core-projects'; import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; +import '@cloudbeaver/core-root/dist/__custom_mocks__/expectWebsocketClosedMessage'; import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; import { coreRoutingManifest } from '@cloudbeaver/core-routing'; import { coreSDKManifest } from '@cloudbeaver/core-sdk'; import { coreSettingsManifest } from '@cloudbeaver/core-settings'; -import { coreThemingManifest } from '@cloudbeaver/core-theming'; +import { + expectDeprecatedSettingMessage, + expectNoDeprecatedSettingMessage, +} from '@cloudbeaver/core-settings/dist/__custom_mocks__/expectDeprecatedSettingMessage'; +import { coreStorageManifest } from '@cloudbeaver/core-storage'; import { coreUIManifest } from '@cloudbeaver/core-ui'; import { coreViewManifest } from '@cloudbeaver/core-view'; -import { dataViewerManifest } from '@cloudbeaver/plugin-data-viewer'; import { datasourceContextSwitchPluginManifest } from '@cloudbeaver/plugin-datasource-context-switch'; import { navigationTabsPlugin } from '@cloudbeaver/plugin-navigation-tabs'; import { navigationTreePlugin } from '@cloudbeaver/plugin-navigation-tree'; import { objectViewerManifest } from '@cloudbeaver/plugin-object-viewer'; import { createApp } from '@cloudbeaver/tests-runner'; -import { DataExportSettings, DataExportSettingsService } from './DataExportSettingsService'; +import { DataExportSettingsService } from './DataExportSettingsService'; import { dataExportManifest } from './manifest'; const endpoint = createGQLEndpoint(); +const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); const app = createApp( dataExportManifest, coreLocalizationManifest, coreEventsManifest, - corePluginManifest, - coreProductManifest, coreRootManifest, coreSDKManifest, coreBrowserManifest, coreSettingsManifest, + coreStorageManifest, coreViewManifest, coreAuthenticationManifest, coreProjectsManifest, @@ -63,35 +65,23 @@ const app = createApp( coreDialogsManifest, coreNavigationTree, coreAppManifest, - coreThemingManifest, datasourceContextSwitchPluginManifest, navigationTreePlugin, navigationTabsPlugin, objectViewerManifest, - dataViewerManifest, coreClientActivityManifest, ); -const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); - -beforeAll(() => app.init()); - const testValueA = true; const testValueB = true; const deprecatedSettings = { - plugin_data_export: { - disabled: testValueB, - } as DataExportSettings, + 'plugin_data_export.disabled': testValueB, }; const newSettings = { ...deprecatedSettings, - plugin: { - 'data-export': { - disabled: testValueA, - } as DataExportSettings, - }, + 'plugin.data-export.disabled': testValueA, }; test('New settings override deprecated', async () => { @@ -102,7 +92,8 @@ test('New settings override deprecated', async () => { await config.refresh(); - expect(settings.settings.getValue('disabled')).toBe(testValueA); + expect(settings.disabled).toBe(testValueA); + expectNoDeprecatedSettingMessage(); }); test('Deprecated settings are used if new settings are not defined', async () => { @@ -113,5 +104,6 @@ test('Deprecated settings are used if new settings are not defined', async () => await config.refresh(); - expect(settings.settings.getValue('disabled')).toBe(testValueB); + expect(settings.disabled).toBe(testValueB); + expectDeprecatedSettingMessage(); }); diff --git a/webapp/packages/plugin-data-export/src/DataExportSettingsService.ts b/webapp/packages/plugin-data-export/src/DataExportSettingsService.ts index dbf119d689..5210503545 100644 --- a/webapp/packages/plugin-data-export/src/DataExportSettingsService.ts +++ b/webapp/packages/plugin-data-export/src/DataExportSettingsService.ts @@ -6,31 +6,40 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { createSettingsAliasResolver, PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; -import { ServerSettingsResolverService, ServerSettingsService } from '@cloudbeaver/core-root'; -import { schema } from '@cloudbeaver/core-utils'; +import { + createSettingsAliasResolver, + ROOT_SETTINGS_LAYER, + SettingsManagerService, + SettingsProvider, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; const defaultSettings = schema.object({ - disabled: schema.coerce.boolean().default(false), + 'plugin.data-export.disabled': schemaExtra.stringedBoolean().default(false), }); export type DataExportSettings = schema.infer; @injectable() export class DataExportSettingsService extends Dependency { - readonly settings: PluginSettings; + get disabled(): boolean { + return this.settings.getValue('plugin.data-export.disabled'); + } + readonly settings: SettingsProvider; constructor( - private readonly pluginManagerService: PluginManagerService, + private readonly settingsProviderService: SettingsProviderService, private readonly settingsManagerService: SettingsManagerService, - private readonly serverSettingsService: ServerSettingsService, - private readonly serverSettingsResolverService: ServerSettingsResolverService, + private readonly settingsResolverService: SettingsResolverService, ) { super(); - this.settings = this.pluginManagerService.createSettings('data-export', 'plugin', defaultSettings); - this.serverSettingsResolverService.addResolver( + this.settings = this.settingsProviderService.createSettings(defaultSettings); + this.settingsResolverService.addResolver( + ROOT_SETTINGS_LAYER, /** @deprecated Use settings instead, will be removed in 23.0.0 */ - createSettingsAliasResolver(this.serverSettingsService, this.settings, 'plugin_data_export'), + createSettingsAliasResolver(this.settingsResolverService, this.settings, { 'plugin.data-export.disabled': 'plugin_data_export.disabled' }), ); this.registerSettings(); diff --git a/webapp/packages/plugin-data-export/tsconfig.json b/webapp/packages/plugin-data-export/tsconfig.json index 9c21a8add0..c4c69c86c9 100644 --- a/webapp/packages/plugin-data-export/tsconfig.json +++ b/webapp/packages/plugin-data-export/tsconfig.json @@ -57,15 +57,6 @@ { "path": "../core-navigation-tree/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-product/tsconfig.json" - }, { "path": "../core-projects/tsconfig.json" }, @@ -91,7 +82,10 @@ "path": "../core-settings/tsconfig.json" }, { - "path": "../core-theming/tsconfig.json" + "path": "../core-settings/tsconfig.json" + }, + { + "path": "../core-storage/tsconfig.json" }, { "path": "../core-ui/tsconfig.json" @@ -111,9 +105,6 @@ { "path": "../plugin-data-viewer/tsconfig.json" }, - { - "path": "../plugin-data-viewer/tsconfig.json" - }, { "path": "../plugin-datasource-context-switch/tsconfig.json" }, diff --git a/webapp/packages/plugin-data-spreadsheet-new/package.json b/webapp/packages/plugin-data-spreadsheet-new/package.json index 8adf9bd91c..c044aebfab 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/package.json +++ b/webapp/packages/plugin-data-spreadsheet-new/package.json @@ -26,9 +26,8 @@ "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-executor": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-theming": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", @@ -54,13 +53,12 @@ "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-product": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-routing": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", + "@cloudbeaver/core-storage": "~0.1.0", "@cloudbeaver/core-theming": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.test.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.test.ts index 99ffe65f11..dd09e44af3 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.test.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.test.ts @@ -18,18 +18,21 @@ import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; import { coreProjectsManifest } from '@cloudbeaver/core-projects'; import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; +import '@cloudbeaver/core-root/dist/__custom_mocks__/expectWebsocketClosedMessage'; import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; import { coreRoutingManifest } from '@cloudbeaver/core-routing'; import { coreSDKManifest } from '@cloudbeaver/core-sdk'; import { coreSettingsManifest } from '@cloudbeaver/core-settings'; -import { coreThemingManifest } from '@cloudbeaver/core-theming'; +import { + expectDeprecatedSettingMessage, + expectNoDeprecatedSettingMessage, +} from '@cloudbeaver/core-settings/dist/__custom_mocks__/expectDeprecatedSettingMessage'; +import { coreStorageManifest } from '@cloudbeaver/core-storage'; import { coreUIManifest } from '@cloudbeaver/core-ui'; import { coreViewManifest } from '@cloudbeaver/core-view'; import { dataViewerManifest } from '@cloudbeaver/plugin-data-viewer'; @@ -39,21 +42,21 @@ import { navigationTreePlugin } from '@cloudbeaver/plugin-navigation-tree'; import { objectViewerManifest } from '@cloudbeaver/plugin-object-viewer'; import { createApp } from '@cloudbeaver/tests-runner'; -import { DataGridSettings, DataGridSettingsService } from './DataGridSettingsService'; +import { DataGridSettingsService } from './DataGridSettingsService'; import { dataSpreadsheetNewManifest } from './manifest'; const endpoint = createGQLEndpoint(); +const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); const app = createApp( dataSpreadsheetNewManifest, coreLocalizationManifest, coreEventsManifest, - corePluginManifest, - coreProductManifest, coreRootManifest, coreSDKManifest, coreBrowserManifest, coreSettingsManifest, coreViewManifest, + coreStorageManifest, coreAuthenticationManifest, coreProjectsManifest, coreUIManifest, @@ -63,7 +66,6 @@ const app = createApp( coreDialogsManifest, coreNavigationTree, coreAppManifest, - coreThemingManifest, datasourceContextSwitchPluginManifest, navigationTreePlugin, navigationTabsPlugin, @@ -72,26 +74,16 @@ const app = createApp( coreClientActivityManifest, ); -const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); - -beforeAll(() => app.init()); - const testValueDeprecated = true; const testValueNew = false; const deprecatedSettings = { - plugin_data_spreadsheet_new: { - hidden: testValueDeprecated, - } as DataGridSettings, + 'plugin_data_spreadsheet_new.hidden': testValueDeprecated, }; const newSettings = { ...deprecatedSettings, - plugin: { - 'data-spreadsheet': { - hidden: testValueNew, - } as DataGridSettings, - }, + 'plugin.data-spreadsheet.hidden': testValueNew, }; test('New settings override deprecated', async () => { @@ -102,7 +94,8 @@ test('New settings override deprecated', async () => { await config.refresh(); - expect(settings.settings.getValue('hidden')).toBe(testValueNew); + expect(settings.hidden).toBe(testValueNew); + expectNoDeprecatedSettingMessage(); }); test('Deprecated settings are used if new settings are not defined', async () => { @@ -113,5 +106,6 @@ test('Deprecated settings are used if new settings are not defined', async () => await config.refresh(); - expect(settings.settings.getValue('hidden')).toBe(testValueDeprecated); + expect(settings.hidden).toBe(testValueDeprecated); + expectDeprecatedSettingMessage(); }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.ts b/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.ts index 456803cddc..9204466e14 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGridSettingsService.ts @@ -9,35 +9,41 @@ import { Dependency, injectable } from '@cloudbeaver/core-di'; import { createSettingsAliasResolver, ESettingsValueType, - PluginManagerService, - PluginSettings, + ROOT_SETTINGS_LAYER, SettingsManagerService, -} from '@cloudbeaver/core-plugin'; -import { ServerSettingsResolverService, ServerSettingsService } from '@cloudbeaver/core-root'; -import { schema } from '@cloudbeaver/core-utils'; + SettingsProvider, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; import { DATA_EDITOR_SETTINGS_GROUP } from '@cloudbeaver/plugin-data-viewer'; const defaultSettings = schema.object({ - hidden: schema.coerce.boolean().default(false), + 'plugin.data-spreadsheet.hidden': schemaExtra.stringedBoolean().default(false), }); export type DataGridSettings = schema.infer; @injectable() export class DataGridSettingsService extends Dependency { - readonly settings: PluginSettings; + get hidden(): boolean { + return this.settings.getValue('plugin.data-spreadsheet.hidden'); + } + readonly settings: SettingsProvider; constructor( - private readonly pluginManagerService: PluginManagerService, + private readonly settingsProviderService: SettingsProviderService, private readonly settingsManagerService: SettingsManagerService, - private readonly serverSettingsService: ServerSettingsService, - private readonly serverSettingsResolverService: ServerSettingsResolverService, + private readonly settingsResolverService: SettingsResolverService, ) { super(); - this.settings = this.pluginManagerService.createSettings('data-spreadsheet', 'plugin', defaultSettings); - this.serverSettingsResolverService.addResolver( + this.settings = this.settingsProviderService.createSettings(defaultSettings); + this.settingsResolverService.addResolver( + ROOT_SETTINGS_LAYER, /** @deprecated Use settings instead, will be removed in 23.0.0 */ - createSettingsAliasResolver(this.serverSettingsService, this.settings, 'plugin_data_spreadsheet_new'), + createSettingsAliasResolver(this.settingsResolverService, this.settings, { + 'plugin.data-spreadsheet.hidden': 'plugin_data_spreadsheet_new.hidden', + }), ); this.registerSettings(); @@ -47,7 +53,10 @@ export class DataGridSettingsService extends Dependency { this.settingsManagerService.registerSettings(this.settings, () => [ { group: DATA_EDITOR_SETTINGS_GROUP, - key: 'hidden', + key: 'plugin.data-spreadsheet.hidden', + access: { + scope: ['server'], + }, type: ESettingsValueType.Checkbox, name: 'plugin_data_spreadsheet_new_settings_disable', }, diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts index 7a5726b7c4..d62444e410 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts +++ b/webapp/packages/plugin-data-spreadsheet-new/src/SpreadsheetBootstrap.ts @@ -41,7 +41,7 @@ export class SpreadsheetBootstrap extends Bootstrap { id: 'spreadsheet_grid', dataFormat: ResultDataFormat.Resultset, getPresentationComponent: () => SpreadsheetGrid, - hidden: () => this.dataGridSettingsService.settings.getValue('hidden'), + hidden: () => this.dataGridSettingsService.hidden, title: 'Table', icon: 'table-icon-sm', }); diff --git a/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json b/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json index 5a51933c77..a8baaa9d60 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json +++ b/webapp/packages/plugin-data-spreadsheet-new/tsconfig.json @@ -60,24 +60,12 @@ { "path": "../core-navigation-tree/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-product/tsconfig.json" - }, { "path": "../core-projects/tsconfig.json" }, { "path": "../core-root/tsconfig.json" }, - { - "path": "../core-root/tsconfig.json" - }, { "path": "../core-routing/tsconfig.json" }, @@ -90,6 +78,12 @@ { "path": "../core-settings/tsconfig.json" }, + { + "path": "../core-settings/tsconfig.json" + }, + { + "path": "../core-storage/tsconfig.json" + }, { "path": "../core-theming/tsconfig.json" }, diff --git a/webapp/packages/plugin-data-viewer/package.json b/webapp/packages/plugin-data-viewer/package.json index f2cf01946e..6f5df7fc7e 100644 --- a/webapp/packages/plugin-data-viewer/package.json +++ b/webapp/packages/plugin-data-viewer/package.json @@ -28,10 +28,10 @@ "@cloudbeaver/core-executor": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", @@ -55,14 +55,12 @@ "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-product": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-routing": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", - "@cloudbeaver/core-theming": "~0.1.0", + "@cloudbeaver/core-storage": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", "@cloudbeaver/plugin-datasource-context-switch": "~0.1.0", diff --git a/webapp/packages/plugin-data-viewer/src/DATA_EDITOR_SETTINGS_GROUP.ts b/webapp/packages/plugin-data-viewer/src/DATA_EDITOR_SETTINGS_GROUP.ts index af6a66c737..758da1127b 100644 --- a/webapp/packages/plugin-data-viewer/src/DATA_EDITOR_SETTINGS_GROUP.ts +++ b/webapp/packages/plugin-data-viewer/src/DATA_EDITOR_SETTINGS_GROUP.ts @@ -5,6 +5,6 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-plugin'; +import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-settings'; export const DATA_EDITOR_SETTINGS_GROUP = ROOT_SETTINGS_GROUP.createSubGroup('plugin_data_viewer_data_viewer_settings_group'); diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerService.ts index c72a5278c4..1eb2cc78b3 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerService.ts @@ -18,7 +18,7 @@ export class DataViewerService { return true; } - return !this.dataViewerSettingsService.settings.getValue('disableCopyData'); + return !this.dataViewerSettingsService.disableCopyData; } constructor( @@ -32,7 +32,7 @@ export class DataViewerService { } const isAdmin = this.sessionPermissionsResource.has(EAdminPermission.admin); - const disabled = this.dataViewerSettingsService.settings.getValue('disableEdit'); + const disabled = this.dataViewerSettingsService.disableEdit; return isAdmin || !disabled; } diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts index 522eac178f..e93a0de0cc 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.test.ts @@ -18,18 +18,21 @@ import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; import { coreProjectsManifest } from '@cloudbeaver/core-projects'; import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; +import '@cloudbeaver/core-root/dist/__custom_mocks__/expectWebsocketClosedMessage'; import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; import { coreRoutingManifest } from '@cloudbeaver/core-routing'; import { coreSDKManifest } from '@cloudbeaver/core-sdk'; import { coreSettingsManifest } from '@cloudbeaver/core-settings'; -import { coreThemingManifest } from '@cloudbeaver/core-theming'; +import { + expectDeprecatedSettingMessage, + expectNoDeprecatedSettingMessage, +} from '@cloudbeaver/core-settings/dist/__custom_mocks__/expectDeprecatedSettingMessage'; +import { coreStorageManifest } from '@cloudbeaver/core-storage'; import { coreUIManifest } from '@cloudbeaver/core-ui'; import { coreViewManifest } from '@cloudbeaver/core-view'; import { datasourceContextSwitchPluginManifest } from '@cloudbeaver/plugin-datasource-context-switch'; @@ -38,19 +41,20 @@ import { navigationTreePlugin } from '@cloudbeaver/plugin-navigation-tree'; import { objectViewerManifest } from '@cloudbeaver/plugin-object-viewer'; import { createApp } from '@cloudbeaver/tests-runner'; -import { DataViewerSettings, DataViewerSettingsService } from './DataViewerSettingsService'; +import { DataViewerSettingsService } from './DataViewerSettingsService'; import { dataViewerManifest } from './manifest'; const endpoint = createGQLEndpoint(); +const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); const app = createApp( dataViewerManifest, coreLocalizationManifest, coreEventsManifest, - corePluginManifest, - coreProductManifest, - coreRootManifest, coreSDKManifest, + coreClientActivityManifest, + coreRootManifest, coreBrowserManifest, + coreStorageManifest, coreSettingsManifest, coreViewManifest, coreAuthenticationManifest, @@ -62,34 +66,22 @@ const app = createApp( coreDialogsManifest, coreNavigationTree, coreAppManifest, - coreThemingManifest, datasourceContextSwitchPluginManifest, navigationTreePlugin, navigationTabsPlugin, objectViewerManifest, - coreClientActivityManifest, ); -const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); - -beforeAll(() => app.init()); - const testValueDeprecated = true; const testValueNew = false; const deprecatedSettings = { - 'core.app.dataViewer': { - disableEdit: testValueDeprecated, - } as DataViewerSettings, + 'core.app.dataViewer.disableEdit': testValueDeprecated, }; const newSettings = { ...deprecatedSettings, - plugin: { - 'data-viewer': { - disableEdit: testValueNew, - } as DataViewerSettings, - }, + 'plugin.data-viewer.disableEdit': testValueNew, }; async function setupSettingsService(mockConfig: any = {}) { @@ -106,13 +98,15 @@ async function setupSettingsService(mockConfig: any = {}) { test('New settings override deprecated settings', async () => { const settingsService = await setupSettingsService(newSettings); - expect(settingsService.settings.getValue('disableEdit')).toBe(testValueNew); + expect(settingsService.disableEdit).toBe(testValueNew); + expectNoDeprecatedSettingMessage(); }); test('Deprecated settings are used if new settings are not defined', async () => { const settingsService = await setupSettingsService(deprecatedSettings); - expect(settingsService.settings.getValue('disableEdit')).toBe(testValueDeprecated); + expect(settingsService.disableEdit).toBe(testValueDeprecated); + expectDeprecatedSettingMessage(); }); describe('DataViewerSettingsService.getDefaultRowsCount', () => { @@ -120,13 +114,9 @@ describe('DataViewerSettingsService.getDefaultRowsCount', () => { beforeAll(async () => { settingsService = await setupSettingsService({ - plugin: { - 'data-viewer': { - fetchMin: 200, - fetchMax: 1000, - fetchDefault: 300, - }, - }, + 'plugin.data-viewer.fetchMin': '200', + 'plugin.data-viewer.fetchMax': '1000', + 'plugin.data-viewer.fetchDefault': '300', }); }); diff --git a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts index 8e47e20604..d409689046 100644 --- a/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts +++ b/webapp/packages/plugin-data-viewer/src/DataViewerSettingsService.ts @@ -6,104 +6,159 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; +import { ServerSettingsManagerService } from '@cloudbeaver/core-root'; import { createSettingsAliasResolver, ESettingsValueType, - PluginManagerService, - PluginSettings, + ISettingDescription, + ROOT_SETTINGS_LAYER, SettingsManagerService, -} from '@cloudbeaver/core-plugin'; -import { ServerSettingsResolverService, ServerSettingsService } from '@cloudbeaver/core-root'; -import { schema } from '@cloudbeaver/core-utils'; + SettingsProvider, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; import { DATA_EDITOR_SETTINGS_GROUP } from './DATA_EDITOR_SETTINGS_GROUP'; const defaultSettings = schema.object({ - disableEdit: schema.coerce.boolean().default(false), - disableCopyData: schema.coerce.boolean().default(false), - fetchMin: schema.coerce.number().min(10).default(100), - fetchMax: schema.coerce.number().min(10).default(5000), - fetchDefault: schema.coerce.number().min(10).default(200), + 'plugin.data-viewer.disableEdit': schemaExtra.stringedBoolean().default(false), + 'plugin.data-viewer.disableCopyData': schemaExtra.stringedBoolean().default(false), + 'plugin.data-viewer.fetchMin': schema.coerce.number().min(10).default(100), + 'plugin.data-viewer.fetchMax': schema.coerce.number().min(10).default(5000), + 'resultset.maxrows': schema.coerce.number().min(10).default(200), }); export type DataViewerSettings = schema.infer; @injectable() export class DataViewerSettingsService extends Dependency { - readonly settings: PluginSettings; + get disableEdit(): boolean { + return this.settings.getValue('plugin.data-viewer.disableEdit'); + } + + get disableCopyData(): boolean { + return this.settings.getValue('plugin.data-viewer.disableCopyData'); + } + + get maxFetchSize(): number { + return this.settings.getValue('plugin.data-viewer.fetchMax'); + } + + get minFetchSize(): number { + return this.settings.getValue('plugin.data-viewer.fetchMin'); + } + + get defaultFetchSize(): number { + return this.settings.getValue('resultset.maxrows'); + } + + readonly settings: SettingsProvider; constructor( - private readonly pluginManagerService: PluginManagerService, + private readonly settingsProviderService: SettingsProviderService, private readonly settingsManagerService: SettingsManagerService, - private readonly serverSettingsService: ServerSettingsService, - private readonly serverSettingsResolverService: ServerSettingsResolverService, + private readonly settingsResolverService: SettingsResolverService, + private readonly serverSettingsManagerService: ServerSettingsManagerService, ) { super(); - this.settings = this.pluginManagerService.createSettings('data-viewer', 'plugin', defaultSettings); - this.serverSettingsResolverService.addResolver( + this.settings = this.settingsProviderService.createSettings(defaultSettings); + this.settingsResolverService.addResolver( + ROOT_SETTINGS_LAYER, /** @deprecated Use settings instead, will be removed in 23.0.0 */ - createSettingsAliasResolver(this.serverSettingsService, this.settings, 'core.app.dataViewer'), + createSettingsAliasResolver(this.settingsResolverService, this.settings, { + 'plugin.data-viewer.disableEdit': 'core.app.dataViewer.disableEdit', + 'plugin.data-viewer.disableCopyData': 'core.app.dataViewer.disableCopyData', + 'plugin.data-viewer.fetchMin': 'core.app.dataViewer.fetchMin', + 'plugin.data-viewer.fetchMax': 'core.app.dataViewer.fetchMax', + 'resultset.maxrows': 'core.app.dataViewer.fetchDefault', + }), + /** @deprecated Use settings instead, will be removed in 25.0.0 */ + createSettingsAliasResolver(this.settingsResolverService, this.settings, { + 'resultset.maxrows': 'plugin.data-viewer.fetchDefault', + }), ); this.registerSettings(); } - getMaxFetchSize(): number { - return this.settings.getValue('fetchMax'); - } - - getMinFetchSize(): number { - return this.settings.getValue('fetchMin'); - } - - getDefaultFetchSize(): number { - return this.settings.getValue('fetchDefault'); - } - getDefaultRowsCount(count?: number): number { if (typeof count === 'number' && Number.isNaN(count)) { count = 0; } - return count !== undefined ? Math.max(this.getMinFetchSize(), Math.min(count, this.getMaxFetchSize())) : this.getDefaultFetchSize(); + return count !== undefined ? Math.max(this.minFetchSize, Math.min(count, this.maxFetchSize)) : this.defaultFetchSize; } private registerSettings() { - this.settingsManagerService.registerSettings(this.settings, () => [ - { - key: 'disableEdit', - type: ESettingsValueType.Checkbox, - name: 'settings_data_editor_disable_edit_name', - description: 'settings_data_editor_disable_edit_description', - group: DATA_EDITOR_SETTINGS_GROUP, - }, - { - key: 'disableCopyData', - type: ESettingsValueType.Checkbox, - name: 'settings_data_editor_disable_data_copy_name', - description: 'settings_data_editor_disable_data_copy_description', - group: DATA_EDITOR_SETTINGS_GROUP, - }, - { - key: 'fetchMin', - type: ESettingsValueType.Input, - name: 'settings_data_editor_fetch_min_name', - description: 'settings_data_editor_fetch_min_description', - group: DATA_EDITOR_SETTINGS_GROUP, - }, - { - key: 'fetchMax', - type: ESettingsValueType.Input, - name: 'settings_data_editor_fetch_max_name', - description: 'settings_data_editor_fetch_max_description', - group: DATA_EDITOR_SETTINGS_GROUP, - }, - { - key: 'fetchDefault', - type: ESettingsValueType.Input, - name: 'settings_data_editor_fetch_default_name', - description: 'settings_data_editor_fetch_default_description', - group: DATA_EDITOR_SETTINGS_GROUP, - }, - ]); + this.serverSettingsManagerService.setSettingTransformer( + 'resultset.maxrows', + setting => + ({ + ...setting, + name: 'settings_data_editor_fetch_default_name', + description: 'settings_data_editor_fetch_default_description', + group: DATA_EDITOR_SETTINGS_GROUP, + }) as ISettingDescription, + ); + + this.settingsManagerService.registerSettings(this.settings, () => { + const settings: ISettingDescription[] = [ + { + key: 'plugin.data-viewer.disableEdit', + access: { + scope: ['server'], + }, + type: ESettingsValueType.Checkbox, + name: 'settings_data_editor_disable_edit_name', + description: 'settings_data_editor_disable_edit_description', + group: DATA_EDITOR_SETTINGS_GROUP, + }, + { + key: 'plugin.data-viewer.disableCopyData', + access: { + scope: ['server'], + }, + type: ESettingsValueType.Checkbox, + name: 'settings_data_editor_disable_data_copy_name', + description: 'settings_data_editor_disable_data_copy_description', + group: DATA_EDITOR_SETTINGS_GROUP, + }, + { + key: 'plugin.data-viewer.fetchMin', + access: { + scope: ['server'], + }, + type: ESettingsValueType.Input, + name: 'settings_data_editor_fetch_min_name', + description: 'settings_data_editor_fetch_min_description', + group: DATA_EDITOR_SETTINGS_GROUP, + }, + { + key: 'plugin.data-viewer.fetchMax', + access: { + scope: ['server'], + }, + type: ESettingsValueType.Input, + name: 'settings_data_editor_fetch_max_name', + description: 'settings_data_editor_fetch_max_description', + group: DATA_EDITOR_SETTINGS_GROUP, + }, + ]; + + if (!this.serverSettingsManagerService.providedSettings.has('resultset.maxrows')) { + settings.push({ + key: 'resultset.maxrows', + access: { + scope: ['server'], + }, + type: ESettingsValueType.Input, + name: 'settings_data_editor_fetch_default_name', + description: 'settings_data_editor_fetch_default_description', + group: DATA_EDITOR_SETTINGS_GROUP, + }); + } + + return settings; + }); } } diff --git a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx index c8789f7d4e..fc61db52d2 100644 --- a/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx +++ b/webapp/packages/plugin-data-viewer/src/TableViewer/TableFooter/TableFooter.tsx @@ -108,8 +108,8 @@ export const TableFooter = observer(function TableFooter({ resultIndex, m type="number" value={limit} disabled={disabled} - min={dataViewerSettingsService.getMinFetchSize()} - max={dataViewerSettingsService.getMaxFetchSize()} + min={dataViewerSettingsService.minFetchSize} + max={dataViewerSettingsService.maxFetchSize} onChange={e => setLimit(e.target.value)} onBlur={handleChange} {...use({ mod: 'surface' })} diff --git a/webapp/packages/plugin-data-viewer/tsconfig.json b/webapp/packages/plugin-data-viewer/tsconfig.json index 288b591929..e0ffb54a1c 100644 --- a/webapp/packages/plugin-data-viewer/tsconfig.json +++ b/webapp/packages/plugin-data-viewer/tsconfig.json @@ -66,15 +66,6 @@ { "path": "../core-navigation-tree/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-product/tsconfig.json" - }, { "path": "../core-projects/tsconfig.json" }, @@ -100,7 +91,10 @@ "path": "../core-settings/tsconfig.json" }, { - "path": "../core-theming/tsconfig.json" + "path": "../core-settings/tsconfig.json" + }, + { + "path": "../core-storage/tsconfig.json" }, { "path": "../core-ui/tsconfig.json" diff --git a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts index 4d07547f97..59703e5ae2 100644 --- a/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts +++ b/webapp/packages/plugin-datasource-context-switch/src/ConnectionSchemaManager/ConnectionSchemaManagerBootstrap.ts @@ -381,7 +381,7 @@ export class ConnectionSchemaManagerBootstrap extends Bootstrap { private isHidden(): boolean { return ( - this.connectionsSettingsService.settings.getValue('disabled') || + this.connectionsSettingsService.disabled || this.optionsPanelService.active || (!this.connectionSchemaManagerService.isConnectionChangeable && !this.connectionSchemaManagerService.currentConnectionKey) ); diff --git a/webapp/packages/plugin-devtools/package.json b/webapp/packages/plugin-devtools/package.json index ec86526bbf..264c27bd91 100644 --- a/webapp/packages/plugin-devtools/package.json +++ b/webapp/packages/plugin-devtools/package.json @@ -22,7 +22,7 @@ "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", - "@cloudbeaver/core-settings": "~0.1.0", + "@cloudbeaver/core-storage": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", "@cloudbeaver/plugin-settings-menu": "~0.1.0", diff --git a/webapp/packages/plugin-devtools/src/DevToolsService.ts b/webapp/packages/plugin-devtools/src/DevToolsService.ts index e445a8eef2..89a6a0174c 100644 --- a/webapp/packages/plugin-devtools/src/DevToolsService.ts +++ b/webapp/packages/plugin-devtools/src/DevToolsService.ts @@ -9,7 +9,7 @@ import { makeObservable, observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; import { ServerConfigResource } from '@cloudbeaver/core-root'; -import { SettingsService } from '@cloudbeaver/core-settings'; +import { StorageService } from '@cloudbeaver/core-storage'; interface IDevToolsSettings { enabled: boolean; @@ -35,13 +35,16 @@ export class DevToolsService { private readonly settings: IDevToolsSettings; - constructor(private readonly serverConfigResource: ServerConfigResource, private readonly settingsService: SettingsService) { + constructor( + private readonly serverConfigResource: ServerConfigResource, + private readonly storageService: StorageService, + ) { this.settings = getDefaultDevToolsSettings(); makeObservable(this, { settings: observable, }); - this.settingsService.registerSettings(DEVTOOLS, this.settings, getDefaultDevToolsSettings); + this.storageService.registerSettings(DEVTOOLS, this.settings, getDefaultDevToolsSettings); this.serverConfigResource.onDataUpdate.addHandler(this.syncSettingsOverride.bind(this)); } diff --git a/webapp/packages/plugin-devtools/tsconfig.json b/webapp/packages/plugin-devtools/tsconfig.json index 5e4f868cf7..c8e16f9b58 100644 --- a/webapp/packages/plugin-devtools/tsconfig.json +++ b/webapp/packages/plugin-devtools/tsconfig.json @@ -22,7 +22,7 @@ "path": "../core-root/tsconfig.json" }, { - "path": "../core-settings/tsconfig.json" + "path": "../core-storage/tsconfig.json" }, { "path": "../core-ui/tsconfig.json" diff --git a/webapp/packages/plugin-localization/src/PluginBootstrap.ts b/webapp/packages/plugin-localization/src/PluginBootstrap.ts index 9b1a1be4b5..e523ea1053 100644 --- a/webapp/packages/plugin-localization/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-localization/src/PluginBootstrap.ts @@ -23,7 +23,7 @@ export class PluginBootstrap extends Bootstrap { super(); } - register(): void | Promise { + register(): void { this.menuService.setHandler({ id: 'localization-menu', isApplicable: context => context.get(DATA_CONTEXT_MENU) === LOCALIZATION_MENU, @@ -57,7 +57,7 @@ export class PluginBootstrap extends Bootstrap { label, tooltip: label, }, - { onSelect: () => this.localizationService.changeLocaleAsync(lang.isoCode) }, + { onSelect: () => this.localizationService.changeLocale(lang.isoCode) }, { isDisabled: () => this.localizationService.currentLanguage === lang.isoCode }, ); }); @@ -66,6 +66,4 @@ export class PluginBootstrap extends Bootstrap { }, }); } - - load(): void | Promise {} } diff --git a/webapp/packages/plugin-log-viewer/package.json b/webapp/packages/plugin-log-viewer/package.json index d852e2d412..1a55d03074 100644 --- a/webapp/packages/plugin-log-viewer/package.json +++ b/webapp/packages/plugin-log-viewer/package.json @@ -22,10 +22,10 @@ "@cloudbeaver/core-blocks": "~0.1.0", "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", "@cloudbeaver/plugin-tools-panel": "~0.1.0", @@ -40,15 +40,12 @@ "@cloudbeaver/core-authentication": "~0.1.0", "@cloudbeaver/core-browser": "~0.1.0", "@cloudbeaver/core-client-activity": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-product": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-routing": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", - "@cloudbeaver/core-theming": "~0.1.0", + "@cloudbeaver/core-storage": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", "@cloudbeaver/plugin-tools-panel": "~0.1.0", "@cloudbeaver/tests-runner": "~0.1.0", diff --git a/webapp/packages/plugin-log-viewer/src/LogViewer/LOG_VIEWER_SETTINGS_GROUP.ts b/webapp/packages/plugin-log-viewer/src/LogViewer/LOG_VIEWER_SETTINGS_GROUP.ts index 8a2e07c2fc..ecf989c917 100644 --- a/webapp/packages/plugin-log-viewer/src/LogViewer/LOG_VIEWER_SETTINGS_GROUP.ts +++ b/webapp/packages/plugin-log-viewer/src/LogViewer/LOG_VIEWER_SETTINGS_GROUP.ts @@ -5,6 +5,6 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-plugin'; +import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-settings'; export const LOG_VIEWER_SETTINGS_GROUP = ROOT_SETTINGS_GROUP.createSubGroup('plugin_log_viewer_log_viewer_settings_group'); diff --git a/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerService.ts b/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerService.ts index 8cd9d1ac9b..41a54b6707 100644 --- a/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerService.ts +++ b/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerService.ts @@ -30,7 +30,7 @@ export class LogViewerService { } get disabled() { - return this.toolsPanelService.disabled || this.logViewerSettingsService.settings.getValue('disabled'); + return this.toolsPanelService.disabled || this.logViewerSettingsService.disabled; } constructor( diff --git a/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerSettingsService.test.ts b/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerSettingsService.test.ts index 3ae780ca4e..c4b2d8bcfe 100644 --- a/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerSettingsService.test.ts +++ b/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerSettingsService.test.ts @@ -12,75 +12,61 @@ import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; import { mockAuthentication } from '@cloudbeaver/core-authentication/dist/__custom_mocks__/mockAuthentication'; import { coreBrowserManifest } from '@cloudbeaver/core-browser'; import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; -import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; +import '@cloudbeaver/core-root/dist/__custom_mocks__/expectWebsocketClosedMessage'; import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; import { coreRoutingManifest } from '@cloudbeaver/core-routing'; import { coreSDKManifest } from '@cloudbeaver/core-sdk'; import { coreSettingsManifest } from '@cloudbeaver/core-settings'; -import { coreThemingManifest } from '@cloudbeaver/core-theming'; +import { + expectDeprecatedSettingMessage, + expectNoDeprecatedSettingMessage, +} from '@cloudbeaver/core-settings/dist/__custom_mocks__/expectDeprecatedSettingMessage'; +import { coreStorageManifest } from '@cloudbeaver/core-storage'; import { coreViewManifest } from '@cloudbeaver/core-view'; import toolsPanelPlugin from '@cloudbeaver/plugin-tools-panel'; import { createApp } from '@cloudbeaver/tests-runner'; import { logViewerPlugin } from '../manifest'; -import { LogViewerSettings, LogViewerSettingsService } from './LogViewerSettingsService'; +import { LogViewerSettingsService } from './LogViewerSettingsService'; const endpoint = createGQLEndpoint(); +const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); const app = createApp( logViewerPlugin, toolsPanelPlugin, - corePluginManifest, - coreProductManifest, coreRootManifest, + coreStorageManifest, coreSDKManifest, coreViewManifest, coreAuthenticationManifest, coreSettingsManifest, coreBrowserManifest, coreLocalizationManifest, - coreEventsManifest, coreAppManifest, coreRoutingManifest, - coreThemingManifest, coreClientActivityManifest, ); -const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); - -beforeAll(() => app.init()); - const deprecatedSettings = { - core: { - app: { - logViewer: { - refreshTimeout: 1, - maxLogRecords: 2, - logBatchSize: 3, - maxFailedRequests: 4, - disabled: true, - } as LogViewerSettings, - }, - }, + 'core.app.logViewer.refreshTimeout': 1, + 'core.app.logViewer.maxLogRecords': 2, + 'core.app.logViewer.logBatchSize': 3, + 'core.app.logViewer.maxFailedRequests': 4, + 'core.app.logViewer.disabled': true, }; const newSettings = { ...deprecatedSettings, - plugin: { - 'log-viewer': { - refreshTimeout: 5, - maxLogRecords: 6, - logBatchSize: 7, - maxFailedRequests: 8, - disabled: false, - } as LogViewerSettings, - }, + 'plugin.log-viewer.refreshTimeout': 5, + 'plugin.log-viewer.maxLogRecords': 6, + 'plugin.log-viewer.logBatchSize': 7, + 'plugin.log-viewer.maxFailedRequests': 8, + 'plugin.log-viewer.disabled': false, }; test('New settings override deprecated settings', async () => { @@ -91,11 +77,12 @@ test('New settings override deprecated settings', async () => { await config.refresh(); - expect(settings.settings.getValue('refreshTimeout')).toBe(5); - expect(settings.settings.getValue('maxLogRecords')).toBe(6); - expect(settings.settings.getValue('logBatchSize')).toBe(7); - expect(settings.settings.getValue('maxFailedRequests')).toBe(8); - expect(settings.settings.getValue('disabled')).toBe(false); + expect(settings.refreshTimeout).toBe(5); + expect(settings.maxLogRecords).toBe(6); + expect(settings.logBatchSize).toBe(7); + expect(settings.maxFailedRequests).toBe(8); + expect(settings.disabled).toBe(false); + expectNoDeprecatedSettingMessage(); }); test('Deprecated settings are used if new settings are not defined', async () => { @@ -106,9 +93,10 @@ test('Deprecated settings are used if new settings are not defined', async () => await config.refresh(); - expect(settings.settings.getValue('refreshTimeout')).toBe(1); - expect(settings.settings.getValue('maxLogRecords')).toBe(2); - expect(settings.settings.getValue('logBatchSize')).toBe(3); - expect(settings.settings.getValue('maxFailedRequests')).toBe(4); - expect(settings.settings.getValue('disabled')).toBe(true); + expect(settings.refreshTimeout).toBe(1); + expect(settings.maxLogRecords).toBe(2); + expect(settings.logBatchSize).toBe(3); + expect(settings.maxFailedRequests).toBe(4); + expect(settings.disabled).toBe(true); + expectDeprecatedSettingMessage(); }); diff --git a/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerSettingsService.ts b/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerSettingsService.ts index 7cd4461798..69f03b74bf 100644 --- a/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerSettingsService.ts +++ b/webapp/packages/plugin-log-viewer/src/LogViewer/LogViewerSettingsService.ts @@ -6,35 +6,70 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { createSettingsAliasResolver, PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; -import { ServerSettingsResolverService, ServerSettingsService } from '@cloudbeaver/core-root'; -import { schema } from '@cloudbeaver/core-utils'; +import { + createSettingsAliasResolver, + ESettingsValueType, + ROOT_SETTINGS_LAYER, + SettingsManagerService, + SettingsProvider, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; + +import { LOG_VIEWER_SETTINGS_GROUP } from './LOG_VIEWER_SETTINGS_GROUP'; const defaultSettings = schema.object({ - refreshTimeout: schema.coerce.number().default(3000), - maxLogRecords: schema.coerce.number().default(1000), - logBatchSize: schema.coerce.number().default(2000), - maxFailedRequests: schema.coerce.number().default(3), - disabled: schema.coerce.boolean().default(false), + 'plugin.log-viewer.refreshTimeout': schema.coerce.number().default(3000), + 'plugin.log-viewer.maxLogRecords': schema.coerce.number().default(1000), + 'plugin.log-viewer.logBatchSize': schema.coerce.number().default(2000), + 'plugin.log-viewer.maxFailedRequests': schema.coerce.number().default(3), + 'plugin.log-viewer.disabled': schemaExtra.stringedBoolean().default(false), }); export type LogViewerSettings = schema.infer; @injectable() export class LogViewerSettingsService extends Dependency { - readonly settings: PluginSettings; + get disabled(): boolean { + return this.settings.getValue('plugin.log-viewer.disabled'); + } + + get refreshTimeout(): number { + return this.settings.getValue('plugin.log-viewer.refreshTimeout'); + } + + get maxLogRecords(): number { + return this.settings.getValue('plugin.log-viewer.maxLogRecords'); + } + + get logBatchSize(): number { + return this.settings.getValue('plugin.log-viewer.logBatchSize'); + } + + get maxFailedRequests(): number { + return this.settings.getValue('plugin.log-viewer.maxFailedRequests'); + } + + readonly settings: SettingsProvider; constructor( - private readonly pluginManagerService: PluginManagerService, + private readonly settingsProviderService: SettingsProviderService, private readonly settingsManagerService: SettingsManagerService, - private readonly serverSettingsService: ServerSettingsService, - private readonly serverSettingsResolverService: ServerSettingsResolverService, + private readonly settingsResolverService: SettingsResolverService, ) { super(); - this.settings = this.pluginManagerService.createSettings('log-viewer', 'plugin', defaultSettings); - this.serverSettingsResolverService.addResolver( + this.settings = this.settingsProviderService.createSettings(defaultSettings); + this.settingsResolverService.addResolver( + ROOT_SETTINGS_LAYER, /** @deprecated Use settings instead, will be removed in 23.0.0 */ - createSettingsAliasResolver(this.serverSettingsService, this.settings, 'core.app.logViewer'), + createSettingsAliasResolver(this.settingsResolverService, this.settings, { + 'plugin.log-viewer.disabled': 'core.app.logViewer.disabled', + 'plugin.log-viewer.logBatchSize': 'core.app.logViewer.logBatchSize', + 'plugin.log-viewer.maxFailedRequests': 'core.app.logViewer.maxFailedRequests', + 'plugin.log-viewer.maxLogRecords': 'core.app.logViewer.maxLogRecords', + 'plugin.log-viewer.refreshTimeout': 'core.app.logViewer.refreshTimeout', + }), ); this.registerSettings(); @@ -71,8 +106,11 @@ export class LogViewerSettingsService extends Dependency { // description: 'Max failed requests', // }, // { + // key: 'plugin.log-viewer.disabled', + // access: { + // scope: ['server', 'client'], + // }, // group: LOG_VIEWER_SETTINGS_GROUP, - // key: 'disabled', // type: ESettingsValueType.Checkbox, // name: 'Disable log viewer', // description: 'Disable log viewer', diff --git a/webapp/packages/plugin-log-viewer/src/SessionLogsResource.ts b/webapp/packages/plugin-log-viewer/src/SessionLogsResource.ts index d06ae05265..e157e757d5 100644 --- a/webapp/packages/plugin-log-viewer/src/SessionLogsResource.ts +++ b/webapp/packages/plugin-log-viewer/src/SessionLogsResource.ts @@ -53,7 +53,7 @@ export class SessionLogsResource extends CachedDataResource { } protected async loader(): Promise { - const maxLogEntries = this.logViewerSettingsService.settings.getValue('logBatchSize'); + const maxLogEntries = this.logViewerSettingsService.logBatchSize; const { log } = await this.graphQLService.sdk.readSessionLog({ maxEntries: maxLogEntries, diff --git a/webapp/packages/plugin-log-viewer/tsconfig.json b/webapp/packages/plugin-log-viewer/tsconfig.json index 8849b8f400..869502bac3 100644 --- a/webapp/packages/plugin-log-viewer/tsconfig.json +++ b/webapp/packages/plugin-log-viewer/tsconfig.json @@ -27,24 +27,12 @@ { "path": "../core-di/tsconfig.json" }, - { - "path": "../core-events/tsconfig.json" - }, { "path": "../core-localization/tsconfig.json" }, { "path": "../core-localization/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-product/tsconfig.json" - }, { "path": "../core-resource/tsconfig.json" }, @@ -67,7 +55,10 @@ "path": "../core-settings/tsconfig.json" }, { - "path": "../core-theming/tsconfig.json" + "path": "../core-settings/tsconfig.json" + }, + { + "path": "../core-storage/tsconfig.json" }, { "path": "../core-utils/tsconfig.json" diff --git a/webapp/packages/plugin-navigation-tabs/package.json b/webapp/packages/plugin-navigation-tabs/package.json index 5d7eb1aeb6..c19297ea53 100644 --- a/webapp/packages/plugin-navigation-tabs/package.json +++ b/webapp/packages/plugin-navigation-tabs/package.json @@ -27,7 +27,7 @@ "@cloudbeaver/core-extensions": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", - "@cloudbeaver/core-settings": "~0.1.0", + "@cloudbeaver/core-storage": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", diff --git a/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsService.ts b/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsService.ts index 5f101769f2..47d6a97672 100644 --- a/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsService.ts +++ b/webapp/packages/plugin-navigation-tabs/src/NavigationTabs/NavigationTabsService.ts @@ -14,7 +14,7 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; import { ProjectsService } from '@cloudbeaver/core-projects'; import { resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; -import { SettingsService } from '@cloudbeaver/core-settings'; +import { StorageService } from '@cloudbeaver/core-storage'; import { isArraysEqual, MetadataMap, TempMap } from '@cloudbeaver/core-utils'; import { ACTION_OPEN_IN_TAB, IActiveView, View } from '@cloudbeaver/core-view'; @@ -110,7 +110,7 @@ export class NavigationTabsService extends View { constructor( private readonly notificationService: NotificationService, - private readonly settingsService: SettingsService, + private readonly storageService: StorageService, private readonly userInfoResource: UserInfoResource, private readonly projectsService: ProjectsService, private readonly administrationScreenService: AdministrationScreenService, @@ -152,7 +152,7 @@ export class NavigationTabsService extends View { }); this.tempHistoryState = new TempMap(this.historyState); - this.settingsService.registerSettings( + this.storageService.registerSettings( `${NAVIGATION_TABS_BASE_KEY}_tab_map`, this.tabsMap, () => new Map(), @@ -171,7 +171,7 @@ export class NavigationTabsService extends View { }, ); - this.settingsService.registerSettings( + this.storageService.registerSettings( `${NAVIGATION_TABS_BASE_KEY}_history`, this.historyState, () => new Map(), @@ -185,7 +185,7 @@ export class NavigationTabsService extends View { }, ); - this.settingsService.registerSettings( + this.storageService.registerSettings( NAVIGATION_TABS_BASE_KEY, this.state, () => new Map(), @@ -200,7 +200,7 @@ export class NavigationTabsService extends View { ); this.userInfoResource.onUserChange.addHandler(this.unloadTabs.bind(this)); - this.settingsService.onStorageChange.next(this.onStateUpdate, () => {}); + this.storageService.onStorageChange.next(this.onStateUpdate, () => {}); } getTabMetadata(tabId: string): ITabMetadata { diff --git a/webapp/packages/plugin-navigation-tabs/tsconfig.json b/webapp/packages/plugin-navigation-tabs/tsconfig.json index ab2ccac4d4..e196419645 100644 --- a/webapp/packages/plugin-navigation-tabs/tsconfig.json +++ b/webapp/packages/plugin-navigation-tabs/tsconfig.json @@ -37,7 +37,7 @@ "path": "../core-resource/tsconfig.json" }, { - "path": "../core-settings/tsconfig.json" + "path": "../core-storage/tsconfig.json" }, { "path": "../core-ui/tsconfig.json" diff --git a/webapp/packages/plugin-navigation-tree/package.json b/webapp/packages/plugin-navigation-tree/package.json index 557047c29d..39c3346508 100644 --- a/webapp/packages/plugin-navigation-tree/package.json +++ b/webapp/packages/plugin-navigation-tree/package.json @@ -28,9 +28,9 @@ "@cloudbeaver/core-executor": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-theming": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", @@ -53,13 +53,13 @@ "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-product": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-routing": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", + "@cloudbeaver/core-storage": "~0.1.0", "@cloudbeaver/core-theming": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTreeSettingsService.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTreeSettingsService.ts index 78ccca9e76..5b1ec11ff6 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTreeSettingsService.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTreeSettingsService.ts @@ -8,11 +8,11 @@ import { computed, makeObservable } from 'mobx'; import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; -import { schema } from '@cloudbeaver/core-utils'; +import { SettingsManagerService, SettingsProvider, SettingsProviderService } from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; const defaultSettings = schema.object({ - disabled: schema.coerce.boolean().default(false), + 'plugin.navigation-tree.disabled': schemaExtra.stringedBoolean().default(false), }); export type NavigationTreeSettings = schema.infer; @@ -20,14 +20,17 @@ export type NavigationTreeSettings = schema.infer; @injectable() export class NavigationTreeSettingsService extends Dependency { get disabled(): boolean { - return this.settings.getValue('disabled'); + return this.settings.getValue('plugin.navigation-tree.disabled'); } - readonly settings: PluginSettings; + readonly settings: SettingsProvider; - constructor(private readonly pluginManagerService: PluginManagerService, private readonly settingsManagerService: SettingsManagerService) { + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + ) { super(); - this.settings = this.pluginManagerService.createSettings('navigation-tree', 'plugin', defaultSettings); + this.settings = this.settingsProviderService.createSettings(defaultSettings); this.registerSettings(); diff --git a/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts b/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts index f81dad3d5a..4fde8af006 100644 --- a/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts +++ b/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts @@ -187,17 +187,13 @@ export class NavNodeContextMenuService extends Bootstrap { this.menuService.addCreator({ isApplicable: context => context.has(DATA_CONTEXT_NAV_NODE) && !context.has(DATA_CONTEXT_MENU_NESTED), getItems: (context, items) => { - const editingGlobalPermission = this.navTreeSettingsService.settings.getValue('editing'); - - const deleteGlobalPermission = this.navTreeSettingsService.settings.getValue('deleting'); - items = [ACTION_OPEN, ACTION_REFRESH, ...items]; - if (editingGlobalPermission) { + if (this.navTreeSettingsService.editing) { items.push(ACTION_RENAME); } - if (deleteGlobalPermission) { + if (this.navTreeSettingsService.deleting) { items.push(ACTION_DELETE); } diff --git a/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeView/NavNodeViewService.test.ts b/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeView/NavNodeViewService.test.ts index 23980e341c..517eb27d08 100644 --- a/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeView/NavNodeViewService.test.ts +++ b/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeView/NavNodeViewService.test.ts @@ -18,16 +18,17 @@ import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; import { coreProductManifest } from '@cloudbeaver/core-product'; import { coreProjectsManifest } from '@cloudbeaver/core-projects'; import { coreRootManifest } from '@cloudbeaver/core-root'; import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; +import '@cloudbeaver/core-root/dist/__custom_mocks__/expectWebsocketClosedMessage'; import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; import { coreRoutingManifest } from '@cloudbeaver/core-routing'; import { coreSDKManifest } from '@cloudbeaver/core-sdk'; import { coreSettingsManifest } from '@cloudbeaver/core-settings'; +import { coreStorageManifest } from '@cloudbeaver/core-storage'; import { coreThemingManifest } from '@cloudbeaver/core-theming'; import { coreUIManifest } from '@cloudbeaver/core-ui'; import { coreViewManifest } from '@cloudbeaver/core-view'; @@ -39,11 +40,11 @@ import { navigationTreePlugin } from '../../manifest'; import { NavNodeViewService } from './NavNodeViewService'; const endpoint = createGQLEndpoint(); +mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); const app = createApp( navigationTreePlugin, coreLocalizationManifest, coreEventsManifest, - corePluginManifest, coreProductManifest, coreRootManifest, coreSDKManifest, @@ -53,6 +54,7 @@ const app = createApp( coreAuthenticationManifest, coreProjectsManifest, coreUIManifest, + coreStorageManifest, coreRoutingManifest, coreAdministrationManifest, coreConnectionsManifest, @@ -65,10 +67,6 @@ const app = createApp( coreClientActivityManifest, ); -mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); - -beforeAll(() => app.init()); - describe('filterDuplicates', () => { test('Filter duplicates', async () => { const navNodeViewService = getService(app, NavNodeViewService); diff --git a/webapp/packages/plugin-navigation-tree/tsconfig.json b/webapp/packages/plugin-navigation-tree/tsconfig.json index 6edb4662a5..faffb67e14 100644 --- a/webapp/packages/plugin-navigation-tree/tsconfig.json +++ b/webapp/packages/plugin-navigation-tree/tsconfig.json @@ -66,12 +66,6 @@ { "path": "../core-navigation-tree/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-plugin/tsconfig.json" - }, { "path": "../core-product/tsconfig.json" }, @@ -96,6 +90,12 @@ { "path": "../core-settings/tsconfig.json" }, + { + "path": "../core-settings/tsconfig.json" + }, + { + "path": "../core-storage/tsconfig.json" + }, { "path": "../core-theming/tsconfig.json" }, diff --git a/webapp/packages/plugin-object-viewer/package.json b/webapp/packages/plugin-object-viewer/package.json index 63ed292a73..f6ce1a07d4 100644 --- a/webapp/packages/plugin-object-viewer/package.json +++ b/webapp/packages/plugin-object-viewer/package.json @@ -33,6 +33,7 @@ "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", + "@cloudbeaver/plugin-connections": "~0.1.0", "@cloudbeaver/plugin-navigation-tabs": "~0.1.0", "@cloudbeaver/plugin-navigation-tree": "~0.1.0", "@cloudbeaver/plugin-react-data-grid": "~0.1.0", diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts index 6564997118..b5219cac05 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts @@ -28,7 +28,7 @@ export class ObjectPropertyTableFooterService { menus: [MENU_OBJECT_VIEWER_FOOTER], isApplicable: context => { const selected = context.tryGet(DATA_CONTEXT_NAV_NODES); - return selected !== undefined && this.navTreeSettingsService.settings.getValue('deleting'); + return selected !== undefined && !this.navTreeSettingsService.deleting; }, getItems: (_, items) => [...items, ACTION_DELETE], }); diff --git a/webapp/packages/plugin-object-viewer/tsconfig.json b/webapp/packages/plugin-object-viewer/tsconfig.json index 05badced81..180c20dba1 100644 --- a/webapp/packages/plugin-object-viewer/tsconfig.json +++ b/webapp/packages/plugin-object-viewer/tsconfig.json @@ -57,6 +57,9 @@ { "path": "../core-view/tsconfig.json" }, + { + "path": "../plugin-connections/tsconfig.json" + }, { "path": "../plugin-navigation-tabs/tsconfig.json" }, diff --git a/webapp/packages/plugin-product/src/ProductInfoDialog.tsx b/webapp/packages/plugin-product/src/ProductInfoDialog.tsx index ec84275c3a..5208922a46 100644 --- a/webapp/packages/plugin-product/src/ProductInfoDialog.tsx +++ b/webapp/packages/plugin-product/src/ProductInfoDialog.tsx @@ -39,7 +39,7 @@ export const ProductInfoDialog = observer>(function P const version = useAppVersion(); const productInfo = serverConfigResource.data?.productInfo; - const logoIcon = themeService.currentThemeId === 'light' ? '/icons/product-logo_light.svg' : '/icons/product-logo_dark.svg'; + const logoIcon = themeService.themeId === 'light' ? '/icons/product-logo_light.svg' : '/icons/product-logo_dark.svg'; const styles = useS(ProductInfoDialogStyles); diff --git a/webapp/packages/plugin-resource-manager-scripts/package.json b/webapp/packages/plugin-resource-manager-scripts/package.json index b45c70794e..ab8e7b1078 100644 --- a/webapp/packages/plugin-resource-manager-scripts/package.json +++ b/webapp/packages/plugin-resource-manager-scripts/package.json @@ -23,11 +23,11 @@ "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-dialogs": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-resource-manager": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", diff --git a/webapp/packages/plugin-resource-manager-scripts/src/ResourceManagerScriptsService.ts b/webapp/packages/plugin-resource-manager-scripts/src/ResourceManagerScriptsService.ts index f7a8b0b349..35464bc8b9 100644 --- a/webapp/packages/plugin-resource-manager-scripts/src/ResourceManagerScriptsService.ts +++ b/webapp/packages/plugin-resource-manager-scripts/src/ResourceManagerScriptsService.ts @@ -42,7 +42,7 @@ export class ResourceManagerScriptsService { } get enabled() { - return this.resourceManagerService.enabled && !this.resourceManagerScriptsSettingsService.settings.getValue('disabled'); + return this.resourceManagerService.enabled && !this.resourceManagerScriptsSettingsService.disabled; } constructor( diff --git a/webapp/packages/plugin-resource-manager-scripts/src/ResourceManagerScriptsSettingsService.ts b/webapp/packages/plugin-resource-manager-scripts/src/ResourceManagerScriptsSettingsService.ts index 5186065609..622de0c426 100644 --- a/webapp/packages/plugin-resource-manager-scripts/src/ResourceManagerScriptsSettingsService.ts +++ b/webapp/packages/plugin-resource-manager-scripts/src/ResourceManagerScriptsSettingsService.ts @@ -6,22 +6,29 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; -import { schema } from '@cloudbeaver/core-utils'; +import { SettingsManagerService, SettingsProvider, SettingsProviderService } from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; const settingsSchema = schema.object({ - disabled: schema.coerce.boolean().default(false), + 'plugin.resource-manager-scripts.disabled': schemaExtra.stringedBoolean().default(false), }); type Settings = typeof settingsSchema; @injectable() export class ResourceManagerScriptsSettingsService extends Dependency { - readonly settings: PluginSettings; + readonly settings: SettingsProvider; - constructor(private readonly pluginManagerService: PluginManagerService, private readonly settingsManagerService: SettingsManagerService) { + get disabled(): boolean { + return this.settings.getValue('plugin.resource-manager-scripts.disabled'); + } + + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + ) { super(); - this.settings = this.pluginManagerService.createSettings('resource-manager-scripts', 'plugin', settingsSchema); + this.settings = this.settingsProviderService.createSettings(settingsSchema); this.registerSettings(); } diff --git a/webapp/packages/plugin-resource-manager-scripts/tsconfig.json b/webapp/packages/plugin-resource-manager-scripts/tsconfig.json index 39a62cb47e..656657d261 100644 --- a/webapp/packages/plugin-resource-manager-scripts/tsconfig.json +++ b/webapp/packages/plugin-resource-manager-scripts/tsconfig.json @@ -24,9 +24,6 @@ { "path": "../core-localization/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, { "path": "../core-projects/tsconfig.json" }, @@ -39,6 +36,9 @@ { "path": "../core-root/tsconfig.json" }, + { + "path": "../core-settings/tsconfig.json" + }, { "path": "../core-ui/tsconfig.json" }, diff --git a/webapp/packages/plugin-resource-manager/package.json b/webapp/packages/plugin-resource-manager/package.json index af563830bc..038b0fae5d 100644 --- a/webapp/packages/plugin-resource-manager/package.json +++ b/webapp/packages/plugin-resource-manager/package.json @@ -21,23 +21,19 @@ "@cloudbeaver/core-authentication": "~0.1.0", "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-resource-manager": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "mobx": "^6.12.0" }, "peerDependencies": {}, "devDependencies": { - "@cloudbeaver/core-browser": "~0.1.0", "@cloudbeaver/core-client-activity": "~0.1.0", - "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-product": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", diff --git a/webapp/packages/plugin-resource-manager/src/ResourceManagerSettingsService.test.ts b/webapp/packages/plugin-resource-manager/src/ResourceManagerSettingsService.test.ts index c63be311bb..1909024ac5 100644 --- a/webapp/packages/plugin-resource-manager/src/ResourceManagerSettingsService.test.ts +++ b/webapp/packages/plugin-resource-manager/src/ResourceManagerSettingsService.test.ts @@ -7,58 +7,46 @@ */ import '@testing-library/jest-dom'; -import { coreBrowserManifest } from '@cloudbeaver/core-browser'; import { coreClientActivityManifest } from '@cloudbeaver/core-client-activity'; -import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; +import '@cloudbeaver/core-root/dist/__custom_mocks__/expectWebsocketClosedMessage'; import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; import { coreSDKManifest } from '@cloudbeaver/core-sdk'; import { coreSettingsManifest } from '@cloudbeaver/core-settings'; +import { + expectDeprecatedSettingMessage, + expectNoDeprecatedSettingMessage, +} from '@cloudbeaver/core-settings/dist/__custom_mocks__/expectDeprecatedSettingMessage'; import { createApp } from '@cloudbeaver/tests-runner'; import { resourceManagerPlugin } from './manifest'; -import { ResourceManagerSettings, ResourceManagerSettingsService } from './ResourceManagerSettingsService'; +import { ResourceManagerSettingsService } from './ResourceManagerSettingsService'; const endpoint = createGQLEndpoint(); +const server = mockGraphQL(...mockAppInit(endpoint)); const app = createApp( resourceManagerPlugin, - coreEventsManifest, - corePluginManifest, - coreProductManifest, coreRootManifest, coreSDKManifest, coreSettingsManifest, - coreBrowserManifest, coreLocalizationManifest, coreClientActivityManifest, ); -const server = mockGraphQL(...mockAppInit(endpoint)); - -beforeAll(() => app.init()); - const testValueDeprecated = true; const testValueNew = false; const deprecatedSettings = { - plugin_resource_manager: { - disabled: testValueDeprecated, - } as ResourceManagerSettings, + 'plugin_resource_manager.disabled': testValueDeprecated, }; const newSettings = { ...deprecatedSettings, - plugin: { - 'resource-manager': { - disabled: testValueNew, - } as ResourceManagerSettings, - }, + 'plugin.resource-manager.disabled': testValueNew, }; test('New settings equal deprecated settings A', async () => { @@ -69,7 +57,8 @@ test('New settings equal deprecated settings A', async () => { await config.refresh(); - expect(settings.settings.getValue('disabled')).toBe(testValueNew); + expect(settings.disabled).toBe(testValueNew); + expectNoDeprecatedSettingMessage(); }); test('New settings equal deprecated settings B', async () => { @@ -80,5 +69,6 @@ test('New settings equal deprecated settings B', async () => { await config.refresh(); - expect(settings.settings.getValue('disabled')).toBe(testValueDeprecated); + expect(settings.disabled).toBe(testValueDeprecated); + expectDeprecatedSettingMessage(); }); diff --git a/webapp/packages/plugin-resource-manager/src/ResourceManagerSettingsService.ts b/webapp/packages/plugin-resource-manager/src/ResourceManagerSettingsService.ts index 3d9b5d2f0f..fe35dda4ee 100644 --- a/webapp/packages/plugin-resource-manager/src/ResourceManagerSettingsService.ts +++ b/webapp/packages/plugin-resource-manager/src/ResourceManagerSettingsService.ts @@ -6,31 +6,42 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { createSettingsAliasResolver, PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; -import { ServerSettingsResolverService, ServerSettingsService } from '@cloudbeaver/core-root'; -import { schema } from '@cloudbeaver/core-utils'; +import { + createSettingsAliasResolver, + ROOT_SETTINGS_LAYER, + SettingsManagerService, + SettingsProvider, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; const defaultSettings = schema.object({ - disabled: schema.coerce.boolean().default(false), //! use resourceManagerEnabled in server config instead + 'plugin.resource-manager.disabled': schemaExtra.stringedBoolean().default(false), //! use resourceManagerEnabled in server config instead }); export type ResourceManagerSettings = schema.infer; @injectable() export class ResourceManagerSettingsService extends Dependency { - readonly settings: PluginSettings; + get disabled(): boolean { + return this.settings.getValue('plugin.resource-manager.disabled'); + } + readonly settings: SettingsProvider; constructor( - private readonly pluginManagerService: PluginManagerService, + private readonly settingsProviderService: SettingsProviderService, private readonly settingsManagerService: SettingsManagerService, - private readonly serverSettingsService: ServerSettingsService, - private readonly serverSettingsResolverService: ServerSettingsResolverService, + private readonly settingsResolverService: SettingsResolverService, ) { super(); - this.settings = this.pluginManagerService.createSettings('resource-manager', 'plugin', defaultSettings); - this.serverSettingsResolverService.addResolver( + this.settings = this.settingsProviderService.createSettings(defaultSettings); + this.settingsResolverService.addResolver( + ROOT_SETTINGS_LAYER, /** @deprecated Use settings instead, will be removed in 23.0.0 */ - createSettingsAliasResolver(this.serverSettingsService, this.settings, 'plugin_resource_manager'), + createSettingsAliasResolver(this.settingsResolverService, this.settings, { + 'plugin.resource-manager.disabled': 'plugin_resource_manager.disabled', + }), ); this.registerSettings(); diff --git a/webapp/packages/plugin-resource-manager/tsconfig.json b/webapp/packages/plugin-resource-manager/tsconfig.json index 98ccf5e9f7..9e5eb9f961 100644 --- a/webapp/packages/plugin-resource-manager/tsconfig.json +++ b/webapp/packages/plugin-resource-manager/tsconfig.json @@ -9,33 +9,18 @@ { "path": "../core-authentication/tsconfig.json" }, - { - "path": "../core-browser/tsconfig.json" - }, { "path": "../core-client-activity/tsconfig.json" }, { "path": "../core-di/tsconfig.json" }, - { - "path": "../core-events/tsconfig.json" - }, { "path": "../core-localization/tsconfig.json" }, { "path": "../core-localization/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-product/tsconfig.json" - }, { "path": "../core-projects/tsconfig.json" }, @@ -60,6 +45,9 @@ { "path": "../core-settings/tsconfig.json" }, + { + "path": "../core-settings/tsconfig.json" + }, { "path": "../core-utils/tsconfig.json" }, diff --git a/webapp/packages/plugin-settings-administration/src/SettingsAdministration.tsx b/webapp/packages/plugin-settings-administration/src/SettingsAdministration.tsx index afddafba7e..bb4713f0fb 100644 --- a/webapp/packages/plugin-settings-administration/src/SettingsAdministration.tsx +++ b/webapp/packages/plugin-settings-administration/src/SettingsAdministration.tsx @@ -16,14 +16,17 @@ import { Settings } from '@cloudbeaver/plugin-settings-panel'; import style from './SettingsAdministration.m.css'; +const clientScope = ['server']; + export const SettingsAdministration = observer(function SettingsAdministration() { const translate = useTranslate(); const serverSettingsService = useService(ServerSettingsService); const notificationService = useService(NotificationService); const styles = useS(style); + const changed = serverSettingsService.isEdited(); async function handleSave() { - if (!serverSettingsService.isChanged) { + if (!changed) { return; } try { @@ -44,8 +47,6 @@ export const SettingsAdministration = observer(f serverSettingsService.resetChanges(); } - const changed = serverSettingsService.isChanged; - return (
@@ -59,7 +60,7 @@ export const SettingsAdministration = observer(f - +
); diff --git a/webapp/packages/plugin-settings-panel/package.json b/webapp/packages/plugin-settings-panel/package.json index 098282b6dc..86d5a073b7 100644 --- a/webapp/packages/plugin-settings-panel/package.json +++ b/webapp/packages/plugin-settings-panel/package.json @@ -21,7 +21,6 @@ "@cloudbeaver/core-blocks": "~0.1.0", "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "@cloudbeaver/plugin-navigation-tree": "~0.1.0", @@ -32,6 +31,7 @@ "peerDependencies": {}, "devDependencies": { "@types/react": "^18.2.63", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "typescript-plugin-css-modules": "^5.1.0" } } diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/Setting.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/Setting.tsx index 646186f875..27eb2c8ddb 100644 --- a/webapp/packages/plugin-settings-panel/src/SettingsPanel/Setting.tsx +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/Setting.tsx @@ -9,29 +9,53 @@ import { observer } from 'mobx-react-lite'; import { Combobox, FieldCheckbox, InputField, Textarea, useCustomInputValidation, useTranslate } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { ESettingsValueType, type ISettingDescriptionWithScope, PluginManagerService } from '@cloudbeaver/core-plugin'; -import type { ISettingsSource } from '@cloudbeaver/core-settings'; -import { schemaValidationError } from '@cloudbeaver/core-utils'; +import { + ESettingsValueType, + type ISettingDescription, + type ISettingsSource, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; +import { isNotNullDefined, schemaValidationError } from '@cloudbeaver/core-utils'; interface Props { source: ISettingsSource; - setting: ISettingDescriptionWithScope; + setting: ISettingDescription; } export const Setting = observer(function Setting({ source, setting }) { + const settingsResolverService = useService(SettingsResolverService); + const settingsProviderService = useService(SettingsProviderService); const translate = useTranslate(); - const pluginManagerService = useService(PluginManagerService); - const defaultValue = pluginManagerService.getSettings(setting.scope)?.getDefaultValue(setting.key); - - const key = `${setting.scope}.${setting.key}`; - const value = source.getValue(key) ?? defaultValue ?? ''; const name = translate(setting.name); const description = translate(setting.description); const disabled = false; - const readOnly = false; + const readOnly = settingsResolverService.isReadOnly(setting.key) ?? false; + + let value = source.getEditedValue(setting.key); + if (readOnly) { + value = settingsResolverService.getValue(setting.key); + } + + if (setting.key in settingsProviderService.schema.shape) { + const schema = settingsProviderService.schema.shape[setting.key]; + if (!isNotNullDefined(value)) { + const result = schema.safeParse(undefined); + value = result.success ? result.data : ''; + } + + const result = schema.safeParse(value); + value = result.success ? result.data : value; + } + + value = value ?? ''; + const customValidation = useCustomInputValidation(value => { - const result = setting.schema.safeParse(value); + if (!(setting.key in settingsProviderService.schema.shape)) { + return null; + } + const result = settingsProviderService.schema.shape[setting.key].safeParse(value); if (result.success) { return null; @@ -41,13 +65,13 @@ export const Setting = observer(function Setting({ source, setting }) { }); function handleChange(value: any) { - source.setValue(key, value); + source.setValue(setting.key, value); } if (setting.type === ESettingsValueType.Checkbox) { return ( (function Setting({ source, setting }) { const options = setting.options?.map(option => ({ ...option, name: translate(option.name) })) || []; return ( value.id} + keySelector={value => value.value} valueSelector={value => value.name} value={value} - title={description} + title={name} disabled={disabled} readOnly={readOnly} - tiny + description={description} + small onSelect={handleChange} > {name} @@ -82,7 +107,15 @@ export const Setting = observer(function Setting({ source, setting }) { if (setting.type === ESettingsValueType.Textarea) { return ( - ); @@ -91,7 +124,7 @@ export const Setting = observer(function Setting({ source, setting }) { return ( (function Settings({ source }) { +export const Settings = observer(function Settings({ source, accessor }) { const translate = useTranslate(); - const settings = useSettings(); + const settings = useSettings(accessor); const treeData = useTreeData({ rootId: ROOT_SETTINGS_GROUP.id, @@ -50,10 +51,14 @@ export const Settings = observer(function Settings({ source }) { return {translate('plugin_settings_panel_empty')}; } + function handleClick(id: string) { + document.querySelector('#' + getSettingGroupId(id))?.scrollIntoView(); + } + return ( diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroup.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroup.tsx new file mode 100644 index 0000000000..9fce782570 --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroup.tsx @@ -0,0 +1,34 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { Group, GroupTitle } from '@cloudbeaver/core-blocks'; +import type { ISettingDescription, ISettingsSource, SettingsGroup as SettingsGroupType } from '@cloudbeaver/core-settings'; + +import { getSettingGroupId } from './getSettingGroupId'; +import { Setting } from './Setting'; +import { SettingsGroupTitle } from './SettingsGroupTitle'; + +interface Props { + group: SettingsGroupType; + source: ISettingsSource; + settings: ISettingDescription[]; +} + +export const SettingsGroup = observer(function SettingsGroup({ group, source, settings }) { + return ( + + ); +}); diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroupTitle.m.css b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroupTitle.m.css new file mode 100644 index 0000000000..cd14771b35 --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroupTitle.m.css @@ -0,0 +1,11 @@ +.icon { + transform: rotate(-90deg); + height: 16px; + width: 16px; +} + +.box { + display: flex; + align-items: center; + gap: 4px; +} diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroupTitle.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroupTitle.tsx new file mode 100644 index 0000000000..0673078577 --- /dev/null +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroupTitle.tsx @@ -0,0 +1,33 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { Icon, s, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { ROOT_SETTINGS_GROUP, type SettingsGroup } from '@cloudbeaver/core-settings'; + +import style from './SettingsGroupTitle.m.css'; + +interface Props { + group: SettingsGroup; +} + +export const SettingsGroupTitle = observer(function SettingsGroupTitle({ group }) { + const translate = useTranslate(); + const styles = useS(style); + return ( +
+ {group.parent && group.parent !== ROOT_SETTINGS_GROUP && ( + <> + + + + )} +
{translate(group.name)}
+
+ ); +}); diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/SettingsGroups.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/SettingsGroups.tsx index 5ce61e1cf7..4b963fbb85 100644 --- a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/SettingsGroups.tsx +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsGroups/SettingsGroups.tsx @@ -9,21 +9,17 @@ import { observer } from 'mobx-react-lite'; import { ITreeData, Tree } from '@cloudbeaver/plugin-navigation-tree'; -import { getSettingGroupId } from '../getSettingGroupId'; import { groupNodeRenderer } from './groupNodeRenderer'; interface Props { treeData: ITreeData; + onClick?: (groupId: string) => void; } -export const SettingsGroups = observer(function SettingsGroups({ treeData }) { +export const SettingsGroups = observer(function SettingsGroups({ treeData, onClick }) { function getNodeHeight(id: string) { return 24; } - function handleClick(id: string) { - document.querySelector('#' + getSettingGroupId(id))?.scrollIntoView(); - } - - return ; + return ; }); diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsList.tsx b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsList.tsx index 094123fe39..443d493530 100644 --- a/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsList.tsx +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/SettingsList.tsx @@ -7,24 +7,26 @@ */ import { observer } from 'mobx-react-lite'; -import { Container, Group, GroupTitle, useTranslate } from '@cloudbeaver/core-blocks'; -import { type ISettingDescriptionWithScope, ROOT_SETTINGS_GROUP, type SettingsGroup } from '@cloudbeaver/core-plugin'; -import type { ISettingsSource } from '@cloudbeaver/core-settings'; +import { Container } from '@cloudbeaver/core-blocks'; +import { + type ISettingDescription, + type ISettingsSource, + ROOT_SETTINGS_GROUP, + type SettingsGroup as SettingsGroupType, +} from '@cloudbeaver/core-settings'; import type { ITreeData } from '@cloudbeaver/plugin-navigation-tree'; -import { getSettingGroupId } from './getSettingGroupId'; -import { Setting } from './Setting'; +import { SettingsGroup } from './SettingsGroup'; import { useTreeScrollSync } from './useTreeScrollSync'; interface Props { treeData: ITreeData; source: ISettingsSource; - settings: Map[]>; + settings: Map[]>; onSettingsOpen?: (groupId: string) => void; } export const SettingsList = observer(function SettingsList({ treeData, source, settings, onSettingsOpen }) { - const translate = useTranslate(); const list = []; const groups = [...treeData.getChildren(treeData.rootId)]; const ref = useTreeScrollSync(treeData, onSettingsOpen); @@ -42,12 +44,7 @@ export const SettingsList = observer(function SettingsList({ treeData, so return ( {list.map(({ group, settings }) => ( - - {translate(group.name)} - {settings.map((setting, i) => ( - - ))} - + ))} ); diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/useSettings.ts b/webapp/packages/plugin-settings-panel/src/SettingsPanel/useSettings.ts index a9d0038e04..0407d7b7a8 100644 --- a/webapp/packages/plugin-settings-panel/src/SettingsPanel/useSettings.ts +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/useSettings.ts @@ -7,23 +7,27 @@ */ import { computed, observable } from 'mobx'; -import { useObservableRef } from '@cloudbeaver/core-blocks'; +import { useAutoLoad, useObservableRef } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; -import { ISettingDescriptionWithScope, ROOT_SETTINGS_GROUP, SettingsGroup, SettingsManagerService } from '@cloudbeaver/core-plugin'; +import { ISettingDescription, ROOT_SETTINGS_GROUP, SettingsGroup, SettingsManagerService } from '@cloudbeaver/core-settings'; interface ISettings { - settings: Map[]>; + settings: Map[]>; groups: Set; } -export function useSettings(): ISettings { +export function useSettings(accessor?: string[]): ISettings { const settingsManagerService = useService(SettingsManagerService); + useAutoLoad(useSettings, settingsManagerService.loaders); + return useObservableRef( () => ({ get settings() { const map = new Map(); - const settings = this.settingsManagerService.getSettings().sort((a, b) => a.name.localeCompare(b.name)); + const settings = this.settingsManagerService.activeSettings + .filter(setting => accessor?.some(value => setting.access.scope.includes(value))) + .sort((a, b) => a.name.localeCompare(b.name)); for (const setting of settings) { map.set(setting.group, [...(map.get(setting.group) || []), setting]); @@ -51,6 +55,6 @@ export function useSettings(): ISettings { groups: computed, settingsManagerService: observable.ref, }, - { settingsManagerService }, + { settingsManagerService, accessor }, ); } diff --git a/webapp/packages/plugin-settings-panel/src/SettingsPanel/useTreeScrollSync.ts b/webapp/packages/plugin-settings-panel/src/SettingsPanel/useTreeScrollSync.ts index 5650395d8d..05f18865c4 100644 --- a/webapp/packages/plugin-settings-panel/src/SettingsPanel/useTreeScrollSync.ts +++ b/webapp/packages/plugin-settings-panel/src/SettingsPanel/useTreeScrollSync.ts @@ -7,7 +7,7 @@ */ import { useEffect, useRef } from 'react'; -import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-plugin'; +import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-settings'; import type { ITreeData } from '@cloudbeaver/plugin-navigation-tree'; import { getSettingGroupIdFromElementId } from './getSettingGroupIdFromElementId'; diff --git a/webapp/packages/plugin-settings-panel/tsconfig.json b/webapp/packages/plugin-settings-panel/tsconfig.json index 8750ededd1..47174ca2d2 100644 --- a/webapp/packages/plugin-settings-panel/tsconfig.json +++ b/webapp/packages/plugin-settings-panel/tsconfig.json @@ -15,9 +15,6 @@ { "path": "../core-localization/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, { "path": "../core-settings/tsconfig.json" }, diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/package.json b/webapp/packages/plugin-sql-editor-navigation-tab-script/package.json index e513638f3b..afe545e738 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/package.json +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/package.json @@ -25,12 +25,12 @@ "@cloudbeaver/core-executor": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-resource-manager": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", + "@cloudbeaver/core-storage": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", "@cloudbeaver/plugin-navigation-tabs": "~0.1.0", diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts index d064fbea4e..f078f91618 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/PluginBootstrap.ts @@ -239,7 +239,7 @@ export class PluginBootstrap extends Bootstrap { const resource = await this.resourceManagerResource.load(resourceKey); - const maxSize = this.sqlEditorSettingsService.settings.getValue('maxFileSize'); + const maxSize = this.sqlEditorSettingsService.maxFileSize; const size = Math.round(resource.length / 1000); // kilobyte if (size > maxSize) { diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceEditorSettingsService.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceEditorSettingsService.ts index a6df6639c2..10741b11f0 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceEditorSettingsService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceEditorSettingsService.ts @@ -6,20 +6,25 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; +import { SettingsManagerService, SettingsProvider, SettingsProviderService } from '@cloudbeaver/core-settings'; import { schema } from '@cloudbeaver/core-utils'; -const defaultSettings = schema.object({}); +const defaultSettings = schema.object({ + 'plugin.sql-editor-navigation-tab-resource': schema.object({}), +}); export type ResourceEditorSettings = typeof defaultSettings; @injectable() export class ResourceEditorSettingsService extends Dependency { - readonly settings: PluginSettings; + readonly settings: SettingsProvider; - constructor(private readonly pluginManagerService: PluginManagerService, private readonly settingsManagerService: SettingsManagerService) { + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + ) { super(); - this.settings = this.pluginManagerService.createSettings('sql-editor-navigation-tab-resource', 'plugin', defaultSettings); + this.settings = this.settingsProviderService.createSettings(defaultSettings); this.registerSettings(); } diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts index 5e4e23611b..2f3d94e65d 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/src/ResourceSqlDataSourceBootstrap.ts @@ -16,7 +16,7 @@ import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects import { ICachedTreeMoveData, resourceKeyList, ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; import { ResourceManagerResource } from '@cloudbeaver/core-resource-manager'; import { NetworkStateService } from '@cloudbeaver/core-root'; -import { SettingsService } from '@cloudbeaver/core-settings'; +import { StorageService } from '@cloudbeaver/core-storage'; import { createPath, getPathParent } from '@cloudbeaver/core-utils'; import { NavigationTabsService } from '@cloudbeaver/plugin-navigation-tabs'; import { NavResourceNodeService } from '@cloudbeaver/plugin-navigation-tree-rm'; @@ -51,7 +51,7 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { private readonly sqlEditorTabResourceService: SqlEditorTabResourceService, private readonly sqlEditorService: SqlEditorService, private readonly projectsService: ProjectsService, - settingsService: SettingsService, + storageService: StorageService, ) { super(); this.dataSourceStateState = new Map(); @@ -61,7 +61,7 @@ export class ResourceSqlDataSourceBootstrap extends Bootstrap { dataSourceStateState: observable, }); - settingsService.registerSettings( + storageService.registerSettings( RESOURCE_TAB_STATE, this.dataSourceStateState, () => new Map(), diff --git a/webapp/packages/plugin-sql-editor-navigation-tab-script/tsconfig.json b/webapp/packages/plugin-sql-editor-navigation-tab-script/tsconfig.json index 2f0f848cdb..92d9867bb6 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab-script/tsconfig.json +++ b/webapp/packages/plugin-sql-editor-navigation-tab-script/tsconfig.json @@ -30,9 +30,6 @@ { "path": "../core-navigation-tree/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, { "path": "../core-projects/tsconfig.json" }, @@ -48,6 +45,9 @@ { "path": "../core-settings/tsconfig.json" }, + { + "path": "../core-storage/tsconfig.json" + }, { "path": "../core-utils/tsconfig.json" }, diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts index dd5f6e16e3..828be24926 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorBootstrap.ts @@ -232,7 +232,7 @@ export class SqlEditorBootstrap extends Bootstrap { }, isHidden: (context, action) => { if (action === ACTION_SQL_EDITOR_NEW) { - return this.sqlEditorSettingsService.settings.getValue('disabled'); + return this.sqlEditorSettingsService.disabled; } return false; diff --git a/webapp/packages/plugin-sql-editor/package.json b/webapp/packages/plugin-sql-editor/package.json index 92214247be..ff53442248 100644 --- a/webapp/packages/plugin-sql-editor/package.json +++ b/webapp/packages/plugin-sql-editor/package.json @@ -28,11 +28,11 @@ "@cloudbeaver/core-executor": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", + "@cloudbeaver/core-storage": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", @@ -55,14 +55,12 @@ "@cloudbeaver/core-events": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-navigation-tree": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", - "@cloudbeaver/core-product": "~0.1.0", "@cloudbeaver/core-projects": "~0.1.0", "@cloudbeaver/core-root": "~0.1.0", "@cloudbeaver/core-routing": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-settings": "~0.1.0", - "@cloudbeaver/core-theming": "~0.1.0", + "@cloudbeaver/core-storage": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", "@cloudbeaver/plugin-data-viewer": "~0.1.0", diff --git a/webapp/packages/plugin-sql-editor/src/SQL_EDITOR_SETTINGS_GROUP.ts b/webapp/packages/plugin-sql-editor/src/SQL_EDITOR_SETTINGS_GROUP.ts index 4c90847f82..61bcedc3ce 100644 --- a/webapp/packages/plugin-sql-editor/src/SQL_EDITOR_SETTINGS_GROUP.ts +++ b/webapp/packages/plugin-sql-editor/src/SQL_EDITOR_SETTINGS_GROUP.ts @@ -5,6 +5,6 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-plugin'; +import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-settings'; export const SQL_EDITOR_SETTINGS_GROUP = ROOT_SETTINGS_GROUP.createSubGroup('plugin_sql_editor_sql_editor_settings_group'); diff --git a/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSourceBootstrap.ts b/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSourceBootstrap.ts index 64b6fd720a..8e141e8b2d 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSourceBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlDataSource/LocalStorage/LocalStorageSqlDataSourceBootstrap.ts @@ -8,7 +8,7 @@ import { action, makeObservable, observable } from 'mobx'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { SettingsService } from '@cloudbeaver/core-settings'; +import { StorageService } from '@cloudbeaver/core-storage'; import { createSqlDataSourceHistoryInitialState } from '../SqlDataSourceHistory/createSqlDataSourceHistoryInitialState'; import { validateSqlDataSourceHistoryState } from '../SqlDataSourceHistory/validateSqlDataSourceHistoryState'; @@ -22,7 +22,10 @@ const localStorageKey = 'local-storage-sql-data-source'; export class LocalStorageSqlDataSourceBootstrap extends Bootstrap { private readonly dataSourceStateState = new Map(); - constructor(private readonly sqlDataSourceService: SqlDataSourceService, settingsService: SettingsService) { + constructor( + private readonly sqlDataSourceService: SqlDataSourceService, + storageService: StorageService, + ) { super(); this.dataSourceStateState = new Map(); @@ -31,7 +34,7 @@ export class LocalStorageSqlDataSourceBootstrap extends Bootstrap { dataSourceStateState: observable.deep, }); - settingsService.registerSettings( + storageService.registerSettings( localStorageKey, this.dataSourceStateState, () => new Map(), diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditor/useTools.tsx b/webapp/packages/plugin-sql-editor/src/SqlEditor/useTools.tsx index 28d3595fdb..90f8e0faba 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditor/useTools.tsx +++ b/webapp/packages/plugin-sql-editor/src/SqlEditor/useTools.tsx @@ -71,7 +71,7 @@ export function useTools(state: ISqlEditorTabState): Readonly { }, checkFileValidity(file: File) { - const maxSize = this.sqlEditorSettingsService.settings.getValue('maxFileSize'); + const maxSize = this.sqlEditorSettingsService.maxFileSize; const size = Math.round(file.size / 1024); // kilobyte const aboveMaxSize = size > maxSize; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts index d8b786611a..1ea6d17d74 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorService.ts @@ -36,11 +36,11 @@ export type SQLProposal = SqlCompletionProposal; @injectable() export class SqlEditorService { get autoSave() { - return this.sqlEditorSettingsService.settings.getValue('autoSave') && !this.serverConfigResource.isFeatureEnabled(FEATURE_GIT_ID, true); + return this.sqlEditorSettingsService.autoSave && !this.serverConfigResource.isFeatureEnabled(FEATURE_GIT_ID, true); } - get canInsertAlias() { - return this.sqlEditorSettingsService.proposalInsertTableSettings.getValue('alias'); + get insertTableAlias() { + return this.sqlEditorSettingsService.insertTableAlias; } constructor( diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.test.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.test.ts index 8730ae712e..d8175b1806 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.test.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.test.ts @@ -18,18 +18,21 @@ import { coreDialogsManifest } from '@cloudbeaver/core-dialogs'; import { coreEventsManifest } from '@cloudbeaver/core-events'; import { coreLocalizationManifest } from '@cloudbeaver/core-localization'; import { coreNavigationTree } from '@cloudbeaver/core-navigation-tree'; -import { corePluginManifest } from '@cloudbeaver/core-plugin'; -import { coreProductManifest } from '@cloudbeaver/core-product'; import { coreProjectsManifest } from '@cloudbeaver/core-projects'; import { coreRootManifest, ServerConfigResource } from '@cloudbeaver/core-root'; import { createGQLEndpoint } from '@cloudbeaver/core-root/dist/__custom_mocks__/createGQLEndpoint'; +import '@cloudbeaver/core-root/dist/__custom_mocks__/expectWebsocketClosedMessage'; import { mockAppInit } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockAppInit'; import { mockGraphQL } from '@cloudbeaver/core-root/dist/__custom_mocks__/mockGraphQL'; import { mockServerConfig } from '@cloudbeaver/core-root/dist/__custom_mocks__/resolvers/mockServerConfig'; import { coreRoutingManifest } from '@cloudbeaver/core-routing'; import { coreSDKManifest } from '@cloudbeaver/core-sdk'; import { coreSettingsManifest } from '@cloudbeaver/core-settings'; -import { coreThemingManifest } from '@cloudbeaver/core-theming'; +import { + expectDeprecatedSettingMessage, + expectNoDeprecatedSettingMessage, +} from '@cloudbeaver/core-settings/dist/__custom_mocks__/expectDeprecatedSettingMessage'; +import { coreStorageManifest } from '@cloudbeaver/core-storage'; import { coreUIManifest } from '@cloudbeaver/core-ui'; import { coreViewManifest } from '@cloudbeaver/core-view'; import { dataViewerManifest } from '@cloudbeaver/plugin-data-viewer'; @@ -40,20 +43,20 @@ import { objectViewerManifest } from '@cloudbeaver/plugin-object-viewer'; import { createApp } from '@cloudbeaver/tests-runner'; import { sqlEditorPluginManifest } from './manifest'; -import { SqlEditorSettings, SqlEditorSettingsService } from './SqlEditorSettingsService'; +import { SqlEditorSettingsService } from './SqlEditorSettingsService'; const endpoint = createGQLEndpoint(); +const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); const app = createApp( sqlEditorPluginManifest, coreLocalizationManifest, coreEventsManifest, - corePluginManifest, - coreProductManifest, coreRootManifest, coreSDKManifest, coreBrowserManifest, coreSettingsManifest, coreViewManifest, + coreStorageManifest, coreAuthenticationManifest, coreProjectsManifest, coreUIManifest, @@ -63,7 +66,6 @@ const app = createApp( coreDialogsManifest, coreNavigationTree, coreAppManifest, - coreThemingManifest, datasourceContextSwitchPluginManifest, navigationTreePlugin, navigationTabsPlugin, @@ -72,30 +74,16 @@ const app = createApp( coreClientActivityManifest, ); -const server = mockGraphQL(...mockAppInit(endpoint), ...mockAuthentication(endpoint)); - -beforeAll(() => app.init()); - const testValueNew = 1; const testValueDeprecated = 2; const deprecatedSettings = { - core: { - app: { - sqlEditor: { - maxFileSize: testValueDeprecated, - } as SqlEditorSettings, - }, - }, + 'core.app.sqlEditor.maxFileSize': testValueDeprecated, }; const newSettings = { ...deprecatedSettings, - plugin: { - 'sql-editor': { - maxFileSize: testValueNew, - } as SqlEditorSettings, - }, + 'plugin.sql-editor.maxFileSize': testValueNew, }; test('New settings override deprecated settings', async () => { @@ -106,7 +94,8 @@ test('New settings override deprecated settings', async () => { await config.refresh(); - expect(settings.settings.getValue('maxFileSize')).toBe(testValueNew); + expect(settings.maxFileSize).toBe(testValueNew); + expectNoDeprecatedSettingMessage(); }); test('Deprecated settings are used if new settings are not defined', async () => { @@ -117,5 +106,6 @@ test('Deprecated settings are used if new settings are not defined', async () => await config.refresh(); - expect(settings.settings.getValue('maxFileSize')).toBe(testValueDeprecated); + expect(settings.maxFileSize).toBe(testValueDeprecated); + expectDeprecatedSettingMessage(); }); diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts index f311e5f971..521393b0a5 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorSettingsService.ts @@ -6,83 +6,152 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; +import { ServerSettingsManagerService } from '@cloudbeaver/core-root'; import { createSettingsAliasResolver, ESettingsValueType, - PluginManagerService, - PluginSettings, + ISettingDescription, + ROOT_SETTINGS_LAYER, SettingsManagerService, -} from '@cloudbeaver/core-plugin'; -import { ServerSettingsResolverService, ServerSettingsService } from '@cloudbeaver/core-root'; -import { schema } from '@cloudbeaver/core-utils'; + SettingsProvider, + SettingsProviderService, + SettingsResolverService, +} from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; import { SQL_EDITOR_SETTINGS_GROUP } from './SQL_EDITOR_SETTINGS_GROUP'; -const defaultSettings = schema.object({ - maxFileSize: schema.coerce.number().default(10 * 1024), // kilobyte - disabled: schema.coerce.boolean().default(false), - autoSave: schema.coerce.boolean().default(true), -}); +const TABLE_ALIAS_OPTIONS = ['NONE', 'PLAIN', 'EXTENDED'] as const; -const defaultProposalInsertTableSettings = schema.object({ - alias: schema.coerce.boolean().default(true), +const TABLE_ALIAS_SETTING_OPTIONS = [ + { + value: 'NONE', + name: 'ui_disable', + }, + { + value: 'PLAIN', + name: 'my_table mt', + }, + { + value: 'EXTENDED', + name: 'my_table AS mt', + }, +]; + +const defaultSettings = schema.object({ + 'plugin.sql-editor.maxFileSize': schema.coerce.number().default(10 * 1024), // kilobyte + 'plugin.sql-editor.disabled': schemaExtra.stringedBoolean().default(false), + 'plugin.sql-editor.autoSave': schemaExtra.stringedBoolean().default(true), + 'sql.proposals.insert.table.alias': schema.coerce + .string() + .transform(value => { + switch (value) { + case 'false': + return 'NONE'; + case 'true': + return 'PLAIN'; + default: + return value; + } + }) + .pipe(schema.enum(TABLE_ALIAS_OPTIONS)) + .default('PLAIN'), }); export type SqlEditorSettings = schema.infer; @injectable() export class SqlEditorSettingsService extends Dependency { - readonly settings: PluginSettings; - readonly proposalInsertTableSettings: PluginSettings; + get maxFileSize(): number { + return this.settings.getValue('plugin.sql-editor.maxFileSize'); + } + + get disabled(): boolean { + return this.settings.getValue('plugin.sql-editor.disabled'); + } + + get autoSave(): boolean { + return this.settings.getValue('plugin.sql-editor.autoSave'); + } + + get insertTableAlias(): schema.infer['sql.proposals.insert.table.alias'] { + return this.settings.getValue('sql.proposals.insert.table.alias'); + } + + readonly settings: SettingsProvider; constructor( - private readonly pluginManagerService: PluginManagerService, + private readonly settingsProviderService: SettingsProviderService, private readonly settingsManagerService: SettingsManagerService, - private readonly serverSettingsService: ServerSettingsService, - private readonly serverSettingsResolverService: ServerSettingsResolverService, + private readonly settingsResolverService: SettingsResolverService, + private readonly serverSettingsManagerService: ServerSettingsManagerService, ) { super(); - this.settings = this.pluginManagerService.createSettings('sql-editor', 'plugin', defaultSettings); - this.proposalInsertTableSettings = this.pluginManagerService.createSettings('proposals.insert.table', 'sql', defaultProposalInsertTableSettings); - this.serverSettingsResolverService.addResolver( + this.settings = this.settingsProviderService.createSettings(defaultSettings); + this.settingsResolverService.addResolver( + ROOT_SETTINGS_LAYER, /** @deprecated Use settings instead, will be removed in 23.0.0 */ - createSettingsAliasResolver(this.serverSettingsService, this.settings, 'core.app.sqlEditor'), + createSettingsAliasResolver(this.settingsResolverService, this.settings, { + 'plugin.sql-editor.autoSave': 'core.app.sqlEditor.autoSave', + 'plugin.sql-editor.maxFileSize': 'core.app.sqlEditor.maxFileSize', + 'plugin.sql-editor.disabled': 'core.app.sqlEditor.disabled', + }), ); this.registerSettings(); } private registerSettings() { - this.settingsManagerService.registerSettings(this.settings, () => [ - // { - // group: SQL_EDITOR_SETTINGS_GROUP, - // key: 'disabled', - // type: ESettingsValueType.Checkbox, - // name: 'Disable SQL editor', - // }, - // { - // group: SQL_EDITOR_SETTINGS_GROUP, - // key: 'maxFileSize', - // type: ESettingsValueType.Input, - // name: 'Max file size (KB)', - // description: 'Max file size for SQL editor in kilobytes', - // }, - // { - // group: SQL_EDITOR_SETTINGS_GROUP, - // key: 'autoSave', - // type: ESettingsValueType.Checkbox, - // name: 'Auto save', - // description: 'Auto save SQL editor content', - // }, - ]); + this.serverSettingsManagerService.setGroupOverride('editors/sqlEditor', SQL_EDITOR_SETTINGS_GROUP); + this.serverSettingsManagerService.setSettingTransformer( + 'sql.proposals.insert.table.alias', + setting => + ({ + ...setting, + group: SQL_EDITOR_SETTINGS_GROUP, + name: 'sql_editor_settings_insert_table_aliases_name', + description: 'sql_editor_settings_insert_table_aliases_desc', + options: [...(setting.options?.filter(option => !TABLE_ALIAS_OPTIONS.includes(option.value as any)) || []), ...TABLE_ALIAS_SETTING_OPTIONS], + }) as ISettingDescription, + ); - this.settingsManagerService.registerSettings(this.proposalInsertTableSettings, () => [ - { - group: SQL_EDITOR_SETTINGS_GROUP, - key: 'alias', - type: ESettingsValueType.Checkbox, - name: 'sql_editor_settings_insert_table_aliases_name', - description: 'sql_editor_settings_insert_table_aliases_desc', - }, - ]); + this.settingsManagerService.registerSettings(this.settings, () => { + const settings: ISettingDescription[] = [ + // { + // group: SQL_EDITOR_SETTINGS_GROUP, + // key: 'disabled', + // type: ESettingsValueType.Checkbox, + // name: 'Disable SQL editor', + // }, + // { + // group: SQL_EDITOR_SETTINGS_GROUP, + // key: 'maxFileSize', + // type: ESettingsValueType.Input, + // name: 'Max file size (KB)', + // description: 'Max file size for SQL editor in kilobytes', + // }, + // { + // group: SQL_EDITOR_SETTINGS_GROUP, + // key: 'autoSave', + // type: ESettingsValueType.Checkbox, + // name: 'Auto save', + // description: 'Auto save SQL editor content', + // }, + ]; + + if (!this.serverSettingsManagerService.providedSettings.has('sql.proposals.insert.table.alias')) { + settings.push({ + key: 'sql.proposals.insert.table.alias', + access: { + scope: ['server', 'client'], + }, + group: SQL_EDITOR_SETTINGS_GROUP, + type: ESettingsValueType.Select, + name: 'sql_editor_settings_insert_table_aliases_name', + description: 'sql_editor_settings_insert_table_aliases_desc', + options: TABLE_ALIAS_SETTING_OPTIONS, + }); + } + return settings; + }); } } diff --git a/webapp/packages/plugin-sql-editor/tsconfig.json b/webapp/packages/plugin-sql-editor/tsconfig.json index 57f61ab7c2..3a53123311 100644 --- a/webapp/packages/plugin-sql-editor/tsconfig.json +++ b/webapp/packages/plugin-sql-editor/tsconfig.json @@ -66,15 +66,6 @@ { "path": "../core-navigation-tree/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-plugin/tsconfig.json" - }, - { - "path": "../core-product/tsconfig.json" - }, { "path": "../core-projects/tsconfig.json" }, @@ -103,7 +94,10 @@ "path": "../core-settings/tsconfig.json" }, { - "path": "../core-theming/tsconfig.json" + "path": "../core-storage/tsconfig.json" + }, + { + "path": "../core-storage/tsconfig.json" }, { "path": "../core-ui/tsconfig.json" diff --git a/webapp/packages/plugin-theme/src/PluginBootstrap.ts b/webapp/packages/plugin-theme/src/PluginBootstrap.ts index e2dd8ad7a5..705e64fdfd 100644 --- a/webapp/packages/plugin-theme/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-theme/src/PluginBootstrap.ts @@ -14,7 +14,10 @@ import { THEME_MENU } from './THEME_MENU'; @injectable() export class PluginBootstrap extends Bootstrap { - constructor(private readonly menuService: MenuService, private readonly themeService: ThemeService) { + constructor( + private readonly menuService: MenuService, + private readonly themeService: ThemeService, + ) { super(); } @@ -43,7 +46,7 @@ export class PluginBootstrap extends Bootstrap { onSelect: () => this.themeService.changeTheme(theme.id), }, { - isDisabled: () => this.themeService.currentThemeId === theme.id, + isDisabled: () => this.themeService.themeId === theme.id, }, ), ); diff --git a/webapp/packages/plugin-tools-panel/package.json b/webapp/packages/plugin-tools-panel/package.json index 37731f399d..7249fac7c1 100644 --- a/webapp/packages/plugin-tools-panel/package.json +++ b/webapp/packages/plugin-tools-panel/package.json @@ -21,7 +21,7 @@ "@cloudbeaver/core-blocks": "~0.1.0", "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-plugin": "~0.1.0", + "@cloudbeaver/core-settings": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", "@cloudbeaver/core-utils": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", diff --git a/webapp/packages/plugin-tools-panel/src/TOOLS_PANEL_SETTINGS_GROUP.ts b/webapp/packages/plugin-tools-panel/src/TOOLS_PANEL_SETTINGS_GROUP.ts index 73533ab30c..8e929950a5 100644 --- a/webapp/packages/plugin-tools-panel/src/TOOLS_PANEL_SETTINGS_GROUP.ts +++ b/webapp/packages/plugin-tools-panel/src/TOOLS_PANEL_SETTINGS_GROUP.ts @@ -5,6 +5,6 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-plugin'; +import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-settings'; export const TOOLS_PANEL_SETTINGS_GROUP = ROOT_SETTINGS_GROUP.createSubGroup('plugin_tools_panel_tools_settings_group'); diff --git a/webapp/packages/plugin-tools-panel/src/ToolsPanel/ToolsPanelService.ts b/webapp/packages/plugin-tools-panel/src/ToolsPanel/ToolsPanelService.ts index f1eaa10aaa..d9bb627840 100644 --- a/webapp/packages/plugin-tools-panel/src/ToolsPanel/ToolsPanelService.ts +++ b/webapp/packages/plugin-tools-panel/src/ToolsPanel/ToolsPanelService.ts @@ -17,7 +17,7 @@ export class ToolsPanelService { readonly tabsContainer: TabsContainer; get disabled() { - return this.toolsPanelSettingsService.settings.getValue('disabled'); + return this.toolsPanelSettingsService.disabled; } constructor(private readonly toolsPanelSettingsService: ToolsPanelSettingsService) { diff --git a/webapp/packages/plugin-tools-panel/src/ToolsPanelSettingsService.ts b/webapp/packages/plugin-tools-panel/src/ToolsPanelSettingsService.ts index 76f033bff5..6d1df02f76 100644 --- a/webapp/packages/plugin-tools-panel/src/ToolsPanelSettingsService.ts +++ b/webapp/packages/plugin-tools-panel/src/ToolsPanelSettingsService.ts @@ -6,24 +6,31 @@ * you may not use this file except in compliance with the License. */ import { Dependency, injectable } from '@cloudbeaver/core-di'; -import { ESettingsValueType, PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; -import { schema } from '@cloudbeaver/core-utils'; +import { ESettingsValueType, SettingsManagerService, SettingsProvider, SettingsProviderService } from '@cloudbeaver/core-settings'; +import { schema, schemaExtra } from '@cloudbeaver/core-utils'; import { TOOLS_PANEL_SETTINGS_GROUP } from './TOOLS_PANEL_SETTINGS_GROUP'; const settings = schema.object({ - disabled: schema.coerce.boolean().default(false), + 'plugin.tools-panel.disabled': schemaExtra.stringedBoolean().default(false), }); type Settings = typeof settings; @injectable() export class ToolsPanelSettingsService extends Dependency { - readonly settings: PluginSettings; + get disabled(): boolean { + return this.settings.getValue('plugin.tools-panel.disabled'); + } - constructor(private readonly pluginManagerService: PluginManagerService, private readonly settingsManagerService: SettingsManagerService) { + readonly settings: SettingsProvider; + + constructor( + private readonly settingsProviderService: SettingsProviderService, + private readonly settingsManagerService: SettingsManagerService, + ) { super(); - this.settings = this.pluginManagerService.createSettings('tools-panel', 'plugin', settings); + this.settings = this.settingsProviderService.createSettings(settings); this.registerSettings(); } @@ -31,8 +38,11 @@ export class ToolsPanelSettingsService extends Dependency { private registerSettings() { this.settingsManagerService.registerSettings(this.settings, () => [ { + key: 'plugin.tools-panel.disabled', + access: { + scope: ['server'], + }, group: TOOLS_PANEL_SETTINGS_GROUP, - key: 'disabled', type: ESettingsValueType.Checkbox, name: 'plugin_tools_panel_settings_disable_label', description: 'plugin_tools_panel_settings_disable_description', diff --git a/webapp/packages/plugin-tools-panel/tsconfig.json b/webapp/packages/plugin-tools-panel/tsconfig.json index 56167e7ee0..e953525baa 100644 --- a/webapp/packages/plugin-tools-panel/tsconfig.json +++ b/webapp/packages/plugin-tools-panel/tsconfig.json @@ -19,7 +19,7 @@ "path": "../core-localization/tsconfig.json" }, { - "path": "../core-plugin/tsconfig.json" + "path": "../core-settings/tsconfig.json" }, { "path": "../core-ui/tsconfig.json" diff --git a/webapp/packages/plugin-user-profile-settings/.gitignore b/webapp/packages/plugin-user-profile-settings/.gitignore new file mode 100644 index 0000000000..15bc16c7c3 --- /dev/null +++ b/webapp/packages/plugin-user-profile-settings/.gitignore @@ -0,0 +1,17 @@ +# dependencies +/node_modules + +# testing +/coverage + +# production +/lib + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/plugin-user-profile-settings/package.json b/webapp/packages/plugin-user-profile-settings/package.json new file mode 100644 index 0000000000..bd3fef142f --- /dev/null +++ b/webapp/packages/plugin-user-profile-settings/package.json @@ -0,0 +1,37 @@ +{ + "name": "@cloudbeaver/plugin-user-profile-settings", + "sideEffects": [ + "src/**/*.css", + "src/**/*.scss", + "public/**/*" + ], + "version": "0.1.0", + "description": "", + "license": "Apache-2.0", + "main": "dist/index.js", + "scripts": { + "build": "tsc -b", + "lint": "eslint ./src/ --ext .ts,.tsx", + "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", + "validate-dependencies": "core-cli-validate-dependencies", + "update-ts-references": "rimraf --glob dist && typescript-resolve-references" + }, + "dependencies": { + "@cloudbeaver/core-blocks": "~0.1.0", + "@cloudbeaver/core-di": "~0.1.0", + "@cloudbeaver/core-events": "~0.1.0", + "@cloudbeaver/core-localization": "~0.1.0", + "@cloudbeaver/core-settings-user": "~0.1.0", + "@cloudbeaver/plugin-settings-panel": "~0.1.0", + "@cloudbeaver/plugin-user-profile": "~0.1.0", + "mobx": "^6.12.0", + "mobx-react-lite": "^4.0.5", + "react": "^18.2.0" + }, + "peerDependencies": {}, + "devDependencies": { + "@types/react": "^18.2.63", + "typescript": "^5.3.3", + "typescript-plugin-css-modules": "^5.1.0" + } +} diff --git a/webapp/packages/plugin-user-profile-settings/src/LocaleService.ts b/webapp/packages/plugin-user-profile-settings/src/LocaleService.ts new file mode 100644 index 0000000000..edeb82d255 --- /dev/null +++ b/webapp/packages/plugin-user-profile-settings/src/LocaleService.ts @@ -0,0 +1,35 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { 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/plugin-user-profile-settings/src/UserProfileSettings.m.css b/webapp/packages/plugin-user-profile-settings/src/UserProfileSettings.m.css new file mode 100644 index 0000000000..9a062e342e --- /dev/null +++ b/webapp/packages/plugin-user-profile-settings/src/UserProfileSettings.m.css @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.form { + display: flex; + flex-direction: column; + height: 100%; + flex: 1; +} diff --git a/webapp/packages/plugin-user-profile-settings/src/UserProfileSettings.tsx b/webapp/packages/plugin-user-profile-settings/src/UserProfileSettings.tsx new file mode 100644 index 0000000000..5401db3f97 --- /dev/null +++ b/webapp/packages/plugin-user-profile-settings/src/UserProfileSettings.tsx @@ -0,0 +1,67 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { ColoredContainer, Form, Group, s, ToolsAction, ToolsPanel, useForm, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { UserSettingsService } from '@cloudbeaver/core-settings-user'; +import { Settings } from '@cloudbeaver/plugin-settings-panel'; + +import style from './UserProfileSettings.m.css'; + +const clientScope = ['client']; + +export const UserProfileSettings = observer(function UserProfileSettings() { + const translate = useTranslate(); + const userSettingsService = useService(UserSettingsService); + const notificationService = useService(NotificationService); + const styles = useS(style); + + const changed = userSettingsService.isEdited(); + + async function handleSave() { + if (!changed) { + return; + } + try { + await userSettingsService.save(); + notificationService.logSuccess({ title: 'plugin_user_profile_settings_save_success' }); + } catch (error: any) { + notificationService.logException(error, 'plugin_user_profile_settings_save_fail'); + } + } + + const form = useForm({ + async onSubmit() { + await handleSave(); + }, + }); + + function handleReset() { + userSettingsService.resetChanges(); + } + + return ( +
+ + + + form.submit()}> + {translate('ui_processing_save')} + + + {translate('ui_processing_cancel')} + + + + + +
+ ); +}); diff --git a/webapp/packages/plugin-user-profile-settings/src/UserProfileSettingsPluginBootstrap.ts b/webapp/packages/plugin-user-profile-settings/src/UserProfileSettingsPluginBootstrap.ts new file mode 100644 index 0000000000..3e415fab6d --- /dev/null +++ b/webapp/packages/plugin-user-profile-settings/src/UserProfileSettingsPluginBootstrap.ts @@ -0,0 +1,28 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; +import { UserProfileTabsService } from '@cloudbeaver/plugin-user-profile'; + +const UserProfileSettings = importLazyComponent(() => import('./UserProfileSettings').then(module => module.UserProfileSettings)); + +@injectable() +export class UserProfileSettingsPluginBootstrap extends Bootstrap { + constructor(private readonly userProfileTabsService: UserProfileTabsService) { + super(); + } + + register(): void { + this.userProfileTabsService.tabContainer.add({ + key: 'settings', + name: 'plugin_user_profile_settings_tab_label', + order: 2, + panel: () => UserProfileSettings, + }); + } +} diff --git a/webapp/packages/plugin-user-profile-settings/src/index.ts b/webapp/packages/plugin-user-profile-settings/src/index.ts new file mode 100644 index 0000000000..86d04e5aec --- /dev/null +++ b/webapp/packages/plugin-user-profile-settings/src/index.ts @@ -0,0 +1,2 @@ +export * from './manifest'; + diff --git a/webapp/packages/plugin-user-profile-settings/src/locales/en.ts b/webapp/packages/plugin-user-profile-settings/src/locales/en.ts new file mode 100644 index 0000000000..e64ad0a8f4 --- /dev/null +++ b/webapp/packages/plugin-user-profile-settings/src/locales/en.ts @@ -0,0 +1,5 @@ +export default [ + ['plugin_user_profile_settings_tab_label', 'Settings'], + ['plugin_user_profile_settings_save_success', 'Settings saved successfully'], + ['plugin_user_profile_settings_save_fail', 'Failed to save settings'], +]; diff --git a/webapp/packages/plugin-user-profile-settings/src/locales/it.ts b/webapp/packages/plugin-user-profile-settings/src/locales/it.ts new file mode 100644 index 0000000000..e64ad0a8f4 --- /dev/null +++ b/webapp/packages/plugin-user-profile-settings/src/locales/it.ts @@ -0,0 +1,5 @@ +export default [ + ['plugin_user_profile_settings_tab_label', 'Settings'], + ['plugin_user_profile_settings_save_success', 'Settings saved successfully'], + ['plugin_user_profile_settings_save_fail', 'Failed to save settings'], +]; diff --git a/webapp/packages/plugin-user-profile-settings/src/locales/ru.ts b/webapp/packages/plugin-user-profile-settings/src/locales/ru.ts new file mode 100644 index 0000000000..cc741b4cad --- /dev/null +++ b/webapp/packages/plugin-user-profile-settings/src/locales/ru.ts @@ -0,0 +1,5 @@ +export default [ + ['plugin_user_profile_settings_tab_label', 'Настройки'], + ['plugin_user_profile_settings_save_success', 'Настройки успешно сохранены'], + ['plugin_user_profile_settings_save_fail', 'Не удалось сохранить настройки'], +]; diff --git a/webapp/packages/plugin-user-profile-settings/src/locales/zh.ts b/webapp/packages/plugin-user-profile-settings/src/locales/zh.ts new file mode 100644 index 0000000000..e64ad0a8f4 --- /dev/null +++ b/webapp/packages/plugin-user-profile-settings/src/locales/zh.ts @@ -0,0 +1,5 @@ +export default [ + ['plugin_user_profile_settings_tab_label', 'Settings'], + ['plugin_user_profile_settings_save_success', 'Settings saved successfully'], + ['plugin_user_profile_settings_save_fail', 'Failed to save settings'], +]; diff --git a/webapp/packages/plugin-user-profile-settings/src/manifest.ts b/webapp/packages/plugin-user-profile-settings/src/manifest.ts new file mode 100644 index 0000000000..e4c119f45c --- /dev/null +++ b/webapp/packages/plugin-user-profile-settings/src/manifest.ts @@ -0,0 +1,22 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { PluginManifest } from '@cloudbeaver/core-di'; + +import { LocaleService } from './LocaleService'; +import { UserProfileSettingsPluginBootstrap } from './UserProfileSettingsPluginBootstrap'; + +export const userProfileSettingsPlugin: PluginManifest = { + info: { + name: 'User profile settings plugin', + }, + + providers: [ + LocaleService, + UserProfileSettingsPluginBootstrap, + ], +}; diff --git a/webapp/packages/plugin-user-profile-settings/tsconfig.json b/webapp/packages/plugin-user-profile-settings/tsconfig.json new file mode 100644 index 0000000000..a308990baf --- /dev/null +++ b/webapp/packages/plugin-user-profile-settings/tsconfig.json @@ -0,0 +1,43 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" + }, + "references": [ + { + "path": "../core-blocks/tsconfig.json" + }, + { + "path": "../core-di/tsconfig.json" + }, + { + "path": "../core-events/tsconfig.json" + }, + { + "path": "../core-localization/tsconfig.json" + }, + { + "path": "../core-settings-user/tsconfig.json" + }, + { + "path": "../plugin-settings-panel/tsconfig.json" + }, + { + "path": "../plugin-user-profile/tsconfig.json" + } + ], + "include": [ + "__custom_mocks__/**/*", + "src/**/*", + "src/**/*.json", + "src/**/*.css", + "src/**/*.scss" + ], + "exclude": [ + "**/node_modules", + "lib/**/*", + "dist/**/*" + ] +} diff --git a/webapp/packages/plugin-user-profile/package.json b/webapp/packages/plugin-user-profile/package.json index bf4e240cf7..ad65159c6a 100644 --- a/webapp/packages/plugin-user-profile/package.json +++ b/webapp/packages/plugin-user-profile/package.json @@ -19,6 +19,7 @@ "dependencies": { "@cloudbeaver/core-authentication": "~0.1.0", "@cloudbeaver/core-blocks": "~0.1.0", + "@cloudbeaver/core-data-context": "~0.1.0", "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-dialogs": "~0.1.0", "@cloudbeaver/core-events": "~0.1.0", @@ -27,6 +28,7 @@ "@cloudbeaver/core-resource": "~0.1.0", "@cloudbeaver/core-sdk": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", + "@cloudbeaver/core-utils": "~0.1.0", "@cloudbeaver/core-view": "~0.1.0", "@cloudbeaver/plugin-authentication": "~0.1.0", "@cloudbeaver/plugin-top-app-bar": "~0.1.0", diff --git a/webapp/packages/plugin-user-profile/src/PluginBootstrap.ts b/webapp/packages/plugin-user-profile/src/PluginBootstrap.ts index 9675dfb089..fed9344ecc 100644 --- a/webapp/packages/plugin-user-profile/src/PluginBootstrap.ts +++ b/webapp/packages/plugin-user-profile/src/PluginBootstrap.ts @@ -5,10 +5,11 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { TopNavService } from '@cloudbeaver/plugin-top-app-bar'; -import { UserMenu } from './UserMenu/UserMenu'; +const UserMenu = importLazyComponent(() => import('./UserMenu/UserMenu').then(m => m.UserMenu)); @injectable() export class PluginBootstrap extends Bootstrap { @@ -19,6 +20,4 @@ export class PluginBootstrap extends Bootstrap { register(): void { this.topNavService.placeholder.add(UserMenu, 4); } - - load(): void {} } diff --git a/webapp/packages/plugin-user-profile/src/UserInfo.m.css b/webapp/packages/plugin-user-profile/src/UserInfo.m.css index 7a0bf57c85..3d641c7e2a 100644 --- a/webapp/packages/plugin-user-profile/src/UserInfo.m.css +++ b/webapp/packages/plugin-user-profile/src/UserInfo.m.css @@ -34,10 +34,11 @@ display: block; width: 24px; height: 24px; + padding: 4px; } .userName { display: block; line-height: initial; - margin-left: 8px; + padding: 0 4px; } diff --git a/webapp/packages/plugin-user-profile/src/UserInfo.tsx b/webapp/packages/plugin-user-profile/src/UserInfo.tsx index 8a20ad1b3c..f252324b40 100644 --- a/webapp/packages/plugin-user-profile/src/UserInfo.tsx +++ b/webapp/packages/plugin-user-profile/src/UserInfo.tsx @@ -12,7 +12,7 @@ import { useService } from '@cloudbeaver/core-di'; import type { UserInfo as IUserInfo } from '@cloudbeaver/core-sdk'; import styles from './UserInfo.m.css'; -import { UserProfileService } from './UserProfileService'; +import { UserProfileOptionsPanelService } from './UserProfileOptionsPanelService'; interface Props { info: IUserInfo; @@ -20,11 +20,11 @@ interface Props { export const UserInfo = observer(function UserInfo({ info }) { const translate = useTranslate(); - const userProfileService = useService(UserProfileService); + const userProfileOptionsPanelService = useService(UserProfileOptionsPanelService); const style = useS(styles); return ( -
userProfileService.open()}> +
userProfileOptionsPanelService.open()}>
{info.displayName || info.userId}
diff --git a/webapp/packages/plugin-user-profile/src/UserMenu/UserMenuLazy.ts b/webapp/packages/plugin-user-profile/src/UserMenu/UserMenuLazy.ts new file mode 100644 index 0000000000..d72538790f --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserMenu/UserMenuLazy.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; + +export const UserMenu = importLazyComponent(() => import('./UserMenu').then(m => m.UserMenu)); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthProviders/AuthProvidersList.m.css b/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthProviders/AuthProvidersList.m.css deleted file mode 100644 index dfe45ec4ab..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthProviders/AuthProvidersList.m.css +++ /dev/null @@ -1,25 +0,0 @@ -.container { - display: flex; - flex-direction: column; - overflow: auto; -} - -.list { - overflow: auto; -} - -.cell { - composes: theme-border-color-secondary from global; - border-bottom: 1px solid; - padding: 0 16px; -} - -.iconOrImage { - width: 100%; - height: 100%; -} - -.providerLoginIcon { - width: 24px; - height: 24px; -} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthProviders/AuthProvidersList.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthProviders/AuthProvidersList.tsx deleted file mode 100644 index 82a8e39089..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthProviders/AuthProvidersList.tsx +++ /dev/null @@ -1,68 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { observer } from 'mobx-react-lite'; - -import { AuthProvidersResource } from '@cloudbeaver/core-authentication'; -import { Cell, getComputed, IconOrImage, Link, Loader, s, useResource, useS, useTranslate } from '@cloudbeaver/core-blocks'; -import { resourceKeyList } from '@cloudbeaver/core-resource'; -import type { UserInfo } from '@cloudbeaver/core-sdk'; - -import styles from './AuthProvidersList.m.css'; - -interface Props { - user: UserInfo; - providers: string[]; - className?: string; -} - -export const AuthProvidersList = observer(function AuthProvidersList({ user, providers, className }) { - const providersResource = useResource(AuthProvidersList, AuthProvidersResource, resourceKeyList(providers)); - const translate = useTranslate(); - const style = useS(styles); - const activeProviders = getComputed(() => user.authTokens.map(token => token.authProvider)); - - return ( -
- - {() => ( -
- {providersResource.data.map(provider => { - if (!provider) { - return null; - } - - const title = `${provider.label}\n${provider.description || ''}`; - return ( - - : undefined} - after={ - activeProviders.includes(provider.id) ? ( -
- -
- ) : undefined - } - description={provider.description} - > - {provider.label} -
- - ); - })} -
- )} -
-
- ); -}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthTokens/AuthTokenList.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthTokens/AuthTokenList.tsx deleted file mode 100644 index 03673018aa..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthTokens/AuthTokenList.tsx +++ /dev/null @@ -1,99 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { observer } from 'mobx-react-lite'; - -import { AuthProviderConfiguration, AuthProvidersResource } from '@cloudbeaver/core-authentication'; -import { Button, Cell, IconOrImage, Link, Loader, s, useResource, useS, useTranslate } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { CachedMapAllKey } from '@cloudbeaver/core-resource'; -import type { UserInfo } from '@cloudbeaver/core-sdk'; -import { AuthenticationService } from '@cloudbeaver/plugin-authentication'; - -import styles from './AuthTokenList.m.css'; - -interface Props { - user: UserInfo; - className?: string; -} - -export const AuthTokenList = observer(function AuthTokenList({ user, className }) { - const providersResource = useResource(AuthTokenList, AuthProvidersResource, CachedMapAllKey); - const authenticationService = useService(AuthenticationService); - const style = useS(styles); - const translate = useTranslate(); - - return ( -
- -
- {user.authTokens.map(token => { - const provider = providersResource.resource.get(token.authProvider); - - if (!provider) { - return null; - } - - let configuration: AuthProviderConfiguration | undefined; - let name = provider.label; - let description = token.message || provider.description; - let icon = provider.icon; - - if (token.authConfiguration) { - configuration = provider.configurations?.find(configuration => configuration.id === token.authConfiguration); - - if (configuration) { - name = configuration.displayName; - description = configuration.description; - - if (configuration.iconURL) { - icon = configuration.iconURL; - } - } - } - - const date = new Date(token.loginTime); - const title = `${name}\n${description || ''}`; - - return ( - - : undefined} - after={ - - } - description={ - <> - {description} -
- {token.userId && ( - <> - {token.userId} -
- - )} - {date ? ( - - ) : undefined} - - } - > - {name} -
- - ); - })} -
-
-
- ); -}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/AuthenticationPanel.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/AuthenticationPanel.tsx deleted file mode 100644 index beade8842c..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/AuthenticationPanel.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { observer } from 'mobx-react-lite'; - -import { ColoredContainer, Container } from '@cloudbeaver/core-blocks'; -import { TabPanel } from '@cloudbeaver/core-ui'; - -import { ChangePassword } from './ChangePassword/ChangePassword'; - -interface Props { - className?: string; -} - -export const AuthenticationPanel = observer(function AuthenticationPanel({ className }) { - return ( - - - - - - - - ); -}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/AuthenticationTab.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/AuthenticationTab.tsx deleted file mode 100644 index 850dc7f52e..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/AuthenticationTab.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { observer } from 'mobx-react-lite'; - -import { useTranslate } from '@cloudbeaver/core-blocks'; -import { Tab, TabTitle } from '@cloudbeaver/core-ui'; - -interface Props { - className?: string; -} - -export const AuthenticationTab = observer(function AuthenticationTab({ className }) { - const translate = useTranslate(); - - return ( - - {translate('ui_authentication')} - - ); -}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/ChangePassword/useChangePassword.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/ChangePassword/useChangePassword.ts deleted file mode 100644 index 1486d6935a..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/ChangePassword/useChangePassword.ts +++ /dev/null @@ -1,87 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { action, computed, observable } from 'mobx'; - -import { PasswordPolicyService, UsersResource } from '@cloudbeaver/core-authentication'; -import { useObservableRef } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import { NotificationService } from '@cloudbeaver/core-events'; - -interface IConfig { - oldPassword: string; - password: string; - repeatedPassword: string; -} - -interface IState { - config: IConfig; - submitting: boolean; - readonly formFilled: boolean; - changePassword: () => Promise; - resetConfig: () => void; -} - -export function useChangePassword(): IState { - const usersResource = useService(UsersResource); - const notificationService = useService(NotificationService); - const passwordPolicyService = useService(PasswordPolicyService); - - return useObservableRef( - () => ({ - config: { - oldPassword: '', - password: '', - repeatedPassword: '', - }, - submitting: false, - get formFilled() { - return this.config.password.length > 0 && this.config.oldPassword.length > 0 && this.config.repeatedPassword.length > 0; - }, - async changePassword() { - const validation = this.passwordPolicyService.validatePassword(this.config.password); - - if (!validation.isValid) { - this.notificationService.logError({ - title: 'plugin_user_profile_authentication_change_password_password_validation_error', - message: validation.errorMessage, - }); - return; - } - - if (this.config.password !== this.config.repeatedPassword) { - this.notificationService.logError({ title: 'plugin_user_profile_authentication_change_password_passwords_not_match' }); - return; - } - - try { - this.submitting = true; - await usersResource.updateLocalPassword(this.config.oldPassword, this.config.password); - notificationService.logSuccess({ title: 'plugin_user_profile_authentication_change_password_success' }); - this.resetConfig(); - } catch (exception: any) { - notificationService.logException(exception); - } finally { - this.submitting = false; - } - }, - resetConfig() { - this.config.oldPassword = ''; - this.config.password = ''; - this.config.repeatedPassword = ''; - }, - }), - { - config: observable, - submitting: observable.ref, - formFilled: computed, - changePassword: action.bound, - resetConfig: action, - }, - { usersResource, notificationService, passwordPolicyService }, - ); -} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/AuthenticationPanel.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/AuthenticationPanel.tsx new file mode 100644 index 0000000000..cbfb2c8df7 --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/AuthenticationPanel.tsx @@ -0,0 +1,31 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { ColoredContainer, Container, useAutoLoad } from '@cloudbeaver/core-blocks'; +import { TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; + +import type { UserProfileFormProps } from '../UserProfileFormService'; +import { ChangePassword } from './ChangePassword'; +import type { UserProfileFormAuthenticationPart } from './UserProfileFormAuthenticationPart'; + +export const AuthenticationPanel: TabContainerPanelComponent = observer(function AuthenticationPanel({ tabId }) { + const tab = useTab(tabId); + const tabState = useTabState(); + + useAutoLoad(AuthenticationPanel, tabState, tab.selected); + + const disabled = tabState.isLoading(); + return ( + + + + + + ); +}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/ChangePassword/ChangePassword.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/ChangePassword.tsx similarity index 62% rename from webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/ChangePassword/ChangePassword.tsx rename to webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/ChangePassword.tsx index 590fd2cbd3..5dd544344a 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/Authentication/ChangePassword/ChangePassword.tsx +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/ChangePassword.tsx @@ -7,26 +7,34 @@ */ import { observer } from 'mobx-react-lite'; -import { Button, Form, Group, GroupItem, GroupTitle, InputField, useForm, useTranslate } from '@cloudbeaver/core-blocks'; +import { Form, Group, GroupTitle, InputField, useCustomInputValidation, usePasswordValidation, useTranslate } from '@cloudbeaver/core-blocks'; +import { isValuesEqual } from '@cloudbeaver/core-utils'; -import { useChangePassword } from './useChangePassword'; +import type { IUserProfileFormAuthenticationState } from './IUserProfileFormAuthenticationState'; -export const ChangePassword = observer(function ChangePassword() { +interface Props { + state: IUserProfileFormAuthenticationState; + disabled?: boolean; +} + +export const ChangePassword = observer(function ChangePassword({ state, disabled }) { const translate = useTranslate(); - const state = useChangePassword(); - const disabled = state.submitting; - const form = useForm({ - onSubmit: state.changePassword, + const passwordValidationRef = usePasswordValidation(); + const passwordRepeatRef = useCustomInputValidation(value => { + if (!isValuesEqual(value, state.password, null)) { + return translate('authentication_user_passwords_not_match'); + } + return null; }); return ( -
+ {translate('plugin_user_profile_authentication_change_password')} value?.trim() ?? ''} small @@ -35,10 +43,11 @@ export const ChangePassword = observer(function ChangePassword() { {translate('plugin_user_profile_authentication_change_password_current_password')} value?.trim() ?? ''} small @@ -47,9 +56,10 @@ export const ChangePassword = observer(function ChangePassword() { {translate('plugin_user_profile_authentication_change_password_new_password')} value?.trim() ?? ''} small @@ -57,17 +67,6 @@ export const ChangePassword = observer(function ChangePassword() { > {translate('plugin_user_profile_authentication_change_password_repeat_password')} - - -
); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART.ts new file mode 100644 index 0000000000..4064d7b3f7 --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART.ts @@ -0,0 +1,25 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { PasswordPolicyService, UserInfoResource } from '@cloudbeaver/core-authentication'; +import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import { DATA_CONTEXT_FORM_STATE } from '@cloudbeaver/core-ui'; + +import type { UserProfileFormState } from '../UserProfileFormState'; +import { UserProfileFormAuthenticationPart } from './UserProfileFormAuthenticationPart'; + +export const DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART = createDataContext( + 'User Profile Form Info Part', + context => { + const form = context.get(DATA_CONTEXT_FORM_STATE) as UserProfileFormState; + const di = context.get(DATA_CONTEXT_DI_PROVIDER); + const userInfoResource = di.getServiceByClass(UserInfoResource); + const passwordPolicyService = di.getServiceByClass(PasswordPolicyService); + + return new UserProfileFormAuthenticationPart(form, userInfoResource, passwordPolicyService); + }, +); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/IUserProfileFormAuthenticationState.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/IUserProfileFormAuthenticationState.ts new file mode 100644 index 0000000000..2265572611 --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/IUserProfileFormAuthenticationState.ts @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { schema } from '@cloudbeaver/core-utils'; + +export const USER_PROFILE_FORM_AUTHENTICATION_PART_STATE_SCHEMA = schema + .object({ + oldPassword: schema.string(), + password: schema.string().trim(), + repeatedPassword: schema.string(), + }) + .required(); + +export type IUserProfileFormAuthenticationState = schema.infer; diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPart.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPart.ts new file mode 100644 index 0000000000..bc938e2251 --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPart.ts @@ -0,0 +1,91 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import type { PasswordPolicyService, UserInfoResource, UserResourceIncludes } from '@cloudbeaver/core-authentication'; +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import type { CachedResourceIncludeArgs } from '@cloudbeaver/core-resource'; +import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; +import { FormPart, formValidationContext, IFormState } from '@cloudbeaver/core-ui'; +import { isValuesEqual, schemaValidationError } from '@cloudbeaver/core-utils'; + +import type { IUserProfileFormState } from '../UserProfileFormService'; +import type { UserProfileFormState } from '../UserProfileFormState'; +import { type IUserProfileFormAuthenticationState, USER_PROFILE_FORM_AUTHENTICATION_PART_STATE_SCHEMA } from './IUserProfileFormAuthenticationState'; + +export class UserProfileFormAuthenticationPart extends FormPart { + private baseIncludes: CachedResourceIncludeArgs; + constructor( + formState: UserProfileFormState, + private readonly userInfoResource: UserInfoResource, + private readonly passwordPolicyService: PasswordPolicyService, + ) { + super(formState, { + oldPassword: '', + password: '', + repeatedPassword: '', + }); + this.baseIncludes = ['includeMetaParameters']; + } + + protected format(data: IFormState, contexts: IExecutionContextProvider>): void { + const parsed = USER_PROFILE_FORM_AUTHENTICATION_PART_STATE_SCHEMA.safeParse(this.state); + + this.state = parsed.success ? parsed.data : this.initialState; + } + + isOutdated(): boolean { + return this.userInfoResource.isOutdated(undefined, this.baseIncludes); + } + + isLoaded(): boolean { + return this.loaded && this.userInfoResource.isLoaded(undefined, this.baseIncludes); + } + + isChanged(): boolean { + if (!this.loaded) { + return false; + } + + return ( + !isValuesEqual(this.state.oldPassword, this.initialState.oldPassword, null) || + !isValuesEqual(this.state.password, this.initialState.password, null) || + !isValuesEqual(this.state.repeatedPassword, this.initialState.repeatedPassword, null) + ); + } + + protected validate( + data: IFormState, + contexts: IExecutionContextProvider>, + ): void | Promise { + const state = USER_PROFILE_FORM_AUTHENTICATION_PART_STATE_SCHEMA.safeParse(this.state); + const validation = contexts.getContext(formValidationContext); + + if (!state.success) { + validation.error(schemaValidationError(state.error).toString()); + return; + } + + const passwordValidation = this.passwordPolicyService.validatePassword(this.state.password); + + if (!passwordValidation.isValid) { + validation.error(passwordValidation.errorMessage); + return; + } + } + + protected async saveChanges(): Promise { + await this.userInfoResource.updateLocalPassword(this.state.oldPassword, this.state.password); + } + + protected override async loader() { + this.setInitialState({ + oldPassword: '', + password: '', + repeatedPassword: '', + }); + } +} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartBootstrap.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartBootstrap.ts new file mode 100644 index 0000000000..075b8f18db --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartBootstrap.ts @@ -0,0 +1,31 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; + +import { UserProfileFormService } from '../UserProfileFormService'; +import { DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART } from './DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART'; + +const AuthenticationPanel = importLazyComponent(() => import('./AuthenticationPanel').then(m => m.AuthenticationPanel)); + +@injectable() +export class UserProfileFormAuthenticationPartBootstrap extends Bootstrap { + constructor(private readonly userProfileFormService: UserProfileFormService) { + super(); + } + + register(): void { + this.userProfileFormService.parts.add({ + key: 'authentication', + name: 'ui_authentication', + order: 2, + panel: () => AuthenticationPanel, + stateGetter: props => () => props.formState.dataContext.get(DATA_CONTEXT_USER_PROFILE_FORM_AUTHENTICATION_PART), + }); + } +} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartService.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartService.ts new file mode 100644 index 0000000000..14e8b4f86a --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartService.ts @@ -0,0 +1,19 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; +import { injectable } from '@cloudbeaver/core-di'; + +import type { UserProfileFormProps } from '../UserProfileFormService'; + +@injectable() +export class UserProfileFormAuthenticationPartService { + placeholderContainer: PlaceholderContainer; + constructor() { + this.placeholderContainer = new PlaceholderContainer(); + } +} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserForm.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserForm.tsx deleted file mode 100644 index f46870ad0e..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserForm.tsx +++ /dev/null @@ -1,65 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { observer } from 'mobx-react-lite'; - -import { AUTH_PROVIDER_LOCAL_ID } from '@cloudbeaver/core-authentication'; -import { Button, s, SContext, StatusMessage, useS, useTranslate } from '@cloudbeaver/core-blocks'; -import { ENotificationType } from '@cloudbeaver/core-events'; -import type { UserInfo } from '@cloudbeaver/core-sdk'; -import { TabBigUnderlineStyleRegistry, TabList, TabsState } from '@cloudbeaver/core-ui'; - -import { AuthenticationPanel } from './Authentication/AuthenticationPanel'; -import { AuthenticationTab } from './Authentication/AuthenticationTab'; -import type { IUserProfileFormState } from './IUserProfileFormState'; -import userFormStyles from './UserForm.m.css'; -import { UserInfoPanel } from './UserInfo/UserInfoPanel'; -import { UserInfoTab } from './UserInfo/UserInfoTab'; - -interface Props { - user: UserInfo; - state: IUserProfileFormState; - onClose?: () => void; -} - -export const UserForm = observer(function UserForm({ user, state, onClose }) { - const translate = useTranslate(); - const moduleStyle = useS(userFormStyles); - - const localProvider = user.linkedAuthProviders.includes(AUTH_PROVIDER_LOCAL_ID); - - return ( -
- -
-
-
- -
- - - - {localProvider && } - - -
-
- {onClose && ( - - )} -
-
-
- - {localProvider && } -
-
-
- ); -}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfo/UserInfoPanel.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfo/UserInfoPanel.tsx deleted file mode 100644 index b347d0b3a1..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfo/UserInfoPanel.tsx +++ /dev/null @@ -1,71 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { observer } from 'mobx-react-lite'; - -import { UserMetaParametersResource } from '@cloudbeaver/core-authentication'; -import { - ColoredContainer, - Container, - Group, - GroupTitle, - InputField, - Loader, - ObjectPropertyInfoForm, - useResource, - useTranslate, -} from '@cloudbeaver/core-blocks'; -import type { UserInfo } from '@cloudbeaver/core-sdk'; -import { TabPanel } from '@cloudbeaver/core-ui'; - -import { AuthTokenList } from '../AuthTokens/AuthTokenList'; - -interface Props { - user: UserInfo; - className?: string; -} - -export const UserInfoPanel = observer(function UserInfoPanel({ user, className }) { - const userMetaParameters = useResource(UserInfoPanel, UserMetaParametersResource, undefined); - const translate = useTranslate(); - - return ( - - - - - {translate('plugin_user_profile_info')} - - - {translate('plugin_user_profile_info_id')} - - - {translate('plugin_user_profile_info_displayName')} - - - {translate('authentication_user_role')} - - - - {() => - userMetaParameters.data.length > 0 && ( - - - - ) - } - - - - {translate('plugin_user_profile_auth_tokens')} - - - - - - ); -}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfo/UserInfoTab.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfo/UserInfoTab.tsx deleted file mode 100644 index b2b1dc8564..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfo/UserInfoTab.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { observer } from 'mobx-react-lite'; - -import { useTranslate } from '@cloudbeaver/core-blocks'; -import { Tab, TabTitle } from '@cloudbeaver/core-ui'; - -interface Props { - className?: string; -} - -export const UserInfoTab = observer(function UserInfoTab({ className }) { - const translate = useTranslate(); - - return ( - - {translate('plugin_user_profile_info')} - - ); -}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART.ts new file mode 100644 index 0000000000..7a7b7e339b --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART.ts @@ -0,0 +1,21 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { UserInfoResource } from '@cloudbeaver/core-authentication'; +import { createDataContext, DATA_CONTEXT_DI_PROVIDER } from '@cloudbeaver/core-data-context'; +import { DATA_CONTEXT_FORM_STATE } from '@cloudbeaver/core-ui'; + +import type { UserProfileFormState } from '../UserProfileFormState'; +import { UserProfileFormInfoPart } from './UserProfileFormInfoPart'; + +export const DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART = createDataContext('User Profile Form Info Part', context => { + const form = context.get(DATA_CONTEXT_FORM_STATE) as UserProfileFormState; + const di = context.get(DATA_CONTEXT_DI_PROVIDER); + const userInfoResource = di.getServiceByClass(UserInfoResource); + + return new UserProfileFormInfoPart(form, userInfoResource); +}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/IUserProfileFormInfoState.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/IUserProfileFormInfoState.ts new file mode 100644 index 0000000000..2a377b6f1b --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/IUserProfileFormInfoState.ts @@ -0,0 +1,19 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { schema } from '@cloudbeaver/core-utils'; + +export const USER_PROFILE_FORM_INFO_PART_STATE_SCHEMA = schema + .object({ + userId: schema.string(), + displayName: schema.string().trim(), + authRole: schema.string().optional(), + metaParameters: schema.record(schema.union([schema.string().trim(), schema.any()])).optional(), + }) + .required(); + +export type IUserProfileFormInfoState = schema.infer; diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/AuthTokens/AuthTokenList.m.css b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserActiveAuthMethods/UserActiveAuthMethods.m.css similarity index 100% rename from webapp/packages/plugin-user-profile/src/UserProfileForm/AuthTokens/AuthTokenList.m.css rename to webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserActiveAuthMethods/UserActiveAuthMethods.m.css diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserActiveAuthMethods/UserActiveAuthMethods.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserActiveAuthMethods/UserActiveAuthMethods.tsx new file mode 100644 index 0000000000..1d9d94459e --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserActiveAuthMethods/UserActiveAuthMethods.tsx @@ -0,0 +1,100 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { AuthProviderConfiguration, AuthProvidersResource, UserInfoResource } from '@cloudbeaver/core-authentication'; +import { Button, Cell, Container, IconOrImage, Link, s, useResource, useS, useTranslate } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { CachedMapAllKey } from '@cloudbeaver/core-resource'; +import { AuthenticationService } from '@cloudbeaver/plugin-authentication'; + +import styles from './UserActiveAuthMethods.m.css'; + +interface Props { + className?: string; +} + +export const UserActiveAuthMethods = observer(function UserActiveAuthMethods({ className }) { + const user = useResource(UserActiveAuthMethods, UserInfoResource, undefined); + const providersResource = useResource(UserActiveAuthMethods, AuthProvidersResource, CachedMapAllKey); + const authenticationService = useService(AuthenticationService); + const style = useS(styles); + const translate = useTranslate(); + + if (!user.data) { + return null; + } + + return ( + +
+ {user.data?.authTokens.map(token => { + const provider = providersResource.resource.get(token.authProvider); + + if (!provider) { + return null; + } + + let configuration: AuthProviderConfiguration | undefined; + let name = provider.label; + let description = token.message || provider.description; + let icon = provider.icon; + + if (token.authConfiguration) { + configuration = provider.configurations?.find(configuration => configuration.id === token.authConfiguration); + + if (configuration) { + name = configuration.displayName; + description = configuration.description; + + if (configuration.iconURL) { + icon = configuration.iconURL; + } + } + } + + const date = new Date(token.loginTime); + const title = `${name}\n${description || ''}`; + + return ( + + : undefined} + after={ + + } + description={ + <> + {description} +
+ {token.userId && ( + <> + {token.userId} +
+ + )} + {date ? ( + + ) : undefined} + + } + > + {name} +
+ + ); + })} +
+
+ ); +}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfo.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfo.tsx new file mode 100644 index 0000000000..fdd32ec70e --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfo.tsx @@ -0,0 +1,54 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { ColoredContainer, Container, Group, GroupTitle, InputField, Loader, useAutoLoad, useTranslate } from '@cloudbeaver/core-blocks'; +import { TabContainerPanelComponent, useTab, useTabState } from '@cloudbeaver/core-ui'; + +import type { UserProfileFormProps } from '../UserProfileFormService'; +import { UserActiveAuthMethods } from './UserActiveAuthMethods/UserActiveAuthMethods'; +import { UserProfileFormInfoMetaParameters } from './UserProfileFormInfoMetaParameters'; +import type { UserProfileFormInfoPart } from './UserProfileFormInfoPart'; + +export const UserProfileFormInfo: TabContainerPanelComponent = observer(function UserProfileFormInfo({ tabId }) { + const translate = useTranslate(); + const tab = useTab(tabId); + const tabState = useTabState(); + + useAutoLoad(UserProfileFormInfo, tabState, tab.selected); + + const disabled = tabState.isLoading(); + + return ( + + + + {translate('plugin_user_profile_info')} + + + {translate('plugin_user_profile_info_id')} + + + {translate('plugin_user_profile_info_displayName')} + + + {translate('authentication_user_role')} + + + + + + + + {translate('plugin_user_profile_auth_tokens')} + + + + + ); +}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoMetaParameters.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoMetaParameters.tsx new file mode 100644 index 0000000000..16f3277bd1 --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoMetaParameters.tsx @@ -0,0 +1,33 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { UserMetaParametersResource } from '@cloudbeaver/core-authentication'; +import { Container, ObjectPropertyInfoForm, useResource } from '@cloudbeaver/core-blocks'; + +import type { UserProfileFormInfoPart } from './UserProfileFormInfoPart'; + +interface Props { + tabState: UserProfileFormInfoPart; + tabSelected: boolean; + disabled: boolean; +} + +export const UserProfileFormInfoMetaParameters = observer(function UserProfileFormInfoMetaParameters({ tabState, tabSelected, disabled }) { + const userMetaParameters = useResource(UserProfileFormInfoMetaParameters, UserMetaParametersResource, undefined, { active: tabSelected }); + + if (userMetaParameters.data.length === 0) { + return null; + } + + return ( + + + + ); +}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPart.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPart.ts new file mode 100644 index 0000000000..79fdc406c9 --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPart.ts @@ -0,0 +1,72 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { toJS } from 'mobx'; + +import type { UserInfoResource, UserResourceIncludes } from '@cloudbeaver/core-authentication'; +import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import type { CachedResourceIncludeArgs } from '@cloudbeaver/core-resource'; +import type { AdminUserInfoFragment } from '@cloudbeaver/core-sdk'; +import { FormPart, IFormState } from '@cloudbeaver/core-ui'; +import { isObjectsEqual, isValuesEqual } from '@cloudbeaver/core-utils'; + +import type { IUserProfileFormState } from '../UserProfileFormService'; +import type { UserProfileFormState } from '../UserProfileFormState'; +import { type IUserProfileFormInfoState, USER_PROFILE_FORM_INFO_PART_STATE_SCHEMA } from './IUserProfileFormInfoState'; + +export class UserProfileFormInfoPart extends FormPart { + private baseIncludes: CachedResourceIncludeArgs; + constructor(formState: UserProfileFormState, private readonly userInfoResource: UserInfoResource) { + super(formState, { + userId: userInfoResource.data?.userId || '', + displayName: userInfoResource.data?.displayName || '', + authRole: userInfoResource.data?.authRole || '', + metaParameters: toJS(userInfoResource.data?.metaParameters || {}), + }); + this.baseIncludes = ['includeMetaParameters']; + } + + protected format(data: IFormState, contexts: IExecutionContextProvider>): void { + this.state = USER_PROFILE_FORM_INFO_PART_STATE_SCHEMA.parse(this.state); + } + + isOutdated(): boolean { + return this.userInfoResource.isOutdated(undefined, this.baseIncludes); + } + + isLoaded(): boolean { + return this.loaded && this.userInfoResource.isLoaded(undefined, this.baseIncludes); + } + + isChanged(): boolean { + if (!this.loaded) { + return false; + } + + return ( + !isValuesEqual(this.state.userId, this.initialState.userId, null) || + !isValuesEqual(this.state.displayName, this.initialState.displayName, null) || + !isObjectsEqual(this.state.metaParameters, this.initialState.metaParameters) || + !isValuesEqual(this.state.authRole, this.initialState.authRole, '') + ); + } + + protected saveChanges(): Promise { + return Promise.resolve(); + } + + protected override async loader() { + const user = await this.userInfoResource.load(undefined, this.baseIncludes); + + this.setInitialState({ + userId: user?.userId || '', + displayName: user?.displayName || '', + authRole: user?.authRole ?? '', + metaParameters: toJS(user?.metaParameters || {}), + }); + } +} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.ts new file mode 100644 index 0000000000..c0e35a8811 --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap.ts @@ -0,0 +1,31 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; + +import { UserProfileFormService } from '../UserProfileFormService'; +import { DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART } from './DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART'; + +const UserProfileFormInfo = importLazyComponent(() => import('./UserProfileFormInfo').then(m => m.UserProfileFormInfo)); + +@injectable() +export class UserProfileFormInfoPartBootstrap extends Bootstrap { + constructor(private readonly userProfileFormService: UserProfileFormService) { + super(); + } + + register(): void { + this.userProfileFormService.parts.add({ + key: 'info', + name: 'plugin_user_profile_info', + order: 1, + panel: () => UserProfileFormInfo, + stateGetter: props => () => props.formState.dataContext.get(DATA_CONTEXT_USER_PROFILE_FORM_INFO_PART), + }); + } +} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartService.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartService.ts new file mode 100644 index 0000000000..f6450cc709 --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserInfoPart/UserProfileFormInfoPartService.ts @@ -0,0 +1,19 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { PlaceholderContainer } from '@cloudbeaver/core-blocks'; +import { injectable } from '@cloudbeaver/core-di'; + +import type { UserProfileFormProps } from '../UserProfileFormService'; + +@injectable() +export class UserProfileFormInfoPartService { + placeholderContainer: PlaceholderContainer; + constructor() { + this.placeholderContainer = new PlaceholderContainer(); + } +} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileForm.m.css b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileForm.m.css deleted file mode 100644 index e79bedb055..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileForm.m.css +++ /dev/null @@ -1,3 +0,0 @@ -.loader { - height: 100%; -} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileForm.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileForm.tsx index cf8d2875fa..a68c0ec88f 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileForm.tsx +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileForm.tsx @@ -6,32 +6,28 @@ * you may not use this file except in compliance with the License. */ import { observer } from 'mobx-react-lite'; -import { useCallback } from 'react'; -import { UserInfoResource } from '@cloudbeaver/core-authentication'; -import { Loader, s, useResource } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { BaseForm, IBaseFormSubmitInfo, IFormState } from '@cloudbeaver/core-ui'; -import { UserProfileService } from '../UserProfileService'; -import { UserForm } from './UserForm'; -import style from './UserProfileForm.m.css'; +import { IUserProfileFormState, UserProfileFormService } from './UserProfileFormService'; -export const UserProfileForm = observer(function UserProfileForm() { - const userProfileService = useService(UserProfileService); - const userInfo = useResource(UserProfileForm, UserInfoResource, { - key: undefined, - includes: ['includeMetaParameters'] as const, - }); +interface Props { + state: IFormState; +} - const close = useCallback(() => userProfileService.close(true), []); +export const UserProfileForm = observer(function UserProfileForm({ state }) { + const notificationService = useService(NotificationService); + const userProfileFormService = useService(UserProfileFormService); - if (!userProfileService.formState) { - return null; + function onSubmit({ success }: IBaseFormSubmitInfo) { + if (success) { + notificationService.logSuccess({ title: 'authentication_administration_user_updated' }); + } else { + notificationService.logError({ title: 'authentication_administration_user_update_failed' }); + } } - return ( - - {() => userInfo.data && } - - ); + return ; }); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormBootstrap.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormBootstrap.ts new file mode 100644 index 0000000000..544fae961e --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormBootstrap.ts @@ -0,0 +1,29 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { Bootstrap, injectable } from '@cloudbeaver/core-di'; + +import { UserProfileTabsService } from '../UserProfileTabsService'; + +const UserProfileFormPanel = importLazyComponent(() => import('./UserProfileFormPanel').then(module => module.UserProfileFormPanel)); + +@injectable() +export class UserProfileFormBootstrap extends Bootstrap { + constructor(private readonly userProfileTabsService: UserProfileTabsService) { + super(); + } + + register(): void { + this.userProfileTabsService.tabContainer.add({ + key: 'account', + name: 'plugin_user_profile_account_title', + order: 1, + panel: () => UserProfileFormPanel, + }); + } +} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormPanel.tsx b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormPanel.tsx new file mode 100644 index 0000000000..a77a50f0e3 --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormPanel.tsx @@ -0,0 +1,58 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; +import { useState } from 'react'; + +import { ConfirmationDialog, useExecutor } from '@cloudbeaver/core-blocks'; +import { App, useService } from '@cloudbeaver/core-di'; +import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; +import { ExecutorInterrupter } from '@cloudbeaver/core-executor'; +import { FormMode, TabContainerPanelComponent } from '@cloudbeaver/core-ui'; + +import { userProfileContext } from '../userProfileContext'; +import { UserProfileOptionsPanelService } from '../UserProfileOptionsPanelService'; +import { UserProfileForm } from './UserProfileForm'; +import { UserProfileFormService } from './UserProfileFormService'; +import { UserProfileFormState } from './UserProfileFormState'; + +export const UserProfileFormPanel: TabContainerPanelComponent = observer(function UserProfileFormPanel({ tabId }) { + const appService = useService(App); + const userProfileFormService = useService(UserProfileFormService); + const userProfileOptionsPanelService = useService(UserProfileOptionsPanelService); + const commonDialogService = useService(CommonDialogService); + + const [state] = useState(() => { + const state = new UserProfileFormState(appService, userProfileFormService, {}); + state.setMode(FormMode.Edit); + + return state; + }); + + useExecutor({ + executor: userProfileOptionsPanelService.onClose, + handlers: [ + async function closeHandler(_, contexts) { + const context = contexts.getContext(userProfileContext); + + if (state.isChanged() && !context.force) { + const result = await commonDialogService.open(ConfirmationDialog, { + title: 'connections_public_connection_edit_cancel_title', + message: 'connections_public_connection_edit_cancel_message', + confirmActionText: 'ui_processing_ok', + }); + + if (result === DialogueStateResult.Rejected) { + ExecutorInterrupter.interrupt(contexts); + } + } + }, + ], + }); + + return ; +}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormService.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormService.ts new file mode 100644 index 0000000000..e5c0c4cede --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormService.ts @@ -0,0 +1,22 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { NotificationService } from '@cloudbeaver/core-events'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import { FormBaseService, IFormProps } from '@cloudbeaver/core-ui'; + +export interface IUserProfileFormState {} + +export type UserProfileFormProps = IFormProps; + +@injectable() +export class UserProfileFormService extends FormBaseService { + constructor(localizationService: LocalizationService, notificationService: NotificationService) { + super(localizationService, notificationService, 'User profile form'); + } +} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormState.ts b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormState.ts index 51806c298a..48a22200ea 100644 --- a/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormState.ts +++ b/webapp/packages/plugin-user-profile/src/UserProfileForm/UserProfileFormState.ts @@ -5,22 +5,13 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { IFormStateInfo } from '@cloudbeaver/core-ui'; +import type { App } from '@cloudbeaver/core-di'; +import { FormState } from '@cloudbeaver/core-ui'; -import type { IUserProfileFormState } from './IUserProfileFormState'; +import type { IUserProfileFormState, UserProfileFormService } from './UserProfileFormService'; -export class UserProfileFormState implements IUserProfileFormState { - get info(): IFormStateInfo { - return { - disabled: false, - edited: false, - readonly: false, - statusMessage: null, - statusType: null, - }; +export class UserProfileFormState extends FormState { + constructor(app: App, service: UserProfileFormService, config: IUserProfileFormState) { + super(app, service, config); } - - async validate(): Promise {} - - dispose(): void {} } diff --git a/webapp/packages/plugin-user-profile/src/UserProfileOptionsPanel.m.css b/webapp/packages/plugin-user-profile/src/UserProfileOptionsPanel.m.css new file mode 100644 index 0000000000..f0bd7d9317 --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileOptionsPanel.m.css @@ -0,0 +1,11 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.userProfileOptionsPanel { + height: 100%; +} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileOptionsPanel.tsx b/webapp/packages/plugin-user-profile/src/UserProfileOptionsPanel.tsx new file mode 100644 index 0000000000..5ac346de2e --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileOptionsPanel.tsx @@ -0,0 +1,37 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { observer } from 'mobx-react-lite'; + +import { ColoredContainer, Container, Group, SContext, StyleRegistry, useS } from '@cloudbeaver/core-blocks'; +import { useService } from '@cloudbeaver/core-di'; +import { TabList, TabPanelList, TabsState, TabStyles, TabUnderlineStyleRegistry } from '@cloudbeaver/core-ui'; + +import style from './UserProfileOptionsPanel.m.css'; +import UserProfileTabStyles from './UserProfileTab.m.css'; +import { UserProfileTabsService } from './UserProfileTabsService'; + +export const tabsStyleRegistry: StyleRegistry = [...TabUnderlineStyleRegistry, [TabStyles, { mode: 'append', styles: [UserProfileTabStyles] }]]; + +export const UserProfileOptionsPanel = observer(function UserProfileOptionsPanel() { + const styles = useS(style); + const userProfileTabsService = useService(UserProfileTabsService); + return ( + + + + + + + + + ); +}); diff --git a/webapp/packages/plugin-user-profile/src/UserProfileOptionsPanelService.ts b/webapp/packages/plugin-user-profile/src/UserProfileOptionsPanelService.ts new file mode 100644 index 0000000000..209e21278a --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileOptionsPanelService.ts @@ -0,0 +1,80 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { action, makeObservable } from 'mobx'; + +import { UserInfoResource } from '@cloudbeaver/core-authentication'; +import { importLazyComponent } from '@cloudbeaver/core-blocks'; +import { injectable } from '@cloudbeaver/core-di'; +import { ExecutionContext, Executor, IExecutor, ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor'; +import { OptionsPanelService } from '@cloudbeaver/core-ui'; + +import { userProfileContext } from './userProfileContext'; + +const UserProfileOptionsPanel = importLazyComponent(() => import('./UserProfileOptionsPanel').then(m => m.UserProfileOptionsPanel)); +const panelGetter = () => UserProfileOptionsPanel; + +@injectable() +export class UserProfileOptionsPanelService { + readonly onOpen: ISyncExecutor; + readonly onClose: IExecutor; + + constructor( + private readonly optionsPanelService: OptionsPanelService, + private readonly userInfoResource: UserInfoResource, + ) { + this.onOpen = new SyncExecutor(); + this.onClose = new Executor(); + + this.optionsPanelService.closeTask.next(this.onClose, undefined, () => this.optionsPanelService.isOpen(panelGetter)); + this.userInfoResource.onDataUpdate.addHandler(this.userUpdateHandler.bind(this)); + + makeObservable(this, { + open: action, + close: action, + }); + } + + async open(): Promise { + if (this.optionsPanelService.isOpen(panelGetter)) { + return true; + } + + const state = await this.optionsPanelService.open(panelGetter); + + if (state) { + this.onOpen.execute(); + } + + return state; + } + + async close(force?: boolean): Promise { + if (!this.optionsPanelService.isOpen(panelGetter)) { + return; + } + + const context = new ExecutionContext(undefined); + + if (force) { + const userProfile = context.getContext(userProfileContext); + userProfile.setForce(force); + } + + await this.optionsPanelService.close(context); + } + + private userUpdateHandler() { + if (!this.optionsPanelService.isOpen(panelGetter)) { + return; + } + + if (this.userInfoResource.data === null) { + this.close(true); + } + } +} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileService.ts b/webapp/packages/plugin-user-profile/src/UserProfileService.ts deleted file mode 100644 index c6b68f3903..0000000000 --- a/webapp/packages/plugin-user-profile/src/UserProfileService.ts +++ /dev/null @@ -1,110 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { action, makeObservable, observable } from 'mobx'; - -import { UserInfoResource } from '@cloudbeaver/core-authentication'; -import { ConfirmationDialog } from '@cloudbeaver/core-blocks'; -import { injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs'; -import { ExecutorInterrupter, IExecutorHandler } from '@cloudbeaver/core-executor'; -import { OptionsPanelService } from '@cloudbeaver/core-ui'; - -import type { IUserProfileFormState } from './UserProfileForm/IUserProfileFormState'; -import { UserProfileForm } from './UserProfileForm/UserProfileForm'; -import { UserProfileFormState } from './UserProfileForm/UserProfileFormState'; - -const formGetter = () => UserProfileForm; - -@injectable() -export class UserProfileService { - formState: IUserProfileFormState | null; - - constructor( - private readonly commonDialogService: CommonDialogService, - private readonly optionsPanelService: OptionsPanelService, - private readonly userInfoResource: UserInfoResource, - ) { - this.formState = null; - this.optionsPanelService.closeTask.addHandler(this.closeHandler); - this.userInfoResource.onDataUpdate.addHandler(this.userUpdateHandler.bind(this)); - - makeObservable(this, { - formState: observable.ref, - change: action, - open: action, - close: action, - }); - } - - change(): void { - if (!this.formState) { - this.formState = new UserProfileFormState(); - } - } - - async open(): Promise { - const state = await this.optionsPanelService.open(formGetter); - - if (state) { - this.change(); - } - - return state; - } - - async close(saved?: boolean): Promise { - if (saved) { - this.clearFormState(); - } - - const state = await this.optionsPanelService.close(); - - if (state) { - this.clearFormState(); - } - } - - save(): void { - this.close(true); - } - - private readonly closeHandler: IExecutorHandler = async (data, contexts) => { - if (!this.formState || !this.optionsPanelService.isOpen(formGetter)) { - return; - } - - if (!this.formState.info.edited) { - return; - } - - const result = await this.commonDialogService.open(ConfirmationDialog, { - title: 'connections_public_connection_edit_cancel_title', - message: 'connections_public_connection_edit_cancel_message', - confirmActionText: 'ui_processing_ok', - }); - - if (result === DialogueStateResult.Rejected) { - ExecutorInterrupter.interrupt(contexts); - } - }; - - private userUpdateHandler() { - if (!this.formState || !this.optionsPanelService.isOpen(formGetter)) { - return; - } - - if (this.userInfoResource.data === null) { - this.save(); - } - } - - private clearFormState() { - this.formState?.dispose(); - this.formState = null; - } -} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileTab.m.css b/webapp/packages/plugin-user-profile/src/UserProfileTab.m.css new file mode 100644 index 0000000000..d14d1b6218 --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileTab.m.css @@ -0,0 +1,15 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +.tabInner { + height: 100%; +} + +.tabOuter { + height: 100%; +} diff --git a/webapp/packages/plugin-user-profile/src/UserProfileTabsService.ts b/webapp/packages/plugin-user-profile/src/UserProfileTabsService.ts new file mode 100644 index 0000000000..d1ade670e3 --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/UserProfileTabsService.ts @@ -0,0 +1,18 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { injectable } from '@cloudbeaver/core-di'; +import { TabsContainer } from '@cloudbeaver/core-ui'; + +@injectable() +export class UserProfileTabsService { + readonly tabContainer: TabsContainer; + + constructor() { + this.tabContainer = new TabsContainer('User Profile'); + } +} diff --git a/webapp/packages/plugin-user-profile/src/index.ts b/webapp/packages/plugin-user-profile/src/index.ts index f663c1d2ae..66b8729768 100644 --- a/webapp/packages/plugin-user-profile/src/index.ts +++ b/webapp/packages/plugin-user-profile/src/index.ts @@ -2,6 +2,6 @@ import { userProfilePlugin } from './manifest'; export default userProfilePlugin; +export * from './UserProfileTabsService'; export * from './UserMenu/MENU_USER_PROFILE'; -export * from './UserMenu/UserMenu'; -export * from './UserProfileForm/AuthProviders/AuthProvidersList'; +export * from './UserMenu/UserMenuLazy'; diff --git a/webapp/packages/plugin-user-profile/src/locales/en.ts b/webapp/packages/plugin-user-profile/src/locales/en.ts index c0a5a26775..11c4b6918f 100644 --- a/webapp/packages/plugin-user-profile/src/locales/en.ts +++ b/webapp/packages/plugin-user-profile/src/locales/en.ts @@ -1,4 +1,5 @@ export default [ + ['plugin_user_profile_account_title', 'Account'], ['plugin_user_profile_menu', 'Profile'], ['plugin_user_profile_info', 'Info'], ['plugin_user_profile_info_id', 'UserId'], diff --git a/webapp/packages/plugin-user-profile/src/locales/it.ts b/webapp/packages/plugin-user-profile/src/locales/it.ts index 2c796e7aeb..ae5f701748 100644 --- a/webapp/packages/plugin-user-profile/src/locales/it.ts +++ b/webapp/packages/plugin-user-profile/src/locales/it.ts @@ -1,4 +1,5 @@ export default [ + ['plugin_user_profile_account_title', 'Account'], ['plugin_user_profile_menu', 'Profile'], ['plugin_user_profile_info', 'Info'], ['plugin_user_profile_info_id', 'UserId'], diff --git a/webapp/packages/plugin-user-profile/src/locales/ru.ts b/webapp/packages/plugin-user-profile/src/locales/ru.ts index ac5bc52bc8..3fa4fd3228 100644 --- a/webapp/packages/plugin-user-profile/src/locales/ru.ts +++ b/webapp/packages/plugin-user-profile/src/locales/ru.ts @@ -1,4 +1,5 @@ export default [ + ['plugin_user_profile_account_title', 'Аккаунт'], ['plugin_user_profile_menu', 'Профиль'], ['plugin_user_profile_info', 'Информация'], ['plugin_user_profile_info_id', 'UserId'], diff --git a/webapp/packages/plugin-user-profile/src/locales/zh.ts b/webapp/packages/plugin-user-profile/src/locales/zh.ts index f1abcfe504..cb8b7ee94a 100644 --- a/webapp/packages/plugin-user-profile/src/locales/zh.ts +++ b/webapp/packages/plugin-user-profile/src/locales/zh.ts @@ -1,4 +1,5 @@ export default [ + ['plugin_user_profile_account_title', 'Account'], ['plugin_user_profile_menu', '简介'], ['plugin_user_profile_info', '信息'], ['plugin_user_profile_info_id', '用户ID'], diff --git a/webapp/packages/plugin-user-profile/src/manifest.ts b/webapp/packages/plugin-user-profile/src/manifest.ts index 8a12d11a50..e21efa5f0f 100644 --- a/webapp/packages/plugin-user-profile/src/manifest.ts +++ b/webapp/packages/plugin-user-profile/src/manifest.ts @@ -9,12 +9,30 @@ import type { PluginManifest } from '@cloudbeaver/core-di'; import { LocaleService } from './LocaleService'; import { PluginBootstrap } from './PluginBootstrap'; -import { UserProfileService } from './UserProfileService'; +import { UserProfileFormAuthenticationPartBootstrap } from './UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartBootstrap'; +import { UserProfileFormAuthenticationPartService } from './UserProfileForm/UserAuthenticationPart/UserProfileFormAuthenticationPartService'; +import { UserProfileFormInfoPartBootstrap } from './UserProfileForm/UserInfoPart/UserProfileFormInfoPartBootstrap'; +import { UserProfileFormInfoPartService } from './UserProfileForm/UserInfoPart/UserProfileFormInfoPartService'; +import { UserProfileFormBootstrap } from './UserProfileForm/UserProfileFormBootstrap'; +import { UserProfileFormService } from './UserProfileForm/UserProfileFormService'; +import { UserProfileOptionsPanelService } from './UserProfileOptionsPanelService'; +import { UserProfileTabsService } from './UserProfileTabsService'; export const userProfilePlugin: PluginManifest = { info: { name: 'User profile plugin', }, - providers: [PluginBootstrap, LocaleService, UserProfileService], + providers: [ + PluginBootstrap, + LocaleService, + UserProfileTabsService, + UserProfileOptionsPanelService, + UserProfileFormBootstrap, + UserProfileFormService, + UserProfileFormInfoPartBootstrap, + UserProfileFormInfoPartService, + UserProfileFormAuthenticationPartBootstrap, + UserProfileFormAuthenticationPartService, + ], }; diff --git a/webapp/packages/plugin-user-profile/src/userProfileContext.ts b/webapp/packages/plugin-user-profile/src/userProfileContext.ts new file mode 100644 index 0000000000..b722e2c87c --- /dev/null +++ b/webapp/packages/plugin-user-profile/src/userProfileContext.ts @@ -0,0 +1,21 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +interface IUserProfileContext { + force: boolean; + setForce(force: boolean): void; +} + +export function userProfileContext(): IUserProfileContext { + return { + force: false, + setForce(force: boolean) { + this.force = force; + }, + }; +} diff --git a/webapp/packages/plugin-user-profile/tsconfig.json b/webapp/packages/plugin-user-profile/tsconfig.json index eb0d4fd83b..818a8d3895 100644 --- a/webapp/packages/plugin-user-profile/tsconfig.json +++ b/webapp/packages/plugin-user-profile/tsconfig.json @@ -12,6 +12,9 @@ { "path": "../core-blocks/tsconfig.json" }, + { + "path": "../core-data-context/tsconfig.json" + }, { "path": "../core-di/tsconfig.json" }, @@ -36,6 +39,9 @@ { "path": "../core-ui/tsconfig.json" }, + { + "path": "../core-utils/tsconfig.json" + }, { "path": "../core-view/tsconfig.json" }, diff --git a/webapp/packages/product-default/package.json b/webapp/packages/product-default/package.json index d3ab34b6bf..7d386f3798 100644 --- a/webapp/packages/product-default/package.json +++ b/webapp/packages/product-default/package.json @@ -29,7 +29,6 @@ "@cloudbeaver/core-bootstrap": "~0.1.0", "@cloudbeaver/core-di": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", - "@cloudbeaver/core-product": "~0.1.0", "@cloudbeaver/core-task-manager": "~0.1.0", "@cloudbeaver/core-version-update": "~0.1.0", "@cloudbeaver/plugin-administration": "~0.1.0", @@ -83,6 +82,7 @@ "@cloudbeaver/plugin-top-app-bar-administration": "~0.1.0", "@cloudbeaver/plugin-user-profile": "~0.1.0", "@cloudbeaver/plugin-user-profile-administration": "~0.1.0", + "@cloudbeaver/plugin-user-profile-settings": "~0.1.0", "@cloudbeaver/plugin-version": "~0.1.0", "@cloudbeaver/plugin-version-update-administration": "~0.1.0", "react": "^18.2.0", diff --git a/webapp/packages/product-default/src/ProductBootstrap.ts b/webapp/packages/product-default/src/ProductBootstrap.ts index bfacbc4c0c..f853d9eb11 100644 --- a/webapp/packages/product-default/src/ProductBootstrap.ts +++ b/webapp/packages/product-default/src/ProductBootstrap.ts @@ -5,10 +5,11 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ +import { importLazyComponent } from '@cloudbeaver/core-blocks'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { VersionUpdateService } from '@cloudbeaver/core-version-update'; -import { UpdateInstruction } from './UpdateInstruction'; +const UpdateInstruction = importLazyComponent(() => import('./UpdateInstruction').then(m => m.UpdateInstruction)); @injectable() export class ProductBootstrap extends Bootstrap { @@ -16,8 +17,6 @@ export class ProductBootstrap extends Bootstrap { super(); } - async load() {} - register() { this.versionUpdateService.registerVersionInstruction(() => UpdateInstruction); } diff --git a/webapp/packages/product-default/src/ProductConfigService.ts b/webapp/packages/product-default/src/ProductConfigService.ts deleted file mode 100644 index 0ddeaccf83..0000000000 --- a/webapp/packages/product-default/src/ProductConfigService.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * CloudBeaver - Cloud Database Manager - * Copyright (C) 2020-2024 DBeaver Corp and others - * - * Licensed under the Apache License, Version 2.0. - * you may not use this file except in compliance with the License. - */ -import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { ProductManagerService } from '@cloudbeaver/core-product'; - -import productConfig from './config.json5'; - -@injectable() -export class ProductConfigService extends Bootstrap { - constructor(private readonly productManager: ProductManagerService) { - super(); - } - - register(): void | Promise { - this.productManager.setSettings(productConfig); - } - - load(): void | Promise {} -} diff --git a/webapp/packages/product-default/src/config.json5 b/webapp/packages/product-default/src/config.json5 deleted file mode 100644 index 42d89e7a16..0000000000 --- a/webapp/packages/product-default/src/config.json5 +++ /dev/null @@ -1,37 +0,0 @@ -// Product configuration. Customized web application behavior -// It is in JSON5 format -{ - // Global properties - core: { - // User defaults - user: { - defaultTheme: 'light', - defaultLanguage: 'en', - }, - app: { - // Log viewer config - logViewer: { - refreshTimeout: 3000, - logBatchSize: 1000, - maxLogRecords: 2000, - maxFailedRequests: 3, - }, - }, - }, - plugin: { - 'sql-editor': { - // max size of the file that can be uploaded to the editor (in kilobytes) - maxFileSize: 10240, - }, - }, - plugin_data_spreadsheet_new: { - hidden: false, - }, - plugin_data_export: { - disabled: false, - }, - // Notifications config - core_events: { - notificationsPool: 20, - }, -} diff --git a/webapp/packages/product-default/src/index.ts b/webapp/packages/product-default/src/index.ts index b4b13d7e6b..2fe35f24ab 100644 --- a/webapp/packages/product-default/src/index.ts +++ b/webapp/packages/product-default/src/index.ts @@ -56,6 +56,7 @@ import userProfile from '@cloudbeaver/plugin-user-profile'; import userProfileAdministration from '@cloudbeaver/plugin-user-profile-administration'; import version from '@cloudbeaver/plugin-version'; import versionUpdate from '@cloudbeaver/plugin-version-update-administration'; +import { userProfileSettingsPlugin } from '@cloudbeaver/plugin-user-profile-settings'; import { defaultProductManifest } from './manifest'; @@ -114,6 +115,7 @@ const PLUGINS: PluginManifest[] = [ coreTaskManagerManifest, taskManagerPluginManifest, settingsAdministrationPlugin, + userProfileSettingsPlugin, // must be las one to override all defaultProductManifest, ]; diff --git a/webapp/packages/product-default/src/manifest.ts b/webapp/packages/product-default/src/manifest.ts index 500480c451..ce8aecb861 100644 --- a/webapp/packages/product-default/src/manifest.ts +++ b/webapp/packages/product-default/src/manifest.ts @@ -9,12 +9,11 @@ import type { PluginManifest } from '@cloudbeaver/core-di'; import { LocaleService } from './LocaleService'; import { ProductBootstrap } from './ProductBootstrap'; -import { ProductConfigService } from './ProductConfigService'; export const defaultProductManifest: PluginManifest = { info: { name: 'Default Product', }, - providers: [ProductBootstrap, ProductConfigService, LocaleService], + providers: [ProductBootstrap, LocaleService], }; diff --git a/webapp/packages/product-default/tsconfig.json b/webapp/packages/product-default/tsconfig.json index 0f5134c4b2..b5eb58afc1 100644 --- a/webapp/packages/product-default/tsconfig.json +++ b/webapp/packages/product-default/tsconfig.json @@ -18,9 +18,6 @@ { "path": "../core-localization/tsconfig.json" }, - { - "path": "../core-product/tsconfig.json" - }, { "path": "../core-task-manager/tsconfig.json" }, @@ -177,6 +174,9 @@ { "path": "../plugin-user-profile-administration/tsconfig.json" }, + { + "path": "../plugin-user-profile-settings/tsconfig.json" + }, { "path": "../plugin-user-profile/tsconfig.json" }, diff --git a/webapp/packages/tests-runner/package.json b/webapp/packages/tests-runner/package.json index 26ed76dd65..5bb159d320 100644 --- a/webapp/packages/tests-runner/package.json +++ b/webapp/packages/tests-runner/package.json @@ -25,6 +25,7 @@ "public/**/*" ], "devDependencies": { + "@types/jest": "^29.5.12", "@types/react": "^18.2.63", "typescript": "^5.3.3" } diff --git a/webapp/packages/tests-runner/src/__custom_mocks__/mockKnownConsoleMessages.ts b/webapp/packages/tests-runner/src/__custom_mocks__/mockKnownConsoleMessages.ts new file mode 100644 index 0000000000..b93d9ca5b5 --- /dev/null +++ b/webapp/packages/tests-runner/src/__custom_mocks__/mockKnownConsoleMessages.ts @@ -0,0 +1,134 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +let knownLogPatterns: any[][] = []; +let knownWarnPatterns: any[][] = []; +let knownErrorPatterns: any[][] = []; +let knownInfoPatterns: any[][] = []; +let knownDebugPatterns: any[][] = []; + +let logSpy: jest.Spied; +let warnSpy: jest.Spied; +let errorSpy: jest.Spied; +let infoSpy: jest.Spied; +let debugSpy: jest.Spied; + +beforeAll(() => { + logSpy = jest.spyOn(console, 'log').mockImplementation((...args) => { + if (filterPatterns(knownLogPatterns, args)) { + return; + } + expect(logSpy).not.toHaveBeenCalledWith(...args); + }); + warnSpy = jest.spyOn(console, 'warn').mockImplementation((...args) => { + if (filterPatterns(knownWarnPatterns, args)) { + return; + } + expect(warnSpy).not.toHaveBeenCalledWith(...args); + }); + errorSpy = jest.spyOn(console, 'error').mockImplementation((...args) => { + if (args[0] && String(args[0]).includes('Expected:')) { + // skip exceptions from jest expect + return; + } + if (args[0] && String(args[0]).includes('Error: Uncaught')) { + // skip uncaught exceptions + return; + } + if (filterPatterns(knownErrorPatterns, args)) { + return; + } + expect(errorSpy).not.toHaveBeenCalledWith(...args); + }); + infoSpy = jest.spyOn(console, 'info').mockImplementation((...args) => { + if (filterPatterns(knownInfoPatterns, args)) { + return; + } + expect(infoSpy).not.toHaveBeenCalledWith(...args); + }); + debugSpy = jest.spyOn(console, 'debug').mockImplementation((...args) => { + if (filterPatterns(knownDebugPatterns, args)) { + return; + } + expect(debugSpy).not.toHaveBeenCalledWith(...args); + }); +}); + +beforeEach(() => { + logSpy.mockClear(); + warnSpy.mockClear(); + errorSpy.mockClear(); + infoSpy.mockClear(); + debugSpy.mockClear(); +}); + +afterAll(() => { + logSpy.mockRestore(); + warnSpy.mockRestore(); + errorSpy.mockRestore(); + infoSpy.mockRestore(); + debugSpy.mockRestore(); + + knownLogPatterns = []; + knownWarnPatterns = []; + knownErrorPatterns = []; + knownInfoPatterns = []; + knownDebugPatterns = []; +}); + +function filterPatterns(patterns: any[][], args: any[]) { + return patterns.some(pattern => + pattern.every((value, index) => { + if (value instanceof RegExp) { + return value.test(args[index]); + } + if (typeof value === 'function') { + return value(args[index]); + } + return value === args[index]; + }), + ); +} + +export const consoleSpy = { + get log() { + return logSpy; + }, + get warn() { + return warnSpy; + }, + get error() { + return errorSpy; + }, + get info() { + return infoSpy; + }, + get debug() { + return debugSpy; + }, +}; + +export function addKnownLog(...patterns: any[]) { + knownLogPatterns.push(patterns); +} + +export function addKnownWarn(...patterns: any[]) { + knownWarnPatterns.push(patterns); +} + +export function addKnownError(...patterns: any[]) { + knownErrorPatterns.push(patterns); +} + +export function addKnownInfo(...patterns: any[]) { + knownInfoPatterns.push(patterns); +} + +export function addKnownDebug(...patterns: any[]) { + knownDebugPatterns.push(patterns); +} diff --git a/webapp/packages/tests-runner/src/createApp.ts b/webapp/packages/tests-runner/src/createApp.ts index c19f70ad2b..3a61c6e78f 100644 --- a/webapp/packages/tests-runner/src/createApp.ts +++ b/webapp/packages/tests-runner/src/createApp.ts @@ -9,10 +9,13 @@ import { configure } from 'mobx'; import { App, IServiceInjector, PluginManifest } from '@cloudbeaver/core-di'; +import './__custom_mocks__/mockKnownConsoleMessages'; + export interface IApplication { app: App; injector: IServiceInjector; init(): Promise; + dispose(): void; } export function createApp(...plugins: PluginManifest[]): IApplication { @@ -23,17 +26,21 @@ export function createApp(...plugins: PluginManifest[]): IApplication { const app = new App(plugins); const injector = app.getServiceInjector(); - //@ts-expect-error - app.registerServices(); + beforeAll(async () => { + await app.start(); + }); + afterAll(() => { + app.dispose(); + }); return { app, injector, async init() { - //@ts-expect-error - await app.initializeServices(); - //@ts-expect-error - await app.loadServices(); + await app.start(); + }, + dispose() { + app.dispose(); }, }; } diff --git a/webapp/packages/tests-runner/src/index.ts b/webapp/packages/tests-runner/src/index.ts index a0b7463e18..dbdd413c04 100644 --- a/webapp/packages/tests-runner/src/index.ts +++ b/webapp/packages/tests-runner/src/index.ts @@ -1,3 +1,4 @@ export * from './createApp'; export * from './getService'; export * from './renderInApp'; +export * from './__custom_mocks__/mockKnownConsoleMessages';