CB-4619 user scope settings (#2471)

* CB-4619 feat: update gql schema

* CB-4808 bulk update user parameters

* CB-4808 param type fix

* CB-4619 feat: updateUserPreferences API


* CB-4619 refactor: settings api

* CB-4619 refactor: move file

* CB-4619 refactor: settings api

* CB-4619 fix: pr review

* CB-4619 refactor: remove web-based product settings

* CB-4619 fix: after pr review

* CB-4619 refactor: move product conf to cloudbeaver conf

* CB-4619 fix: hide settings bar and log exception with changing session language

* CB-4619 fix: revert after merge

* CB-4808 new settings api

* CB-4808 update product.conf format

* CB-4808 update product.conf format

* CB-4619 feat: support server provided settings

* CB-4619 feat: prettify settings

* CB-4619 chore: map sql editor settings group

* CB-4808 add use short/long object name settings to CloudBeaver

* CB-4808 update default product.conf

* CB-4808 mode short/long settings to SQLModelPreferences

* CB-4808 review fix

* CB-4619 refactor: unit tests

* CB-4619 fix: prevent websocket opening spam

* CB-4619 fix: dependencies

* CB-4619 fix: remove extra theme loading

* CB-4619 fix: rename settings alias resolver file

* CB-4619 fix: after review

* CB-4619 fix conflicts

* CB-4619 rename server property

* CB-4619 remove shortname property

* CB-4619 fix legacy product conf reading

* CB-4619 merge root and runtime configurations

* CB-4619 chore: remove log viewer setting

* CB-4619 integer fields with env variables fix

* CB-4619 fix: property naming

* CB-4619 fix: parseJSONFlat null values

* CB-4619 fix: settings loading

* CB-4619 fix: save only actual settings

---------

Co-authored-by: Aleksandr Skoblikov <aleksandr.skoblikov@dbeaver.com>
Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com>
Co-authored-by: Evgenia Bezborodova <139753579+EvgeniaBzzz@users.noreply.github.com>
This commit is contained in:
Alexey
2024-03-28 10:30:52 +00:00
committed by GitHub
co-authored by Aleksandr Skoblikov kseniaguzeeva Evgenia Bezborodova
parent b54dd5380d
commit eaa26dfe52
380 changed files with 5094 additions and 3051 deletions
+6 -9
View File
@@ -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_internals>/**",
"**/node_modules/**",
"${workspaceFolder}/webapp/**/node_modules/**/*.js",
"${workspaceFolder}/webapp/**/dist/**/*.js"
]
"disableNetworkCache": true,
"skipFiles": ["<node_internals>/**", "**/node_modules/**"]
},
{
"type": "java",
-32
View File
@@ -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
}
@@ -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,
@@ -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,
@@ -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<String> getScopes() {
if (property instanceof ProductSettingDescriptor productSettingDescriptor) {
return productSettingDescriptor.getScopes();
}
return null;
}
}
@@ -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!
@@ -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<PropertyGroupDescriptor<ProductSettingDescriptor>> groups;
public WebProductSettings(
@NotNull WebSession webSession,
@NotNull List<PropertyGroupDescriptor<ProductSettingDescriptor>> groups
) {
this.webSession = webSession;
this.groups = groups;
}
@NotNull
@Property
public List<WebSettingsGroupInfo> getGroups() {
return groups.stream()
.map(WebSettingsGroupInfo::new)
.collect(Collectors.toList());
}
@NotNull
@Property
public List<WebPropertyInfo> getSettings() {
return groups.stream()
.flatMap(group -> group.getSettings().stream())
.map(setting -> new WebPropertyInfo(webSession, setting))
.collect(Collectors.toList());
}
}
@@ -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<ProductSettingDescriptor> groupDescriptor;
public WebSettingsGroupInfo(@NotNull PropertyGroupDescriptor<ProductSettingDescriptor> groupDescriptor) {
this.groupDescriptor = groupDescriptor;
}
@NotNull
@Property
public String getId() {
return groupDescriptor.getFullId();
}
@NotNull
@Property
public String getDisplayName() {
return groupDescriptor.getDisplayName();
}
}
@@ -728,6 +728,7 @@ public abstract class CBApplication<T extends CBServerConfig> extends BaseWebApp
public void saveProductConfiguration(SMCredentialsProvider credentialsProvider, Map<String, Object> productConfiguration) throws DBException {
getServerConfigurationController().saveProductConfiguration(productConfiguration);
flushConfiguration(credentialsProvider);
sendConfigChangedEvent(credentialsProvider);
}
@@ -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";
@@ -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<String, Object> productSettings = new HashMap<>();
@SerializedName("database")
private WebDatabaseConfig databaseConfiguration = new WebDatabaseConfig();
@@ -184,4 +187,8 @@ public class CBServerConfig implements WebServerConfiguration {
return enableSecurityManager;
}
@NotNull
public Map<String, Object> getProductSettings() {
return productSettings;
}
}
@@ -50,12 +50,12 @@ import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public abstract class CBServerConfigurationController<T extends CBServerConfig> extends BaseServerConfigurationController<T> {
public abstract class CBServerConfigurationController<T extends CBServerConfig>
extends BaseServerConfigurationController<T> {
private static final Log log = Log.getLog(CBServerConfigurationController.class);
// Configurations
protected final Map<String, Object> 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<T extends CBServerConfig>
// Merge new config with old one
mergeOldConfiguration(prevConfig);
patchConfigurationWithProperties(productConfiguration);
patchConfigurationWithProperties(getServerConfiguration().getProductSettings());
}
protected void parseConfiguration(Map<String, Object> configProps) throws DBException {
@@ -123,8 +123,11 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
patchConfigurationWithProperties(configProps); // patch again because properties can be changed
Gson gson = getGson();
Map<String, Object> 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<T extends CBServerConfig>
//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<String, Object> 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<T extends CBServerConfig>
protected void readProductConfiguration(Map<String, Object> 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<T extends CBServerConfig>
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<String, Object> 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<String, Object> 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<T extends CBServerConfig>
writeRuntimeConfig(getRuntimeAppConfigPath(), configurationProperties);
}
private void writeRuntimeConfig(Path runtimeConfigPath, Map<String, Object> configurationProperties) throws DBException {
private void writeRuntimeConfig(Path runtimeConfigPath, Map<String, Object> 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<T extends CBServerConfig>
Map<String, Object> 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<T extends CBServerConfig>
CBConstants.PARAM_SESSION_EXPIRE_PERIOD,
sessionExpireTime);
}
var productConfigProperties = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
Map<String, Object> oldProductRuntimeConfig = JSONUtils.getObject(originServerConfig,
CBConstants.PARAM_PRODUCT_SETTINGS);
if (!CommonUtils.isEmpty(getServerConfiguration().getProductSettings())) {
for (Map.Entry<String, Object> 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<T extends CBServerConfig>
}
public void saveProductConfiguration(Map<String, Object> productConfiguration) throws DBException {
Map<String, Object> mergedConfig = WebAppUtils.mergeConfigurations(this.productConfiguration, productConfiguration);
writeRuntimeConfig(getRuntimeProductConfigFilePath(), mergedConfig);
this.productConfiguration.clear();
this.productConfiguration.putAll(WebAppUtils.flattenMap(mergedConfig));
Map<String, Object> productSettings = getServerConfiguration().getProductSettings();
Map<String, Object> mergedConfig = WebAppUtils.mergeConfigurations(productSettings, productConfiguration);
productSettings.clear();
productSettings.putAll(WebAppUtils.flattenMap(mergedConfig));
}
public T getServerConfiguration() {
@@ -572,7 +594,7 @@ public abstract class CBServerConfigurationController<T extends CBServerConfig>
}
public Map<String, Object> getProductConfiguration() {
return productConfiguration;
return getServerConfiguration().getProductSettings();
}
public SMControllerConfiguration getSecurityManagerConfiguration() {
@@ -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<T extends CBServerConfig> e
super(serverConfig);
}
@Override
protected void readProductConfiguration(Map<String, Object> 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<String, Object> collectServerConfigProperties(
@@ -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<WebDatabaseDriverInfo> getDriverList(@NotNull WebSession webSession, String driverId) throws DBWebException;
@@ -53,6 +53,7 @@ public class WebServiceBindingCore extends WebServiceBindingBase<DBWServiceCore>
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)))
@@ -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<WebServerMessage> readSessionLog(@NotNull WebSession webSession, Integer maxEntries, Boolean clearEntries) {
public List<WebServerMessage> 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) {
@@ -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
@@ -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")
}
@@ -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<String, Object> parameters
) throws DBWebException;
}
@@ -73,6 +73,9 @@ public class WebServiceBindingAuth extends WebServiceBindingBase<DBWServiceAuth>
env -> getService(env).setUserConfigurationParameter(getWebSession(env),
env.getArgument("name"),
env.getArgument("value")))
.dataFetcher("setUserPreferences",
env -> getService(env).setUserConfigurationParameters(getWebSession(env),
env.getArgument("preferences")))
;
}
}
@@ -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<String, Object> parameters
) throws DBWebException {
if (webSession.getUser() == null) {
throw new DBWebException("Preferences cannot be changed for anonymous user");
}
for (Map.Entry<String, Object> parameter : parameters.entrySet()) {
setPreference(webSession, parameter.getKey(), parameter.getValue());
}
return new WebUserInfo(webSession, webSession.getUser());
}
}
@@ -10,7 +10,7 @@
rootURI: "/",
serviceURI: "/api/",
productConfiguration: "workspace/conf/product.conf",
productSettings: {},
expireSessionAfterPeriod: 1800000,
@@ -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
}
}
@@ -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"
},
@@ -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);
});
@@ -34,7 +34,7 @@
"path": "../core-sdk/tsconfig.json"
},
{
"path": "../core-settings/tsconfig.json"
"path": "../core-storage/tsconfig.json"
},
{
"path": "../core-utils/tsconfig.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",
@@ -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');
@@ -37,10 +37,6 @@ export class AuthInfoService {
);
}
async logout(): Promise<void> {
await this.userInfoResource.logout();
}
private federatedAuthentication(
providerId: string,
options: ILoginOptions,
@@ -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();
});
@@ -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<typeof settingsSchema>;
@injectable()
export class AuthSettingsService {
readonly settings: PluginSettings<typeof settingsSchema>;
get disableAnonymousAccess(): boolean {
return this.settings.getValue('core.authentication.disableAnonymousAccess');
}
readonly settings: SettingsProvider<typeof settingsSchema>;
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();
}
@@ -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<void> {
await this.userInfoResource.load();
}
@@ -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<string, Record<string, any>>;
private readonly tempData: TempMap<string, Record<string, any>>;
constructor(private readonly userInfoResource: UserInfoResource, private readonly settingsService: SettingsService) {
constructor(
private readonly userInfoResource: UserInfoResource,
private readonly storageService: StorageService,
) {
this.userData = new Map();
makeObservable<this, 'userData'>(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<T extends Record<any, any>>(key: string, defaultValue: () => T, validate?: (data: T) => boolean): T {
@@ -166,6 +166,20 @@ export class UserInfoResource extends CachedDataResource<UserInfo | null, void,
return result;
}
async updatePreferences(preferences: Record<string, any>): Promise<UserInfo | null> {
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<UserInfo | null> {
await this.load();
@@ -173,7 +187,7 @@ export class UserInfoResource extends CachedDataResource<UserInfo | null, void,
return this.data;
}
this.performUpdate(undefined, [], async () => {
await this.performUpdate(undefined, [], async () => {
await this.graphQLService.sdk.setUserConfigurationParameter({
name: key,
value,
@@ -189,6 +203,15 @@ export class UserInfoResource extends CachedDataResource<UserInfo | null, void,
return this.data;
}
async updateLocalPassword(oldPassword: string, newPassword: string): Promise<void> {
await this.performUpdate(undefined, [], async () => {
await this.graphQLService.sdk.authChangeLocalPassword({
oldPassword: this.authProviderService.hashValue(oldPassword),
newPassword: this.authProviderService.hashValue(newPassword),
});
});
}
async deleteConfigurationParameter(key: ResourceKeySimple<string>): Promise<UserInfo | null> {
await this.load();
@@ -197,7 +220,7 @@ export class UserInfoResource extends CachedDataResource<UserInfo | null, void,
}
const keyList: string[] = [];
this.performUpdate(undefined, [], async () => {
await this.performUpdate(undefined, [], async () => {
await ResourceKeyUtils.forEachAsync(key, async name => {
await this.graphQLService.sdk.setUserConfigurationParameter({
name,
@@ -217,13 +217,6 @@ export class UsersResource extends CachedMapResource<string, AdminUser, UserReso
await this.refresh(userId);
}
async updateLocalPassword(oldPassword: string, newPassword: string): Promise<void> {
await this.graphQLService.sdk.authChangeLocalPassword({
oldPassword: this.authProviderService.hashValue(oldPassword),
newPassword: this.authProviderService.hashValue(newPassword),
});
}
async delete(key: ResourceKeySimple<string>): Promise<void> {
await ResourceKeyUtils.forEachAsync(key, async key => {
if (this.isActiveUser(key)) {
@@ -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"
-6
View File
@@ -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",
@@ -20,7 +20,7 @@ export const AppRefreshButton: React.FC<IProps> = function AppRefreshButton({ cl
const app = useService(App);
function refresh() {
app.start();
app.restart();
}
return (
@@ -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;
}
}
@@ -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;
}
}
@@ -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<ILayoutSizeProps & React.HTMLAttributes<HTMLHeadingElement>> = function GroupTitle({ className, ...rest }) {
interface Props {
sticky?: boolean;
}
export const GroupTitle: React.FC<Props & ILayoutSizeProps & React.HTMLAttributes<HTMLHeadingElement>> = function GroupTitle({
sticky,
className,
...rest
}) {
const style = useS(styles, elementsSizeStyles);
const divProps = filterLayoutFakeProps(rest);
const layoutProps = getLayoutProps(rest);
return <h2 {...divProps} className={s(style, { groupTitle: true, ...layoutProps }, className)} />;
return <h2 {...divProps} className={s(style, { groupTitle: true, sticky, ...layoutProps }, className)} />;
};
@@ -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(<ErrorMessage text="error" />, app);
@@ -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<TKey extends string>(options: CheckboxStateOpti
() => ({
checked,
change(event: React.ChangeEvent<HTMLInputElement>) {
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 },
@@ -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<ILoadableState>, enabled = true, lazy = false) {
const [loadFunctionName] = useState(`${component.name}.useAutoLoad(...)` as const);
if (!Array.isArray(state)) {
state = [state];
state = [state] as ReadonlyArray<ILoadableState>;
}
for (const loader of state as ILoadableState[]) {
for (const loader of state as ReadonlyArray<ILoadableState>) {
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<ILoadableState>) {
if (loader.isError() || (loader.lazy === true && !lazy)) {
continue;
}
@@ -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<HTMLInputElement>) => 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<RenderFieldProps>(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<RenderFieldProps>(function RenderField({
);
}
if (controlType === 'combobox') {
if (controlType === 'selector') {
if (state !== undefined) {
return (
<Combobox
@@ -8,9 +8,13 @@
import { render, screen, waitFor } from '@testing-library/react';
import React, { Suspense } from 'react';
import { addKnownError, consoleSpy } from '@cloudbeaver/tests-runner';
import ErrorBoundary from './__custom__mocks__/ErrorBoundaryMock';
import { importLazyComponent } from './importLazyComponent';
addKnownError(/The above error occurred in one of your React components.*/);
describe('importLazyComponent', () => {
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();
});
});
-1
View File
@@ -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';
+5 -5
View File
@@ -27,10 +27,10 @@ type ExtractStyles<T extends ComponentStyle[]> = Intersect<
? U extends BaseStyles
? U
: U extends (theme: string) => Promise<infer A>
? A extends BaseStyles
? A
? A extends BaseStyles
? A
: never
: never
: never
: never
>;
@@ -47,7 +47,7 @@ export function useS<T extends ComponentStyle[]>(...componentStyles: [...T]): Ex
const [patch, forceUpdate] = useState(0);
const loadedStyles = useRef<BaseStyles[]>([]);
const themeService = useService(ThemeService);
const [currentThemeId, setCurrentThemeId] = useState(() => themeService.currentThemeId);
const [currentThemeId, setCurrentThemeId] = useState(() => themeService.themeId);
const lastThemeRef = useRef<string>(currentThemeId);
// @ts-ignore
const filteredStyles = themeService.mapStyles(componentStyles.flat(Infinity).filter(Boolean) as Style[], context);
@@ -57,7 +57,7 @@ export function useS<T extends ComponentStyle[]>(...componentStyles: [...T]): Ex
executor: themeService.onChange,
handlers: [
function updateThemeId(theme) {
if (currentThemeId !== themeService.currentThemeId && trackTheme) {
if (currentThemeId !== themeService.themeId && trackTheme) {
setCurrentThemeId(theme.id);
}
},
+2 -2
View File
@@ -29,7 +29,7 @@ export function useStyles(...componentStyles: ComponentStyle[]): Record<string,
const stylesRef = useRef<ComponentStyle[]>([]);
const loadedStyles = useRef<BaseStyles[]>([]);
const themeService = useService(ThemeService);
const [currentThemeId, setCurrentThemeId] = useState(() => themeService.currentThemeId);
const [currentThemeId, setCurrentThemeId] = useState(() => themeService.themeId);
const lastThemeRef = useRef<string>(currentThemeId);
//@ts-ignore
const filteredStyles = componentStyles.flat(Infinity).filter(Boolean) as Style[];
@@ -39,7 +39,7 @@ export function useStyles(...componentStyles: ComponentStyle[]): Record<string,
executor: themeService.onChange,
handlers: [
function updateThemeId(theme) {
if (currentThemeId !== themeService.currentThemeId && trackTheme) {
if (currentThemeId !== themeService.themeId && trackTheme) {
setCurrentThemeId(theme.id);
}
},
-18
View File
@@ -9,12 +9,6 @@
{
"path": "../core-authentication/tsconfig.json"
},
{
"path": "../core-browser/tsconfig.json"
},
{
"path": "../core-client-activity/tsconfig.json"
},
{
"path": "../core-di/tsconfig.json"
},
@@ -36,24 +30,12 @@
{
"path": "../core-localization/tsconfig.json"
},
{
"path": "../core-plugin/tsconfig.json"
},
{
"path": "../core-product/tsconfig.json"
},
{
"path": "../core-resource/tsconfig.json"
},
{
"path": "../core-root/tsconfig.json"
},
{
"path": "../core-root/tsconfig.json"
},
{
"path": "../core-sdk/tsconfig.json"
},
{
"path": "../core-sdk/tsconfig.json"
},
+2 -1
View File
@@ -32,7 +32,6 @@
"@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-product": "~0.1.0",
"@cloudbeaver/core-projects": "~0.1.0",
"@cloudbeaver/core-resource": "~0.1.0",
@@ -44,6 +43,8 @@
"@cloudbeaver/core-session-localization": "~0.1.0",
"@cloudbeaver/core-settings": "~0.1.0",
"@cloudbeaver/core-settings-localization": "~0.1.0",
"@cloudbeaver/core-settings-user": "~0.1.0",
"@cloudbeaver/core-storage": "~0.1.0",
"@cloudbeaver/core-theming": "~0.1.0",
"@cloudbeaver/core-ui": "~0.1.0",
"@cloudbeaver/core-version": "~0.1.0",
@@ -19,7 +19,6 @@ 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 { coreResourceManifest } from '@cloudbeaver/core-resource';
@@ -31,6 +30,8 @@ import { coreServerLocalization } from '@cloudbeaver/core-server-localization';
import { coreSessionLocalization } from '@cloudbeaver/core-session-localization';
import { coreSettingsManifest } from '@cloudbeaver/core-settings';
import { coreSettingsLocalizationManifest } from '@cloudbeaver/core-settings-localization';
import { coreSettingsUserManifest } from '@cloudbeaver/core-settings-user';
import { coreStorageManifest } from '@cloudbeaver/core-storage';
import { coreThemingManifest } from '@cloudbeaver/core-theming';
import { coreUIManifest } from '@cloudbeaver/core-ui';
import { coreVersionManifest } from '@cloudbeaver/core-version';
@@ -55,11 +56,11 @@ export const coreManifests: PluginManifest[] = [
coreServerLocalization,
coreBlocksManifest,
coreSettingsManifest,
coreStorageManifest,
coreEventsManifest,
coreResourceManifest,
coreSDKManifest,
coreRootManifest,
corePluginManifest,
coreBrowserManifest,
coreBrowserSettingsManifest,
coreBrowserCookiesManifest,
@@ -77,4 +78,5 @@ export const coreManifests: PluginManifest[] = [
coreAppManifest,
coreClientActivityManifest,
coreNavigationTree,
coreSettingsUserManifest,
];
+6 -3
View File
@@ -51,9 +51,6 @@
{
"path": "../core-navigation-tree/tsconfig.json"
},
{
"path": "../core-plugin/tsconfig.json"
},
{
"path": "../core-product/tsconfig.json"
},
@@ -84,9 +81,15 @@
{
"path": "../core-settings-localization/tsconfig.json"
},
{
"path": "../core-settings-user/tsconfig.json"
},
{
"path": "../core-settings/tsconfig.json"
},
{
"path": "../core-storage/tsconfig.json"
},
{
"path": "../core-theming/tsconfig.json"
},
@@ -11,7 +11,7 @@ import { injectable } from '@cloudbeaver/core-di';
@injectable()
export class CookiesService {
get cookiesEnabled() {
return !this.browserSettingsService.settings.getValue('cookies.disabled');
return !this.browserSettingsService.disabled;
}
constructor(private readonly browserSettingsService: BrowserSettingsService) {}
@@ -20,8 +20,7 @@
"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"
},
"peerDependencies": {},
@@ -29,8 +28,6 @@
"@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",
@@ -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 BROWSER_SETTINGS_GROUP = ROOT_SETTINGS_GROUP.createSubGroup('core_browser_settings_browser_settings_group');
@@ -10,26 +10,28 @@ 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 { BrowserSettingsService, CookiesSettings } from './BrowserSettingsService';
import { BrowserSettingsService } from './BrowserSettingsService';
import { coreBrowserSettingsManifest } from './manifest';
const endpoint = createGQLEndpoint();
const server = mockGraphQL(...mockAppInit(endpoint));
const app = createApp(
coreBrowserManifest,
coreBrowserSettingsManifest,
corePluginManifest,
coreProductManifest,
coreRootManifest,
coreSDKManifest,
coreSettingsManifest,
@@ -37,30 +39,16 @@ const app = createApp(
coreClientActivityManifest,
);
const server = mockGraphQL(...mockAppInit(endpoint));
beforeAll(() => 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();
});
@@ -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<typeof settingsSchema>;
@injectable()
export class BrowserSettingsService extends Dependency {
readonly settings: PluginSettings<typeof settingsSchema>;
get disabled(): boolean {
return this.settings.getValue('core.browser.cookies.disabled');
}
readonly settings: SettingsProvider<typeof settingsSchema>;
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();
}
@@ -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"
},
@@ -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',
@@ -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;
@@ -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}'],
@@ -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"
@@ -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');
@@ -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<typeof settingsSchema>;
@injectable()
export class ConnectionsSettingsService extends Dependency {
readonly settings: PluginSettings<typeof settingsSchema>;
get disabled(): boolean {
return this.settings.getValue('core.connections.disabled');
}
readonly settings: SettingsProvider<typeof settingsSchema>;
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();
}
@@ -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"
}
+10 -2
View File
@@ -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<void> {
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()) {
+2 -1
View File
@@ -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<void> {}
load(): void | Promise<void> {}
}
@@ -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<void>;
import { injectable } from './injectable';
dispose: () => void;
@injectable()
export abstract class Disposable {
@preDestroy()
dispose(): void {}
}
+1 -5
View File
@@ -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",
@@ -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();
});
@@ -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<typeof settingsSchema>;
@injectable()
export class EventsSettingsService extends Dependency {
readonly settings: PluginSettings<typeof settingsSchema>;
get maxPersistentAllow(): number {
return this.settings.getValue('plugin.notifications.maxPersistentAllow');
}
get notificationsPool(): number {
return this.settings.getValue('plugin.notifications.notificationsPool');
}
readonly settings: SettingsProvider<typeof settingsSchema>;
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();
}
@@ -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');
@@ -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;
+3 -15
View File
@@ -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"
},
@@ -32,9 +32,9 @@ export interface IExecutorHandlersCollection<T = unknown, TResult = any | Promis
addContextCreator<TContext>(context: ISyncContextLoader<TContext, T>, creator: ISyncContextLoader<TContext, T>): this;
addContextCreator<TContext>(context: IAsyncContextLoader<TContext, T>, creator: IAsyncContextLoader<TContext, T>): this;
before: <TNext>(executor: IExecutorHandlersCollection<TNext, TResult>, map?: ExecutorDataMap<T, TNext>) => this;
before: <TNext>(executor: IExecutorHandlersCollection<TNext, TResult>, map?: ExecutorDataMap<T, TNext>, filter?: ExecutorDataFilter<T>) => this;
removeBefore: (executor: IExecutorHandlersCollection<any, TResult>) => void;
next: <TNext>(executor: IExecutorHandlersCollection<TNext, TResult>, map?: ExecutorDataMap<T, TNext>) => this;
next: <TNext>(executor: IExecutorHandlersCollection<TNext, TResult>, map?: ExecutorDataMap<T, TNext>, filter?: ExecutorDataFilter<T>) => this;
removeNext: (executor: IExecutorHandlersCollection<any, TResult>) => void;
addCollection: (collection: IExecutorHandlersCollection<T, TResult>) => this;
hasHandler: (handler: IExecutorHandler<T, TResult>) => boolean;
@@ -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": {},
@@ -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' };
@@ -8,5 +8,6 @@
export interface ILocale {
isoCode: string;
displayName: string;
name: string;
nativeName?: string;
}
@@ -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<string>;
private defaultLanguage: string;
readonly onChange: IExecutor<string>;
private language: string | null;
private readonly localeMap: Map<string, Map<string, string>> = 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<LocalizationService, 'localeMap' | 'setCurrentLocale' | 'supportedLanguages' | 'defaultLanguage'>(this, {
defaultLanguage: observable,
makeObservable<LocalizationService, 'localeMap' | 'supportedLanguages' | 'language'>(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 = <T extends TLocalizationToken | undefined>(token: T, fallback?: T, args: Record<string | number, any> = {}): 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<void> {
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<void> {
await this.loadLocaleAsync(this.defaultLanguage);
await this.autoLoadCurrentLanguage();
await this.loadLocale(DEFAULT_LOCALE.isoCode);
await this.loadLocale(this.currentLanguage);
}
async changeLocaleAsync(key: string): Promise<void> {
dispose(): void {
if (this.reactionDisposer) {
this.reactionDisposer();
}
}
async changeLocale(key: string): Promise<void> {
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<void> {
private async loadLocale(localeKey: string): Promise<void> {
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>) => void;
let reject: (reason?: any) => void;
let promise: Promise<void> | null = new Promise<void>((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,
};
}
@@ -11,9 +11,6 @@
},
{
"path": "../core-executor/tsconfig.json"
},
{
"path": "../core-settings/tsconfig.json"
}
],
"include": [
@@ -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",
@@ -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');
@@ -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();
});
@@ -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<typeof settingsSchema>;
@injectable()
export class NavTreeSettingsService extends Dependency {
readonly settings: PluginSettings<typeof settingsSchema>;
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<typeof settingsSchema>;
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,
// },
@@ -65,7 +65,7 @@ export class NavTreeResource extends CachedMapResource<string, string[], Record<
readonly onNodeMove: IExecutor<INavNodeMoveData>;
get childrenLimit(): number {
return this.navTreeSettingsService.settings.getValue('childrenLimit');
return this.navTreeSettingsService.childrenLimit;
}
constructor(
@@ -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"
@@ -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<string, PluginSettings>;
constructor(private readonly productSettingsService: ProductSettingsService) {
this.store = new Map();
}
createSettings<TSchema extends schema.SomeZodObject = schema.AnyZodObject>(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<any> | undefined;
getSettings(scope: string, scopeType: SettingsScopeType): PluginSettings<any> | undefined;
getSettings(scope: string, scopeType?: SettingsScopeType): PluginSettings<any> | undefined {
if (scopeType === undefined) {
return this.store.get(scope);
}
return this.store.get(scopeType + '.' + scope);
}
}
@@ -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<TSchema extends schema.SomeZodObject = schema.AnyZodObject> implements ISettingsSource {
constructor(private readonly source: ISettingsSource, readonly scope: string, readonly schema: TSchema) {}
isReadOnly<TKey extends keyof schema.infer<TSchema>>(key: TKey): boolean {
return this.source.has(key) || false;
}
has<TKey extends keyof schema.infer<TSchema>>(key: TKey): boolean {
return true;
}
isValueDefault<TKey extends keyof schema.infer<TSchema>>(key: TKey): boolean {
return !this.source.has(this.scopedKey(key));
}
getDefaultValue<TKey extends keyof schema.infer<TSchema>>(key: TKey): schema.infer<TSchema>[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<TKey extends keyof schema.infer<TSchema>>(key: TKey): schema.infer<TSchema>[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<TKey extends keyof schema.infer<TSchema>>(key: TKey, value: schema.infer<TSchema>[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)}`;
}
}
@@ -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<SettingsGroup> {
return this.#subGroups;
}
protected groups: Map<string, SettingsGroup>;
#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;
}
}
@@ -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<T = any> {
scope: string;
schema: schema.AnyZodObject;
settingsGetter: SettingsDescriptionGetter<T>;
}
@injectable()
export class SettingsManagerService {
get settings(): ReadonlyArray<ScopeSettingsItem> {
return this.#settings;
}
#settings: ScopeSettingsItem[];
constructor() {
this.#settings = [];
}
registerSettings<TSchema extends schema.SomeZodObject>(
settingsSource: PluginSettings<TSchema>,
settingsGetter: SettingsDescriptionGetter<schema.infer<TSchema>>,
) {
this.#settings.push({
scope: settingsSource.scope,
schema: settingsSource.schema,
settingsGetter,
});
}
getSettings(): ISettingDescriptionWithScope<any>[] {
const settings: ISettingDescriptionWithScope<any>[] = [];
for (const { schema, scope, settingsGetter } of this.settings) {
for (const setting of settingsGetter()) {
settings.push({ ...setting, scope, schema: schema.shape[setting.key] });
}
}
return settings;
}
}
@@ -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<TTarget, TSource> = {
[key in keyof TTarget]: keyof TSource;
};
export function createSettingsAliasResolver<TSource extends schema.SomeZodObject, TTarget extends schema.SomeZodObject = TSource>(
source: ISettingsSource,
target: PluginSettings<TTarget>,
scope: string,
mappings?: SettingsMapping<schema.infer<TTarget>, schema.infer<TSource>>,
): ISettingsSource {
type targetSchema = schema.infer<TTarget>;
type sourceSchema = schema.infer<TSource>;
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();
},
};
}
-10
View File
@@ -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';
@@ -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"
@@ -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);
}
}
@@ -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<string, any>;
constructor(private readonly sessionSettingsService: SessionSettingsService) {
constructor() {
super();
this.settings = new Map();
makeObservable<this, 'settings'>(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<string, any>): 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<void> {}
protected getSnapshot(): Record<string, any> {
return Object.fromEntries(this.settings.entries());
}
}
@@ -1,3 +1,2 @@
export * from './ProductManagerService';
export * from './ProductSettingsService';
export * from './manifest';
+1 -2
View File
@@ -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],
};
@@ -9,9 +9,6 @@
{
"path": "../core-di/tsconfig.json"
},
{
"path": "../core-root/tsconfig.json"
},
{
"path": "../core-settings/tsconfig.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"
},

Some files were not shown because too many files have changed in this diff Show More