diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java index b931441a47..97af6a947c 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CBApplication.java @@ -895,11 +895,7 @@ public synchronized void finishConfiguration( } } - String sessionId = null; - if (credentialsProvider != null && credentialsProvider.getActiveUserCredentials() != null) { - sessionId = credentialsProvider.getActiveUserCredentials().getSmSessionId(); - } - eventController.addEvent(new WSServerConfigurationChangedEvent(sessionId, null)); + sendConfigChangedEvent(credentialsProvider); eventController.setForceSkipEvents(isConfigurationMode()); } @@ -992,11 +988,10 @@ protected void saveRuntimeConfig( sessionExpireTime, appConfig); validateConfiguration(configurationProperties); - writeRuntimeConfig(configurationProperties); + writeRuntimeConfig(getRuntimeAppConfigFile(), configurationProperties); } - private void writeRuntimeConfig(Map configurationProperties) throws DBException { - File runtimeConfigFile = getRuntimeAppConfigFile(); + private void writeRuntimeConfig(File runtimeConfigFile, Map configurationProperties) throws DBException { if (runtimeConfigFile.exists()) { ContentUtils.makeFileBackup(runtimeConfigFile.toPath()); } @@ -1024,33 +1019,7 @@ protected Map collectConfigurationProperties( var serverConfigProperties = new LinkedHashMap(); var originServerConfig = getServerConfigProps(this.originalConfigurationProperties); // get server properties from original configuration file rootConfig.put("server", serverConfigProperties); - if (!CommonUtils.isEmpty(newServerName)) { - copyConfigValue(originServerConfig, - serverConfigProperties, - CBConstants.PARAM_SERVER_NAME, - newServerName); - } - if (!CommonUtils.isEmpty(newServerURL)) { - copyConfigValue( - originServerConfig, serverConfigProperties, CBConstants.PARAM_SERVER_URL, newServerURL); - } - if (sessionExpireTime > 0) { - copyConfigValue( - originServerConfig, - serverConfigProperties, - CBConstants.PARAM_SESSION_EXPIRE_PERIOD, - sessionExpireTime); - } - var databaseConfigProperties = new LinkedHashMap(); - Map oldRuntimeDBConfig = JSONUtils.getObject(originServerConfig, - CBConstants.PARAM_DB_CONFIGURATION); - if (!CommonUtils.isEmpty(databaseConfiguration) && !isDistributed()) { - for (Map.Entry mp : databaseConfiguration.entrySet()) { - copyConfigValue(oldRuntimeDBConfig, databaseConfigProperties, mp.getKey(), mp.getValue()); - } - serverConfigProperties.put(CBConstants.PARAM_DB_CONFIGURATION, databaseConfigProperties); - } - savePasswordPolicyConfig(originServerConfig, serverConfigProperties); + collectServerConfigProperties(newServerName, newServerURL, sessionExpireTime, originServerConfig, serverConfigProperties); } { var appConfigProperties = new LinkedHashMap(); @@ -1160,7 +1129,43 @@ protected Map collectConfigurationProperties( return rootConfig; } - private void savePasswordPolicyConfig(Map originServerConfig, LinkedHashMap serverConfigProperties) { + protected void collectServerConfigProperties( + String newServerName, + String newServerURL, + long sessionExpireTime, + Map originServerConfig, + Map serverConfigProperties + ) { + if (!CommonUtils.isEmpty(newServerName)) { + copyConfigValue(originServerConfig, + serverConfigProperties, + CBConstants.PARAM_SERVER_NAME, + newServerName); + } + if (!CommonUtils.isEmpty(newServerURL)) { + copyConfigValue( + originServerConfig, serverConfigProperties, CBConstants.PARAM_SERVER_URL, newServerURL); + } + if (sessionExpireTime > 0) { + copyConfigValue( + originServerConfig, + serverConfigProperties, + CBConstants.PARAM_SESSION_EXPIRE_PERIOD, + sessionExpireTime); + } + var databaseConfigProperties = new LinkedHashMap(); + Map oldRuntimeDBConfig = JSONUtils.getObject(originServerConfig, + CBConstants.PARAM_DB_CONFIGURATION); + if (!CommonUtils.isEmpty(databaseConfiguration) && !isDistributed()) { + for (Map.Entry mp : databaseConfiguration.entrySet()) { + copyConfigValue(oldRuntimeDBConfig, databaseConfigProperties, mp.getKey(), mp.getValue()); + } + serverConfigProperties.put(CBConstants.PARAM_DB_CONFIGURATION, databaseConfigProperties); + } + savePasswordPolicyConfig(originServerConfig, serverConfigProperties); + } + + private void savePasswordPolicyConfig(Map originServerConfig, Map serverConfigProperties) { // save password policy configuration var passwordPolicyProperties = new LinkedHashMap(); @@ -1297,4 +1302,19 @@ public Class getPlatformClass() { public Class getPlatformUIClass() { return CBPlatformUI.class; } + + public void saveProductConfiguration(SMCredentialsProvider credentialsProvider, Map productConfiguration) throws DBException { + Map mergedConfig = WebAppUtils.mergeConfigurations(this.productConfiguration, productConfiguration); + writeRuntimeConfig(getRuntimeProductConfigFilePath().toFile(), mergedConfig); + this.productConfiguration.putAll(mergedConfig); + sendConfigChangedEvent(credentialsProvider); + } + + protected void sendConfigChangedEvent(SMCredentialsProvider credentialsProvider) { + String sessionId = null; + if (credentialsProvider != null && credentialsProvider.getActiveUserCredentials() != null) { + sessionId = credentialsProvider.getActiveUserCredentials().getSmSessionId(); + } + eventController.addEvent(new WSServerConfigurationChangedEvent(sessionId, null)); + } } diff --git a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls index fa77fb0dd7..586a0e8f96 100644 --- a/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls +++ b/server/bundles/io.cloudbeaver.service.admin/schema/service.admin.graphqls @@ -206,3 +206,8 @@ extend type Query { setDefaultNavigatorSettings( settings: NavigatorSettingsInput!): Boolean! } + +extend type Mutation { + # Updates product configuration + adminUpdateProductConfiguration(configuration: Object!): Boolean! @since(version: "23.3.4") +} diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java index 1f8a328648..19271b41f5 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/DBWServiceAdmin.java @@ -138,6 +138,9 @@ WebAuthProviderConfiguration saveAuthProviderConfiguration( @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) boolean setDefaultNavigatorSettings(WebSession webSession, DBNBrowseSettings settings) throws DBWebException; + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) + boolean updateProductConfiguration(WebSession webSession, Map productConfiguration) throws DBWebException; + //////////////////////////////////////////////////////////////////// // Permissions @@ -179,6 +182,7 @@ boolean setSubjectConnectionAccess(@NotNull WebSession webSession, @NotNull Stri DBWebException; //////////////////////////////////////////////////////////////////// + // User meta parameters @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) @@ -187,11 +191,11 @@ WebPropertyInfo saveUserMetaParameter(WebSession webSession, String id, String d @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) Boolean deleteUserMetaParameter(WebSession webSession, String id) throws DBWebException; - @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) Boolean setUserMetaParameterValues(WebSession webSession, String userId, Map parameters) throws DBWebException; @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) Boolean setTeamMetaParameterValues(WebSession webSession, String teamId, Map parameters) throws DBWebException; + @WebAction(requirePermissions = DBWConstants.PERMISSION_ADMIN) Boolean enableUser(WebSession webSession, String userId, Boolean enabled) throws DBWebException; diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java index 80bfcbb362..9bbc7895ad 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/WebServiceBindingAdmin.java @@ -185,6 +185,9 @@ public void bindWiring(DBWBindingContext model) throws DBWebException { .dataFetcher("setDefaultNavigatorSettings", env -> getService(env).setDefaultNavigatorSettings(getWebSession(env), WebServiceUtils.parseNavigatorSettings(env.getArgument("settings")))) ; + model.getMutationType() + .dataFetcher("adminUpdateProductConfiguration", + env -> getService(env).updateProductConfiguration(getWebSession(env), env.getArgument("configuration"))); } @Override diff --git a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java index 64c1209826..748734679f 100644 --- a/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java +++ b/server/bundles/io.cloudbeaver.service.admin/src/io/cloudbeaver/service/admin/impl/WebServiceAdmin.java @@ -609,6 +609,17 @@ public boolean setDefaultNavigatorSettings(WebSession webSession, DBNBrowseSetti return true; } + + @Override + public boolean updateProductConfiguration(WebSession webSession, Map productConfiguration) throws DBWebException { + try { + CBApplication.getInstance().saveProductConfiguration(webSession, productConfiguration); + return true; + } catch (DBException e) { + throw new DBWebException("Error updating product configuration", e); + } + } + //////////////////////////////////////////////////////////////////// // Access management diff --git a/webapp/packages/core-administration/public/icons/server-configuration.svg b/webapp/packages/core-administration/public/icons/server-configuration.svg new file mode 100644 index 0000000000..b48a05351a --- /dev/null +++ b/webapp/packages/core-administration/public/icons/server-configuration.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts index 1948153ade..5260f4e949 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts @@ -12,7 +12,7 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { Executor, IExecutor } from '@cloudbeaver/core-executor'; import { EAdminPermission, PermissionsService, ServerConfigResource, SessionPermissionsResource } from '@cloudbeaver/core-root'; import { RouterState, ScreenService } from '@cloudbeaver/core-routing'; -import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; +import { SettingsService } from '@cloudbeaver/core-settings'; 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 autoSaveService: LocalStorageSaveService, + private readonly settingsService: SettingsService, private readonly serverConfigResource: ServerConfigResource, private readonly notificationService: NotificationService, ) { @@ -84,8 +84,8 @@ export class AdministrationScreenService { activeScreen: computed, }); - this.autoSaveService.withAutoSave(ADMINISTRATION_ITEMS_STATE, this.itemState, () => new Map()); - this.autoSaveService.withAutoSave(ADMINISTRATION_INFO, this.info, getDefaultAdministrationScreenInfo); + this.settingsService.registerSettings(ADMINISTRATION_ITEMS_STATE, this.itemState, () => new Map()); + this.settingsService.registerSettings(ADMINISTRATION_INFO, this.info, getDefaultAdministrationScreenInfo); this.permissionsResource.onDataUpdate.addPostHandler(() => { this.checkPermissions(this.screenService.routerService.state); }); diff --git a/webapp/packages/core-administration/src/AdministrationSettingsService.ts b/webapp/packages/core-administration/src/AdministrationSettingsService.ts index c6b79f8c9d..6e5188c465 100644 --- a/webapp/packages/core-administration/src/AdministrationSettingsService.ts +++ b/webapp/packages/core-administration/src/AdministrationSettingsService.ts @@ -7,19 +7,20 @@ */ import { injectable } from '@cloudbeaver/core-di'; import { PluginManagerService, PluginSettings } from '@cloudbeaver/core-plugin'; +import { schema } from '@cloudbeaver/core-utils'; -const defaultSettings = { - baseFeatures: [] as string[], -}; +const settingsSchema = schema.object({ + baseFeatures: schema.array(schema.string()).default([]), +}); -export type AdministrationSettings = typeof defaultSettings; +export type AdministrationSettings = schema.infer; @injectable() export class AdministrationSettingsService { - readonly settings: PluginSettings; + readonly settings: PluginSettings; constructor(private readonly pluginManagerService: PluginManagerService) { - this.settings = this.pluginManagerService.createSettings('administration', 'core', defaultSettings); + this.settings = this.pluginManagerService.createSettings('administration', 'core', settingsSchema); } isBase(feature: string): boolean { diff --git a/webapp/packages/core-administration/tsconfig.json b/webapp/packages/core-administration/tsconfig.json index 1587831354..b71c4bdc46 100644 --- a/webapp/packages/core-administration/tsconfig.json +++ b/webapp/packages/core-administration/tsconfig.json @@ -6,6 +6,9 @@ "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo" }, "references": [ + { + "path": "../core-browser/tsconfig.json" + }, { "path": "../core-data-context/tsconfig.json" }, @@ -16,58 +19,55 @@ "path": "../core-events/tsconfig.json" }, { - "path": "../core-executor/tsconfig.json" - }, - { - "path": "../core-localization/tsconfig.json" + "path": "../core-events/tsconfig.json" }, { - "path": "../core-plugin/tsconfig.json" + "path": "../core-executor/tsconfig.json" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-localization/tsconfig.json" }, { - "path": "../core-root/tsconfig.json" + "path": "../core-localization/tsconfig.json" }, { - "path": "../core-routing/tsconfig.json" + "path": "../core-plugin/tsconfig.json" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-plugin/tsconfig.json" }, { - "path": "../core-settings/tsconfig.json" + "path": "../core-product/tsconfig.json" }, { - "path": "../core-theming/tsconfig.json" + "path": "../core-resource/tsconfig.json" }, { - "path": "../core-utils/tsconfig.json" + "path": "../core-root/tsconfig.json" }, { - "path": "../core-browser/tsconfig.json" + "path": "../core-root/tsconfig.json" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-routing/tsconfig.json" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-sdk/tsconfig.json" }, { - "path": "../core-plugin/tsconfig.json" + "path": "../core-sdk/tsconfig.json" }, { - "path": "../core-product/tsconfig.json" + "path": "../core-settings/tsconfig.json" }, { - "path": "../core-root/tsconfig.json" + "path": "../core-settings/tsconfig.json" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-theming/tsconfig.json" }, { - "path": "../core-settings/tsconfig.json" + "path": "../core-utils/tsconfig.json" }, { "path": "../tests-runner/tsconfig.json" diff --git a/webapp/packages/core-app/package.json b/webapp/packages/core-app/package.json index 8ff5cf9c37..cd38d98b18 100644 --- a/webapp/packages/core-app/package.json +++ b/webapp/packages/core-app/package.json @@ -22,17 +22,16 @@ "@cloudbeaver/core-executor": "~0.1.0", "@cloudbeaver/core-localization": "~0.1.0", "@cloudbeaver/core-notifications": "~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-routing": "~0.1.0", "@cloudbeaver/core-theming": "~0.1.0", "@cloudbeaver/core-ui": "~0.1.0", + "@cloudbeaver/core-utils": "~0.1.0", "@cloudbeaver/core-version": "~0.1.0", "mobx-react-lite": "^4.0.5", - "react": "^18.2.0", - "@cloudbeaver/core-utils": "~0.1.0" + "react": "^18.2.0" }, "peerDependencies": {}, "devDependencies": { diff --git a/webapp/packages/core-app/src/CoreSettingsService.ts b/webapp/packages/core-app/src/CoreSettingsService.ts deleted file mode 100644 index 8b18449b54..0000000000 --- a/webapp/packages/core-app/src/CoreSettingsService.ts +++ /dev/null @@ -1,33 +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 { PluginManagerService, PluginSettings } from '@cloudbeaver/core-plugin'; - -const defaultSettings = { - 'app.logViewer.refreshTimeout': 3000, - 'app.logViewer.maxLogRecords': 1000, - 'app.logViewer.logBatchSize': 2000, - 'app.logViewer.maxFailedRequests': 3, - 'app.navigationTree.childrenLimit': 100, - 'app.metadata.editing': true, - 'app.metadata.deleting': true, -}; - -export type CoreSettings = typeof defaultSettings; - -/** - * @deprecated use plugin-log-viewer, core-navigation-tree settings instead - */ -@injectable() -export class CoreSettingsService { - readonly settings: PluginSettings; - - constructor(private readonly pluginManagerService: PluginManagerService) { - this.settings = this.pluginManagerService.getDeprecatedPluginSettings('core', defaultSettings); - } -} diff --git a/webapp/packages/core-app/src/index.ts b/webapp/packages/core-app/src/index.ts index bb15f72a3a..51841f691c 100644 --- a/webapp/packages/core-app/src/index.ts +++ b/webapp/packages/core-app/src/index.ts @@ -1,5 +1,4 @@ // Services -export * from './CoreSettingsService'; export * from './AppScreen/AppScreenService'; export * from './AppScreen/AppScreenBootstrap'; diff --git a/webapp/packages/core-app/src/manifest.ts b/webapp/packages/core-app/src/manifest.ts index d00b384719..0042da0973 100644 --- a/webapp/packages/core-app/src/manifest.ts +++ b/webapp/packages/core-app/src/manifest.ts @@ -10,12 +10,11 @@ import type { PluginManifest } from '@cloudbeaver/core-di'; import { AppLocaleService } from './AppLocaleService'; import { AppScreenBootstrap } from './AppScreen/AppScreenBootstrap'; import { AppScreenService } from './AppScreen/AppScreenService'; -import { CoreSettingsService } from './CoreSettingsService'; export const coreAppManifest: PluginManifest = { info: { name: 'Core App', }, - providers: [AppScreenService, AppScreenBootstrap, CoreSettingsService, AppLocaleService], + providers: [AppScreenService, AppScreenBootstrap, AppLocaleService], }; diff --git a/webapp/packages/core-app/tsconfig.json b/webapp/packages/core-app/tsconfig.json index 58c432283c..a8bdc73634 100644 --- a/webapp/packages/core-app/tsconfig.json +++ b/webapp/packages/core-app/tsconfig.json @@ -21,9 +21,6 @@ { "path": "../core-notifications/tsconfig.json" }, - { - "path": "../core-plugin/tsconfig.json" - }, { "path": "../core-projects/tsconfig.json" }, @@ -43,10 +40,10 @@ "path": "../core-ui/tsconfig.json" }, { - "path": "../core-version/tsconfig.json" + "path": "../core-utils/tsconfig.json" }, { - "path": "../core-utils/tsconfig.json" + "path": "../core-version/tsconfig.json" } ], "include": [ diff --git a/webapp/packages/core-authentication/src/AUTH_SETTINGS_GROUP.ts b/webapp/packages/core-authentication/src/AUTH_SETTINGS_GROUP.ts index 7ea10189d1..bc35d8aa03 100644 --- a/webapp/packages/core-authentication/src/AUTH_SETTINGS_GROUP.ts +++ b/webapp/packages/core-authentication/src/AUTH_SETTINGS_GROUP.ts @@ -5,21 +5,6 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { createSettingsGroup, SettingsData } from '@cloudbeaver/core-settings'; +import { ROOT_SETTINGS_GROUP } from '@cloudbeaver/core-plugin'; -export const AUTH_SETTINGS_GROUP = createSettingsGroup('settings_authentication'); - -export const settings: SettingsData = { - scopeType: 'core', - scope: 'authentication', - settingsData: [ - // TODO: it's administrator settings - // { - // key: 'disableAnonymousAccess', - // type: FormFieldType.Checkbox, - // name: 'settings_authentication_disable_anonymous_access_name', - // description: 'settings_authentication_disable_anonymous_access_description', - // groupId: AUTH_SETTINGS_GROUP.id, - // }, - ], -}; +export const AUTH_SETTINGS_GROUP = ROOT_SETTINGS_GROUP.createSubGroup('core_authentication_auth_settings_group'); diff --git a/webapp/packages/core-authentication/src/AuthSettingsService.ts b/webapp/packages/core-authentication/src/AuthSettingsService.ts index c0c7b6b1f6..5b819b2751 100644 --- a/webapp/packages/core-authentication/src/AuthSettingsService.ts +++ b/webapp/packages/core-authentication/src/AuthSettingsService.ts @@ -6,25 +6,34 @@ * you may not use this file except in compliance with the License. */ import { injectable } from '@cloudbeaver/core-di'; -import { PluginManagerService, PluginSettings } from '@cloudbeaver/core-plugin'; -import { SettingsManagerService } from '@cloudbeaver/core-settings'; +import { PluginManagerService, PluginSettings, SettingsManagerService } from '@cloudbeaver/core-plugin'; +import { schema } from '@cloudbeaver/core-utils'; -import { AUTH_SETTINGS_GROUP, settings } from './AUTH_SETTINGS_GROUP'; +const settingsSchema = schema.object({ + disableAnonymousAccess: schema.coerce.boolean().default(false), +}); -const defaultSettings = { - disableAnonymousAccess: false, -}; - -export type AuthSettings = typeof defaultSettings; +export type AuthSettings = schema.infer; @injectable() export class AuthSettingsService { - readonly settings: PluginSettings; + readonly settings: PluginSettings; + + constructor(private readonly pluginManagerService: PluginManagerService, private readonly settingsManagerService: SettingsManagerService) { + this.settings = this.pluginManagerService.createSettings('authentication', 'core', settingsSchema); - constructor(private readonly pluginManagerService: PluginManagerService, settingsManagerService: SettingsManagerService) { - this.settings = this.pluginManagerService.createSettings('authentication', 'core', defaultSettings); + this.registerSettings(); + } - settingsManagerService.addGroup(AUTH_SETTINGS_GROUP); - settingsManagerService.addSettings(settings.scopeType, settings.scope, settings.settingsData); + private registerSettings() { + this.settingsManagerService.registerSettings(this.settings, () => [ + // { + // key: 'disableAnonymousAccess', + // type: ESettingsValueType.Checkbox, + // name: 'settings_authentication_disable_anonymous_access_name', + // description: 'settings_authentication_disable_anonymous_access_description', + // group: AUTH_SETTINGS_GROUP, + // }, + ]); } } diff --git a/webapp/packages/core-authentication/src/UserDataService.ts b/webapp/packages/core-authentication/src/UserDataService.ts index c5bdaea98e..317336e35d 100644 --- a/webapp/packages/core-authentication/src/UserDataService.ts +++ b/webapp/packages/core-authentication/src/UserDataService.ts @@ -8,7 +8,7 @@ import { makeObservable, observable } from 'mobx'; import { injectable } from '@cloudbeaver/core-di'; -import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; +import { SettingsService } from '@cloudbeaver/core-settings'; import { TempMap } from '@cloudbeaver/core-utils'; import { UserInfoResource } from './UserInfoResource'; @@ -18,7 +18,7 @@ export class UserDataService { private readonly userData: Map>; private readonly tempData: TempMap>; - constructor(private readonly userInfoResource: UserInfoResource, private readonly autoSaveService: LocalStorageSaveService) { + constructor(private readonly userInfoResource: UserInfoResource, private readonly settingsService: SettingsService) { this.userData = new Map(); makeObservable(this, { @@ -27,7 +27,7 @@ export class UserDataService { this.tempData = new TempMap(this.userData); - this.autoSaveService.withAutoSave('user_data', this.userData, () => new Map()); + this.settingsService.registerSettings('user_data', this.userData, () => new Map()); } getUserData>(key: string, defaultValue: () => T, validate?: (data: T) => boolean): T { diff --git a/webapp/packages/core-authentication/src/locales/en.ts b/webapp/packages/core-authentication/src/locales/en.ts index 4554669df2..859671e833 100644 --- a/webapp/packages/core-authentication/src/locales/en.ts +++ b/webapp/packages/core-authentication/src/locales/en.ts @@ -1,5 +1,5 @@ export default [ - ['settings_authentication', 'Authentication'], + ['core_authentication_auth_settings_group', 'Authentication'], ['settings_authentication_disable_anonymous_access_name', 'Disable anonymous access'], ['settings_authentication_disable_anonymous_access_description', 'Disable anonymous access function'], diff --git a/webapp/packages/core-authentication/src/locales/it.ts b/webapp/packages/core-authentication/src/locales/it.ts index 4554669df2..859671e833 100644 --- a/webapp/packages/core-authentication/src/locales/it.ts +++ b/webapp/packages/core-authentication/src/locales/it.ts @@ -1,5 +1,5 @@ export default [ - ['settings_authentication', 'Authentication'], + ['core_authentication_auth_settings_group', 'Authentication'], ['settings_authentication_disable_anonymous_access_name', 'Disable anonymous access'], ['settings_authentication_disable_anonymous_access_description', 'Disable anonymous access function'], diff --git a/webapp/packages/core-authentication/src/locales/ru.ts b/webapp/packages/core-authentication/src/locales/ru.ts index 51fffc3dbd..31219f8af2 100644 --- a/webapp/packages/core-authentication/src/locales/ru.ts +++ b/webapp/packages/core-authentication/src/locales/ru.ts @@ -1,5 +1,5 @@ export default [ - ['settings_authentication', 'Аутентификация'], + ['core_authentication_auth_settings_group', 'Аутентификация'], ['settings_authentication_disable_anonymous_access_name', 'Отключить анонимный доступ'], ['settings_authentication_disable_anonymous_access_description', 'Отключить функцию анонимного доступа'], diff --git a/webapp/packages/core-authentication/src/locales/zh.ts b/webapp/packages/core-authentication/src/locales/zh.ts index 4554669df2..859671e833 100644 --- a/webapp/packages/core-authentication/src/locales/zh.ts +++ b/webapp/packages/core-authentication/src/locales/zh.ts @@ -1,5 +1,5 @@ export default [ - ['settings_authentication', 'Authentication'], + ['core_authentication_auth_settings_group', 'Authentication'], ['settings_authentication_disable_anonymous_access_name', 'Disable anonymous access'], ['settings_authentication_disable_anonymous_access_description', 'Disable anonymous access function'], diff --git a/webapp/packages/core-authentication/tsconfig.json b/webapp/packages/core-authentication/tsconfig.json index 22b6f52556..5b1b6352b5 100644 --- a/webapp/packages/core-authentication/tsconfig.json +++ b/webapp/packages/core-authentication/tsconfig.json @@ -12,70 +12,70 @@ }, "references": [ { - "path": "../core-data-context/tsconfig.json" + "path": "../core-browser/tsconfig.json" }, { - "path": "../core-di/tsconfig.json" + "path": "../core-data-context/tsconfig.json" }, { - "path": "../core-executor/tsconfig.json" + "path": "../core-di/tsconfig.json" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-events/tsconfig.json" }, { - "path": "../core-plugin/tsconfig.json" + "path": "../core-executor/tsconfig.json" }, { - "path": "../core-resource/tsconfig.json" + "path": "../core-localization/tsconfig.json" }, { - "path": "../core-root/tsconfig.json" + "path": "../core-localization/tsconfig.json" }, { - "path": "../core-routing/tsconfig.json" + "path": "../core-plugin/tsconfig.json" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-plugin/tsconfig.json" }, { - "path": "../core-settings/tsconfig.json" + "path": "../core-product/tsconfig.json" }, { - "path": "../core-theming/tsconfig.json" + "path": "../core-resource/tsconfig.json" }, { - "path": "../core-utils/tsconfig.json" + "path": "../core-root/tsconfig.json" }, { - "path": "../core-browser/tsconfig.json" + "path": "../core-root/tsconfig.json" }, { - "path": "../core-events/tsconfig.json" + "path": "../core-routing/tsconfig.json" }, { - "path": "../core-localization/tsconfig.json" + "path": "../core-routing/tsconfig.json" }, { - "path": "../core-plugin/tsconfig.json" + "path": "../core-sdk/tsconfig.json" }, { - "path": "../core-product/tsconfig.json" + "path": "../core-sdk/tsconfig.json" }, { - "path": "../core-root/tsconfig.json" + "path": "../core-settings/tsconfig.json" }, { - "path": "../core-routing/tsconfig.json" + "path": "../core-settings/tsconfig.json" }, { - "path": "../core-sdk/tsconfig.json" + "path": "../core-theming/tsconfig.json" }, { - "path": "../core-settings/tsconfig.json" + "path": "../core-theming/tsconfig.json" }, { - "path": "../core-theming/tsconfig.json" + "path": "../core-utils/tsconfig.json" }, { "path": "../tests-runner/tsconfig.json" diff --git a/webapp/packages/core-blocks/src/Containers/ILayoutSizeProps.ts b/webapp/packages/core-blocks/src/Containers/ILayoutSizeProps.ts index ea039ca292..e5abb40b44 100644 --- a/webapp/packages/core-blocks/src/Containers/ILayoutSizeProps.ts +++ b/webapp/packages/core-blocks/src/Containers/ILayoutSizeProps.ts @@ -17,4 +17,5 @@ export interface ILayoutSizeProps { fill?: boolean; noGrow?: boolean; zeroBasis?: boolean; + groupGap?: boolean; } diff --git a/webapp/packages/core-blocks/src/Containers/filterLayoutFakeProps.ts b/webapp/packages/core-blocks/src/Containers/filterLayoutFakeProps.ts index 5499f4e0f6..e3ec1696dd 100644 --- a/webapp/packages/core-blocks/src/Containers/filterLayoutFakeProps.ts +++ b/webapp/packages/core-blocks/src/Containers/filterLayoutFakeProps.ts @@ -8,12 +8,12 @@ import type { ILayoutSizeProps } from './ILayoutSizeProps'; export function filterLayoutFakeProps(props: T): Omit { - const { noWrap, keepSize, tiny, small, medium, large, maximum, fill, noGrow, zeroBasis, ...rest } = props; + const { noWrap, keepSize, tiny, small, medium, large, maximum, fill, noGrow, zeroBasis, groupGap, ...rest } = props; return rest; } export function getLayoutProps(props: T): ILayoutSizeProps { - const { noWrap, keepSize, tiny, small, medium, large, maximum, fill, noGrow, zeroBasis } = props; - return { noWrap, keepSize, tiny, small, medium, large, maximum, fill, noGrow, zeroBasis }; + const { noWrap, keepSize, tiny, small, medium, large, maximum, fill, noGrow, zeroBasis, groupGap } = props; + return { noWrap, keepSize, tiny, small, medium, large, maximum, fill, noGrow, zeroBasis, groupGap }; } diff --git a/webapp/packages/core-blocks/src/Containers/shared/ElementsSize.m.css b/webapp/packages/core-blocks/src/Containers/shared/ElementsSize.m.css index b38da5b528..3bac01df4c 100644 --- a/webapp/packages/core-blocks/src/Containers/shared/ElementsSize.m.css +++ b/webapp/packages/core-blocks/src/Containers/shared/ElementsSize.m.css @@ -75,3 +75,15 @@ .container > .zeroBasis { flex-basis: 0%; } + +.container.gap > .groupGap + .groupGap { + margin-top: -12px; +} + +.container.gap.compact > .groupGap + .groupGap { + margin-top: -8px; +} + +.container.gap.dense > .groupGap + .groupGap { + margin-top: -4px; +} diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx index d963f59594..a691e52199 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Checkbox.tsx @@ -9,8 +9,6 @@ import { observer } from 'mobx-react-lite'; import type { ComponentStyle } from '@cloudbeaver/core-theming'; -import { filterLayoutFakeProps } from '../../Containers/filterLayoutFakeProps'; -import type { ILayoutSizeProps } from '../../Containers/ILayoutSizeProps'; import { isControlPresented } from '../isControlPresented'; import { CheckboxMarkup, CheckboxMod } from './CheckboxMarkup'; import { CheckboxOnChangeEvent, useCheckboxState } from './useCheckboxState'; @@ -27,13 +25,12 @@ export interface CheckboxBaseProps { export type CheckboxInputProps = Omit< React.InputHTMLAttributes, 'onChange' | 'type' | 'value' | 'defaultValue' | 'checked' | 'defaultChecked' | 'style' -> & - ILayoutSizeProps & { - value?: string; - defaultValue?: string; - defaultChecked?: boolean; - label?: string; - }; +> & { + value?: string; + defaultValue?: string; + defaultChecked?: boolean; + label?: string; +}; export interface ICheckboxControlledProps extends CheckboxInputProps { state?: never; @@ -50,9 +47,9 @@ export interface ICheckboxObjectProps extends CheckboxInput name: TKey; } -export interface CheckboxType { - (props: CheckboxBaseProps & ICheckboxControlledProps): React.ReactElement | null; - (props: CheckboxBaseProps & ICheckboxObjectProps): React.ReactElement | null; +export interface CheckboxType

> { + (props: CheckboxBaseProps & ICheckboxControlledProps & P): React.ReactElement | null; + (props: CheckboxBaseProps & ICheckboxObjectProps & P): React.ReactElement | null; } export const Checkbox: CheckboxType = observer(function Checkbox({ @@ -72,7 +69,6 @@ export const Checkbox: CheckboxType = observer(function Checkbox({ onChange, ...rest }: CheckboxBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps)) { - rest = filterLayoutFakeProps(rest); const checkboxState = useCheckboxState({ value, defaultValue, diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckbox.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckbox.tsx index 51ee9cda01..24f64a2d30 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckbox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckbox.tsx @@ -8,6 +8,7 @@ import { observer } from 'mobx-react-lite'; import { filterLayoutFakeProps, getLayoutProps } from '../../Containers/filterLayoutFakeProps'; +import type { ILayoutSizeProps } from '../../Containers/ILayoutSizeProps'; import { s } from '../../s'; import { useS } from '../../useS'; import { Field } from '../Field'; @@ -16,13 +17,13 @@ import { isControlPresented } from '../isControlPresented'; import { Checkbox, CheckboxBaseProps, CheckboxType, ICheckboxControlledProps, ICheckboxObjectProps } from './Checkbox'; import fieldCheckboxStyles from './FieldCheckbox.m.css'; -export const FieldCheckbox: CheckboxType = observer(function FieldCheckbox({ +export const FieldCheckbox: CheckboxType = observer(function FieldCheckbox({ children, className, ...rest -}: CheckboxBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps)) { +}: CheckboxBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps) & ILayoutSizeProps) { const layoutProps = getLayoutProps(rest); - const checkboxProps = filterLayoutFakeProps(rest); + const checkboxProps = filterLayoutFakeProps(rest) as CheckboxBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps); const styles = useS(fieldCheckboxStyles); if (checkboxProps.autoHide && !isControlPresented(checkboxProps.name, checkboxProps.state)) { @@ -31,7 +32,7 @@ export const FieldCheckbox: CheckboxType = observer(function FieldCheckbox({ return ( - + {children && ( {children} diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Switch.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Switch.tsx index ae8d39784a..996f91fb6f 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/Switch.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/Switch.tsx @@ -8,6 +8,7 @@ import { observer } from 'mobx-react-lite'; import { filterLayoutFakeProps } from '../../Containers/filterLayoutFakeProps'; +import type { ILayoutSizeProps } from '../../Containers/ILayoutSizeProps'; import { s } from '../../s'; import { useS } from '../../useS'; import { Field } from '../Field'; @@ -32,8 +33,8 @@ interface IBaseProps { } interface SwitchType { - (props: IBaseProps & ICheckboxControlledProps): React.ReactElement | null; - (props: IBaseProps & ICheckboxObjectProps): React.ReactElement | null; + (props: IBaseProps & ICheckboxControlledProps & ILayoutSizeProps): React.ReactElement | null; + (props: IBaseProps & ICheckboxObjectProps & ILayoutSizeProps): React.ReactElement | null; } export const Switch: SwitchType = observer(function Switch({ @@ -53,7 +54,7 @@ export const Switch: SwitchType = observer(function Switch({ disabled, onChange, ...rest -}: IBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps)) { +}: IBaseProps & (ICheckboxControlledProps | ICheckboxObjectProps) & ILayoutSizeProps) { const checkboxState = useCheckboxState({ value, defaultValue, diff --git a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx index 9af5dbc436..eccce874b8 100644 --- a/webapp/packages/core-blocks/src/FormControls/Combobox.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Combobox.tsx @@ -62,7 +62,9 @@ interface ComboboxType { (props: ObjectProps): JSX.Element; } -{/* TODO rewrite whole component to select attribute instead of input type text so it has an okay form validation */} +{ + /* TODO rewrite whole component to select attribute instead of input type text so it has an okay form validation */ +} export const Combobox: ComboboxType = observer(function Combobox({ value: controlledValue, defaultValue, @@ -264,7 +266,7 @@ export const Combobox: ComboboxType = observer(function Combobox({ )}

- + {(icon || loading) && (
{loading ? ( diff --git a/webapp/packages/core-blocks/src/FormControls/InputField.m.css b/webapp/packages/core-blocks/src/FormControls/InputField.m.css index 4baf35784c..5d7975f5e2 100644 --- a/webapp/packages/core-blocks/src/FormControls/InputField.m.css +++ b/webapp/packages/core-blocks/src/FormControls/InputField.m.css @@ -48,4 +48,3 @@ .input { font-size: 12px; } - diff --git a/webapp/packages/core-blocks/src/FormControls/useCustomInputValidation.ts b/webapp/packages/core-blocks/src/FormControls/useCustomInputValidation.ts index d8d5452faa..9ee884992f 100644 --- a/webapp/packages/core-blocks/src/FormControls/useCustomInputValidation.ts +++ b/webapp/packages/core-blocks/src/FormControls/useCustomInputValidation.ts @@ -9,12 +9,38 @@ import { useContext, useEffect, useRef } from 'react'; import { ExecutorInterrupter } from '@cloudbeaver/core-executor'; +import { useTranslate } from '../localization/useTranslate'; import { useExecutor } from '../useExecutor'; import { FormContext } from './FormContext'; export function useCustomInputValidation(validation: (value: T) => string | null): React.RefObject { const context = useContext(FormContext); const inputRef = useRef(null); + const translate = useTranslate(); + + function validate(element: HTMLInputElement): boolean { + let value: T = undefined as unknown as T; + + if (element instanceof HTMLInputElement) { + value = element.value as unknown as T; + } + + const valid = element.validity.valid; + const result = validation(value); + + try { + if (typeof result === 'string') { + element.setCustomValidity(result || translate('core_blocks_custom_input_validation_error')); + return false; + } + element.setCustomValidity(''); + return true; + } finally { + if (valid !== element.validity.valid) { + element.reportValidity(); + } + } + } useExecutor({ executor: context?.onValidate, @@ -24,19 +50,8 @@ export function useCustomInputValidation(validation: (value: T) => str return; } - let value: T = undefined as unknown as T; - - if (inputRef.current instanceof HTMLInputElement) { - value = inputRef.current.value as unknown as T; - } - - const result = validation(value); - - if (typeof result === 'string') { - inputRef.current.setCustomValidity(result); + if (!validate(inputRef.current)) { ExecutorInterrupter.interrupt(context); - } else { - inputRef.current?.setCustomValidity(''); } }, ], @@ -48,14 +63,26 @@ export function useCustomInputValidation(validation: (value: T) => str return; } - function resetValidationMessage() { - element?.setCustomValidity(''); + function handleInput(event: Event) { + const target = event.target as HTMLInputElement; + if (target.validity.valid === false) { + validate(target); + } + } + + function handleBlur(event: Event) { + const target = event.target as HTMLInputElement; + if (target.validity.valid === true) { + validate(target); + } } - element.addEventListener('input', resetValidationMessage); + element.addEventListener('input', handleInput); + element.addEventListener('blur', handleBlur); return () => { - element?.removeEventListener('input', resetValidationMessage); + element?.removeEventListener('input', handleInput); + element?.removeEventListener('blur', handleBlur); }; }); diff --git a/webapp/packages/core-blocks/src/ToolsPanel/ToolsAction.tsx b/webapp/packages/core-blocks/src/ToolsPanel/ToolsAction.tsx index 9a26ce3a7d..325e2a659b 100644 --- a/webapp/packages/core-blocks/src/ToolsPanel/ToolsAction.tsx +++ b/webapp/packages/core-blocks/src/ToolsPanel/ToolsAction.tsx @@ -5,26 +5,41 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import type { ButtonHTMLAttributes } from 'react'; +import { type ButtonHTMLAttributes, useState } from 'react'; import { IconOrImage } from '../IconOrImage'; import { Loader } from '../Loader/Loader'; import { s } from '../s'; import { useS } from '../useS'; +import { useStateDelay } from '../useStateDelay'; import style from './ToolsAction.m.css'; -interface Props extends ButtonHTMLAttributes { +interface Props extends Omit, 'onClick'> { icon?: string; viewBox?: string; loading?: boolean; className?: string; + onClick?: (event: React.MouseEvent) => Promise | any; } -export const ToolsAction: React.FC = function ToolsAction({ icon, viewBox, loading, children, className, ...rest }) { +export const ToolsAction: React.FC = function ToolsAction({ icon, viewBox, disabled, loading, children, className, onClick, ...rest }) { const styles = useS(style); + const [loadingState, setLoadingState] = useState(false); + + async function handleClick(event: React.MouseEvent) { + try { + setLoadingState(true); + await onClick?.(event); + } finally { + setLoadingState(false); + } + } + + loading = useStateDelay(loading || loadingState, 300); + disabled = disabled || loadingState; return ( -