dbeaver/pro#5886 core api for permission management (#3519)

* dbeaver/pro#5886 new permissions api

* dbeaver/pro#5886 new permissions api

* dbeaver/pro#5886 test + forgotten commit

* dbeaver/pro#5886 test + forgotten commit, review fixes

* dbeaver/#5886 test + forgotten commits

* dbeaver/pro#5886 clean up CE repos

* dbeaver/pro#5886 gql api for global perms

* dbeaver/pro#5886 new perms validation

* dbeaver/pro#5886 remove unused class

* dbeaver/pro#5886 new api

* chore: update permission key

* dbeaver/pro#5087 feat: permissions management

* feat: check permission for query execution

* fix: override only restrictive permissions

* fix: permissions layer

* fix: status gutter duplication

* fix: script execution error message display

* fix: hide not available script execution actions

* dbeaver/pro#5886 anon perms fix, default value for global perms

* fix: hide settings groups only for specific settings

* feat: restore permissions

* refactor: after review

* fix: merge conflicts

* fix: type import

* fix: merge conflicts

* fix: linting

---------

Co-authored-by: mr-anton-t <42037741+mr-anton-t@users.noreply.github.com>
Co-authored-by: Wroud <wrouds@gmail.com>
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: kseniaguzeeva <112612526+kseniaguzeeva@users.noreply.github.com>
Co-authored-by: sergeyteleshev <iamsergeyteleshev@gmail.com>
This commit is contained in:
Alexander Skoblikov
2025-07-23 03:56:43 +02:00
committed by GitHub
co-authored by mr-anton-t Wroud Evgenia kseniaguzeeva sergeyteleshev
parent 848b95113a
commit 8ebf7eba9c
94 changed files with 861 additions and 519 deletions
@@ -30,6 +30,7 @@ public interface DBWConstants {
String PERMISSION_PRIVATE_PROJECT_ACCESS = "private-project-access";
String PERMISSION_SECRET_MANAGER = "secret-manager";
String GLOBAL_PERMISSION_SCRIPT_EXECUTE = "permission.sql.script.execution";
String PERMISSION_EDIT_STRUCTURE = "edit-meta";
String PERMISSION_EDIT_DATA = "edit-data";
@@ -1022,6 +1022,9 @@ public class WebSession extends BaseWebSession
}
}
public boolean hasGlobalPermission(String permissionId) {
return true;
}
private record PersistentAttribute(Object value) {
}
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 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.
@@ -35,4 +35,6 @@ public @interface WebAction {
boolean initializationRequired() default true;
String[] requireGlobalPermissions() default {};
}
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 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.
@@ -18,22 +18,23 @@ package io.cloudbeaver.model;
import io.cloudbeaver.model.session.WebSession;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.impl.PropertyDescriptor;
import org.jkiss.dbeaver.model.impl.PropertyGroupDescriptor;
import org.jkiss.dbeaver.model.meta.Property;
import org.jkiss.dbeaver.registry.settings.ProductSettingDescriptor;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
public class WebProductSettings {
public class WebGroupPropertiesInfo<T extends PropertyDescriptor> {
@NotNull
private final WebSession webSession;
@NotNull
private final List<PropertyGroupDescriptor<ProductSettingDescriptor>> groups;
private final Collection<PropertyGroupDescriptor<T>> groups;
public WebProductSettings(
public WebGroupPropertiesInfo(
@NotNull WebSession webSession,
@NotNull List<PropertyGroupDescriptor<ProductSettingDescriptor>> groups
@NotNull Collection<PropertyGroupDescriptor<T>> groups
) {
this.webSession = webSession;
this.groups = groups;
@@ -41,7 +42,7 @@ public class WebProductSettings {
@NotNull
@Property
public List<WebSettingsGroupInfo> getGroups() {
public List<WebSettingsGroupInfo<T>> getGroups() {
return groups.stream()
.map(WebSettingsGroupInfo::new)
.collect(Collectors.toList());
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 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.
@@ -17,15 +17,15 @@
package io.cloudbeaver.model;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.model.impl.PropertyDescriptor;
import org.jkiss.dbeaver.model.impl.PropertyGroupDescriptor;
import org.jkiss.dbeaver.model.meta.Property;
import org.jkiss.dbeaver.registry.settings.ProductSettingDescriptor;
public class WebSettingsGroupInfo {
public class WebSettingsGroupInfo<T extends PropertyDescriptor> {
@NotNull
private final PropertyGroupDescriptor<ProductSettingDescriptor> groupDescriptor;
private final PropertyGroupDescriptor<T> groupDescriptor;
public WebSettingsGroupInfo(@NotNull PropertyGroupDescriptor<ProductSettingDescriptor> groupDescriptor) {
public WebSettingsGroupInfo(@NotNull PropertyGroupDescriptor<T> groupDescriptor) {
this.groupDescriptor = groupDescriptor;
}
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 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.
@@ -265,7 +265,8 @@ public abstract class WebServiceBindingBase<API_TYPE extends DBWService> impleme
throw new DBWebExceptionServerNotInitialized(message);
}
String[] reqPermissions = webAction.requirePermissions();
if (reqPermissions.length == 0 && !webAction.authRequired()) {
String[] reqGlobalPermissions = webAction.requireGlobalPermissions();
if (reqPermissions.length == 0 && reqGlobalPermissions.length == 0 && !webAction.authRequired()) {
return;
}
WebSession session = findWebSession(env);
@@ -297,6 +298,13 @@ public abstract class WebServiceBindingBase<API_TYPE extends DBWService> impleme
throw new DBWebExceptionAccessDenied("Access denied");
}
}
// Check permissions
for (String gp : reqGlobalPermissions) {
if (!session.hasGlobalPermission(gp)) {
log.debug("Access to " + method.getName() + " denied for " + session.getUser());
throw new DBWebExceptionAccessDenied("Access denied");
}
}
}
}
@@ -29,6 +29,7 @@ import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.model.navigator.DBNBrowseSettings;
import org.jkiss.dbeaver.model.rm.RMConstants;
import org.jkiss.dbeaver.registry.settings.ProductSettingDescriptor;
import java.util.List;
import java.util.Map;
@@ -48,7 +49,7 @@ public interface DBWServiceCore extends DBWService {
WebPropertyInfo[] getSystemInformationProperties(@NotNull WebSession webSession);
@WebAction
WebProductSettings getProductSettings(@NotNull WebSession webSession);
WebGroupPropertiesInfo<ProductSettingDescriptor> getProductSettings(@NotNull WebSession webSession);
@WebAction
List<WebDatabaseDriverInfo> getDriverList(@NotNull WebSession webSession, String driverId) throws DBWebException;
@@ -54,6 +54,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.ProductSettingDescriptor;
import org.jkiss.dbeaver.registry.settings.ProductSettingsRegistry;
import org.jkiss.dbeaver.runtime.DBWorkbench;
import org.jkiss.utils.CommonUtils;
@@ -644,8 +645,8 @@ public class WebServiceCore implements DBWServiceCore {
}
@Override
public WebProductSettings getProductSettings(@NotNull WebSession webSession) {
return new WebProductSettings(webSession, ProductSettingsRegistry.getInstance().getSettings());
public WebGroupPropertiesInfo<ProductSettingDescriptor> getProductSettings(@NotNull WebSession webSession) {
return new WebGroupPropertiesInfo<>(webSession, ProductSettingsRegistry.getInstance().getSettings());
}
private WebSessionProjectImpl getProjectById(WebSession webSession, String projectId) throws DBWebException {
@@ -16,6 +16,7 @@
*/
package io.cloudbeaver.service.sql;
import io.cloudbeaver.DBWConstants;
import io.cloudbeaver.DBWebException;
import io.cloudbeaver.WebAction;
import io.cloudbeaver.model.WebAsyncTaskInfo;
@@ -90,7 +91,7 @@ public interface DBWServiceSQL extends DBWService {
@WebAction
void setContextDefaults(@NotNull WebSQLContextInfo sqlContext, String catalogName, String schemaName) throws DBWebException;
@WebAction
@WebAction(requireGlobalPermissions = DBWConstants.GLOBAL_PERMISSION_SCRIPT_EXECUTE)
WebAsyncTaskInfo asyncExecuteQuery(
@NotNull WebSQLContextInfo contextInfo,
@NotNull String sql,
@@ -1433,9 +1433,14 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
addSubjectPermissionsUpdateEvent(subjectId, null);
}
public void initialize() throws DBException {
}
private void insertPermissions(Connection dbCon, String subjectId, String[] permissionIds, String grantorId) throws SQLException {
if (!ArrayUtils.isEmpty(permissionIds)) {
try (PreparedStatement dbStat = dbCon.prepareStatement(
try (
PreparedStatement dbStat = dbCon.prepareStatement(
"INSERT INTO {table_prefix}CB_AUTH_PERMISSIONS" +
"(SUBJECT_ID,PERMISSION_ID,GRANT_TIME,GRANTED_BY) VALUES(?,?,?,?)")
) {
@@ -1503,6 +1508,7 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
}
}
protected Set<String> getUserPermissions(String userId, String authRole) throws DBException {
return getUserPermissions(userId);
}
@@ -1584,7 +1590,7 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
}
}
private Set<String> getAnonymousUserPermissions() throws DBException {
protected Set<String> getAnonymousUserPermissions() throws DBException {
var anonymousUserTeam = application.getAppConfiguration().getAnonymousUserTeam();
return getSubjectPermissions(anonymousUserTeam);
}
@@ -2994,7 +3000,7 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
}
private void addSubjectPermissionsUpdateEvent(@NotNull String subjectId, @Nullable SMSubjectType subjectType) {
protected void addSubjectPermissionsUpdateEvent(@NotNull String subjectId, @Nullable SMSubjectType subjectType) {
if (subjectType == null) {
subjectType = getSubjectType(subjectId);
}
@@ -3408,7 +3414,7 @@ public class CBEmbeddedSecurityController<T extends ServletAuthApplication>
}
}
private SMSubjectType getSubjectType(@NotNull String subjectId) {
protected SMSubjectType getSubjectType(@NotNull String subjectId) {
try (Connection dbCon = database.openConnection()) {
String sqlBuilder = "SELECT SUBJECT_TYPE FROM {table_prefix}CB_AUTH_SUBJECT U WHERE SUBJECT_ID = ?";
try (var dbStat = dbCon.prepareStatement(sqlBuilder)) {
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 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.
@@ -46,7 +46,9 @@ public class EmbeddedSecurityControllerFactory<T extends ServletAuthApplication>
SMCredentialsProvider credentialsProvider,
SMControllerConfiguration smConfig
) throws DBException {
boolean initialization = false;
if (DB_INSTANCE == null) {
initialization = true;
synchronized (EmbeddedSecurityControllerFactory.class) {
if (DB_INSTANCE == null) {
DB_INSTANCE = createAndInitDatabaseInstance(
@@ -64,9 +66,13 @@ public class EmbeddedSecurityControllerFactory<T extends ServletAuthApplication>
)).schedule();
}
}
return createEmbeddedSecurityController(
var controller = createEmbeddedSecurityController(
application, DB_INSTANCE, credentialsProvider, smConfig
);
if (initialization) {
controller.initialize();
}
return controller;
}
protected @NotNull CBDatabase createAndInitDatabaseInstance(
@@ -24,7 +24,6 @@ import io.cloudbeaver.model.app.ServletApplication;
import io.cloudbeaver.model.config.WebDatabaseConfig;
import io.cloudbeaver.registry.WebAuthProviderDescriptor;
import io.cloudbeaver.registry.WebAuthProviderRegistry;
import org.jkiss.dbeaver.model.sql.db.InternalProxyConnection;
import io.cloudbeaver.utils.ServletAppUtils;
import org.apache.commons.dbcp2.*;
import org.apache.commons.pool2.impl.GenericObjectPool;
@@ -44,6 +43,7 @@ import org.jkiss.dbeaver.model.security.SMAdminController;
import org.jkiss.dbeaver.model.security.user.SMTeam;
import org.jkiss.dbeaver.model.security.user.SMUser;
import org.jkiss.dbeaver.model.sql.db.InternalDB;
import org.jkiss.dbeaver.model.sql.db.InternalProxyConnection;
import org.jkiss.dbeaver.model.sql.schema.SQLSchemaConfig;
import org.jkiss.dbeaver.registry.DataSourceProviderRegistry;
import org.jkiss.dbeaver.registry.storage.H2Migrator;
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -32,7 +32,7 @@ export class AuthSettingsService {
}
private registerSettings() {
this.settingsManagerService.registerSettings(this.settings, () => [
this.settingsManagerService.registerSettings<typeof settingsSchema>(() => [
// {
// key: 'core.authentication.disableAnonymousAccess',
// access: {
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -20,14 +20,15 @@ const settingsSchema = schema.object({
'core.browser.cookies.disabled': schemaExtra.stringedBoolean().default(false),
});
export type CookiesSettings = schema.infer<typeof settingsSchema>;
export type BrowserSettingsSchema = typeof settingsSchema;
export type CookiesSettings = schema.infer<BrowserSettingsSchema>;
@injectable()
export class BrowserSettingsService extends Dependency {
get disabled(): boolean {
return this.settings.getValue('core.browser.cookies.disabled');
}
readonly settings: SettingsProvider<typeof settingsSchema>;
readonly settings: SettingsProvider<BrowserSettingsSchema>;
constructor(
private readonly settingsProviderService: SettingsProviderService,
@@ -40,7 +41,7 @@ export class BrowserSettingsService extends Dependency {
this.settingsResolverService.addResolver(
ROOT_SETTINGS_LAYER,
/** @deprecated Use settings instead, will be removed in 23.0.0 */
createSettingsAliasResolver(this.settingsResolverService, this.settings, {
createSettingsAliasResolver<BrowserSettingsSchema>(this.settingsResolverService, {
'core.browser.cookies.disabled': 'core.cookies.disabled',
}),
);
@@ -48,7 +49,7 @@ export class BrowserSettingsService extends Dependency {
}
private registerSettings() {
this.settingsManagerService.registerSettings(this.settings, () => [
this.settingsManagerService.registerSettings<typeof settingsSchema>(() => [
// {
// group: BROWSER_COOKIES_SETTINGS_GROUP,
// key: 'cookies.disabled',
@@ -33,7 +33,7 @@ export class ConnectionsSettingsService extends Dependency {
}
private registerSettings() {
this.settingsManagerService.registerSettings(this.settings, () => [
this.settingsManagerService.registerSettings<typeof settingsSchema>(() => [
{
group: CONNECTIONS_SETTINGS_GROUP,
key: 'core.connections.disabled',
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -21,7 +21,8 @@ const settingsSchema = schema.object({
'plugin.notifications.maxPersistentAllow': schema.coerce.number().default(5),
});
export type EventsSettings = schema.infer<typeof settingsSchema>;
export type EventsSettingsSchema = typeof settingsSchema;
export type EventsSettings = schema.infer<EventsSettingsSchema>;
@injectable()
export class EventsSettingsService extends Dependency {
@@ -31,7 +32,7 @@ export class EventsSettingsService extends Dependency {
get notificationsPool(): number {
return this.settings.getValue('plugin.notifications.notificationsPool');
}
readonly settings: SettingsProvider<typeof settingsSchema>;
readonly settings: SettingsProvider<EventsSettingsSchema>;
constructor(
private readonly settingsProviderService: SettingsProviderService,
@@ -44,7 +45,7 @@ export class EventsSettingsService extends Dependency {
this.settingsResolverService.addResolver(
ROOT_SETTINGS_LAYER,
/** @deprecated Use settings instead, will be removed in 23.0.0 */
createSettingsAliasResolver(this.settingsResolverService, this.settings, {
createSettingsAliasResolver<EventsSettingsSchema>(this.settingsResolverService, {
'plugin.notifications.maxPersistentAllow': 'core_events.maxPersistentAllow',
'plugin.notifications.notificationsPool': 'core_events.notificationsPool',
}),
@@ -53,7 +54,7 @@ export class EventsSettingsService extends Dependency {
}
private registerSettings() {
this.settingsManagerService.registerSettings(this.settings, () => [
this.settingsManagerService.registerSettings<typeof settingsSchema>(() => [
// {
// group: NOTIFICATIONS_SETTINGS_GROUP,
// key: 'plugin.notifications.maxPersistentAllow',
@@ -24,7 +24,8 @@ const settingsSchema = schema.object({
'core.navigation-tree.deleting': schemaExtra.stringedBoolean().default(true),
});
export type NavTreeSettings = schema.infer<typeof settingsSchema>;
export type NavTreeSettingsSchema = typeof settingsSchema;
export type NavTreeSettings = schema.infer<NavTreeSettingsSchema>;
@injectable()
export class NavTreeSettingsService extends Dependency {
@@ -37,7 +38,7 @@ export class NavTreeSettingsService extends Dependency {
get deleting(): boolean {
return this.settings.getValue('core.navigation-tree.deleting');
}
readonly settings: SettingsProvider<typeof settingsSchema>;
readonly settings: SettingsProvider<NavTreeSettingsSchema>;
constructor(
private readonly settingsProviderService: SettingsProviderService,
@@ -49,10 +50,10 @@ export class NavTreeSettingsService extends Dependency {
this.settingsResolverService.addResolver(
ROOT_SETTINGS_LAYER,
/** @deprecated Use settings instead, will be removed in 23.0.0 */
createSettingsAliasResolver(this.settingsResolverService, this.settings, {
createSettingsAliasResolver<NavTreeSettingsSchema>(this.settingsResolverService, {
'core.navigation-tree.childrenLimit': 'core.app.navigationTree.childrenLimit',
}),
createSettingsAliasResolver(this.settingsResolverService, this.settings, {
createSettingsAliasResolver<NavTreeSettingsSchema>(this.settingsResolverService, {
'core.navigation-tree.deleting': 'core.app.metadata.deleting',
'core.navigation-tree.editing': 'core.app.metadata.editing',
}),
@@ -62,7 +63,7 @@ export class NavTreeSettingsService extends Dependency {
}
private registerSettings() {
this.settingsManagerService.registerSettings(this.settings, () => [
this.settingsManagerService.registerSettings<typeof settingsSchema>(() => [
{
key: 'core.navigation-tree.childrenLimit',
access: {
@@ -5,7 +5,7 @@
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { action, makeObservable, observable } from 'mobx';
import { makeObservable, observable } from 'mobx';
import { injectable } from '@cloudbeaver/core-di';
import { createSettingsLayer, ROOT_SETTINGS_LAYER, SettingsSource } from '@cloudbeaver/core-settings';
@@ -20,7 +20,6 @@ export class ProductSettingsService extends SettingsSource {
super();
this.settings = new Map();
makeObservable<this, 'settings'>(this, {
clear: action,
settings: observable.shallow,
});
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -23,11 +23,7 @@ export class RootBootstrap extends Bootstrap {
}
override register(): void {
this.settingsManagerService.registerSettings(
this.serverSettingsManagerService.settingsProvider,
this.serverSettingsManagerService.getSettingsGetter(),
this.serverSettingsManagerService.loaders,
);
this.settingsManagerService.registerSettings(this.serverSettingsManagerService.getSettingsGetter(), this.serverSettingsManagerService.loaders);
this.settingsResolverService.addResolver(SERVER_SETTINGS_LAYER, this.serverSettingsService);
}
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -9,24 +9,14 @@ import { action, computed, makeObservable, observable } from 'mobx';
import { injectable } from '@cloudbeaver/core-di';
import { getCachedDataResourceLoaderState } from '@cloudbeaver/core-resource';
import { getObjectPropertyType, type ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import {
ESettingsValueType,
type ISettingDescription,
ROOT_SETTINGS_GROUP,
type SettingDescriptionTransformer,
type SettingsDescriptionGetter,
type SettingsGroup,
SettingsProvider,
SettingsResolverService,
} from '@cloudbeaver/core-settings';
import { getPathParent, type ILoadableState, schema } from '@cloudbeaver/core-utils';
import { type ISettingDescription, ROOT_SETTINGS_GROUP, type SettingsDescriptionGetter, type SettingsGroup } from '@cloudbeaver/core-settings';
import { getPathParent, type ILoadableState } from '@cloudbeaver/core-utils';
import { ServerSettingsResource } from './ServerSettingsResource.js';
import { SettingsTransformationService } from './SettingsTransformationService.js';
@injectable()
export class ServerSettingsManagerService {
readonly settingsProvider: SettingsProvider<schema.AnyZodObject>;
readonly loaders: ReadonlyArray<ILoadableState>;
get providedSettings(): Set<string> {
@@ -34,30 +24,25 @@ export class ServerSettingsManagerService {
}
private get settings(): ReadonlyArray<ISettingDescription<any>> {
return (this.serverSettingsResource.data?.settings || []).map(this.mapSetting.bind(this)).filter(Boolean) as ISettingDescription<any>[];
return (this.serverSettingsResource.data?.settings || [])
.map(this.settingsTransformationService.mapSetting.bind(this.settingsTransformationService, this.serverGroups))
.filter(Boolean) as ISettingDescription<any>[];
}
private serverGroups: Map<string, SettingsGroup>;
private overrideGroups: Map<string, SettingsGroup>;
private settingTransformers: Map<string, SettingDescriptionTransformer>;
constructor(
private readonly settingsTransformationService: SettingsTransformationService,
private readonly serverSettingsResource: ServerSettingsResource,
private readonly settingsResolverService: SettingsResolverService,
) {
this.serverGroups = new Map();
this.overrideGroups = new Map();
this.settingTransformers = new Map();
this.settingsProvider = new SettingsProvider(this.settingsResolverService, schema.object({}));
this.loaders = [getCachedDataResourceLoaderState(this.serverSettingsResource, () => undefined)];
serverSettingsResource.onDataUpdate.addHandler(this.loadSettings.bind(this));
makeObservable<this, 'settings' | 'providedSettings' | 'serverGroups' | 'overrideGroups' | 'settingTransformers' | 'loadSettings'>(this, {
makeObservable<this, 'settings' | 'providedSettings' | 'serverGroups' | 'loadSettings'>(this, {
settings: computed,
providedSettings: computed,
serverGroups: observable.shallow,
overrideGroups: observable.shallow,
settingTransformers: observable.shallow,
loadSettings: action,
});
}
@@ -66,42 +51,6 @@ export class ServerSettingsManagerService {
return () => this.settings;
}
setGroupOverride(group: string, override: SettingsGroup) {
this.overrideGroups.set(group, override);
}
setSettingTransformer(key: string, transformer: SettingDescriptionTransformer) {
if (this.settingTransformers.has(key)) {
throw new Error('Setting transformer already exists');
}
this.settingTransformers.set(key, transformer);
}
private mapSetting(property: ObjectPropertyInfo): ISettingDescription<any> | null {
const key = property.id!;
const transformer = this.settingTransformers.get(key);
const setting: ISettingDescription<any> = {
key,
access: {
scope: this.mapSettingScope(property.scopes || []),
},
group: this.getSettingGroup(property.category),
type: convertObjectPropertyInfoType(property),
name: property.displayName!,
description: property.description!,
options: property.validValues?.map(value => ({ value, name: value })) || [],
};
if (transformer) {
return transformer(setting);
}
return setting;
}
private loadSettings() {
this.serverGroups.forEach(group => group.parent?.deleteSubGroup(group.id));
this.serverGroups.clear();
@@ -119,47 +68,4 @@ export class ServerSettingsManagerService {
this.serverGroups.set(group.id, parentGroup.createSubGroup(group.displayName));
}
}
private getSettingGroup(group?: string): SettingsGroup {
if (!group) {
return ROOT_SETTINGS_GROUP;
}
const overrideGroup = this.overrideGroups.get(group);
if (overrideGroup) {
return overrideGroup;
}
return this.serverGroups.get(group) || ROOT_SETTINGS_GROUP;
}
private mapSettingScope(scopes: string[]): string[] {
if (scopes.length === 0) {
return ['server'];
}
return scopes.map(scope => {
switch (scope) {
case 'global':
return 'server';
case 'user':
return 'client';
default:
return scope;
}
});
}
}
function convertObjectPropertyInfoType(property: ObjectPropertyInfo): ESettingsValueType {
switch (getObjectPropertyType(property)) {
case 'selector':
return ESettingsValueType.Select;
case 'checkbox':
return ESettingsValueType.Checkbox;
case 'textarea':
return ESettingsValueType.Textarea;
case 'input':
default:
return ESettingsValueType.Input;
}
}
@@ -34,7 +34,6 @@ export class ServerSettingsService extends SettingsSource {
makeObservable<this, 'settings' | 'refreshConfig'>(this, {
refreshConfig: action,
settings: observable.shallow,
clear: action,
});
}
@@ -66,7 +65,7 @@ export class ServerSettingsService extends SettingsSource {
this.update(() => {
this.clear();
for (const key of this.settings.keys()) {
this.setValue(key, null);
this.resetValue(key);
}
});
}
@@ -0,0 +1,113 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import { makeObservable, observable } from 'mobx';
import { injectable } from '@cloudbeaver/core-di';
import { getObjectPropertyType, type ObjectPropertyInfo } from '@cloudbeaver/core-sdk';
import {
ESettingsValueType,
type ISettingDescription,
ROOT_SETTINGS_GROUP,
type SettingDescriptionTransformer,
type SettingsGroup,
} from '@cloudbeaver/core-settings';
@injectable()
export class SettingsTransformationService {
private overrideGroups: Map<string, SettingsGroup>;
private settingTransformers: Map<string, SettingDescriptionTransformer>;
constructor() {
this.overrideGroups = new Map();
this.settingTransformers = new Map();
makeObservable<this, 'overrideGroups' | 'settingTransformers'>(this, {
overrideGroups: observable.shallow,
settingTransformers: observable.shallow,
});
}
setGroupOverride(group: string, override: SettingsGroup): void {
this.overrideGroups.set(group, override);
}
setSettingTransformer(key: string, transformer: SettingDescriptionTransformer): void {
if (this.settingTransformers.has(key)) {
throw new Error('Setting transformer already exists');
}
this.settingTransformers.set(key, transformer);
}
mapSetting(groups: Map<string, SettingsGroup>, property: ObjectPropertyInfo): ISettingDescription<any> | null {
const key = property.id!;
const transformer = this.settingTransformers.get(key);
const setting: ISettingDescription<any> = {
key,
access: {
scope: this.mapSettingScope(property.scopes || []),
},
group: this.getSettingGroup(groups, property.category),
type: convertObjectPropertyInfoType(property),
name: property.displayName!,
description: property.description!,
options: property.validValues?.map(value => ({ value, name: value })) || [],
};
if (transformer) {
return transformer(setting);
}
return setting;
}
private getSettingGroup(groups: Map<string, SettingsGroup>, group?: string): SettingsGroup {
if (!group) {
return ROOT_SETTINGS_GROUP;
}
const overrideGroup = this.overrideGroups.get(group);
if (overrideGroup) {
return overrideGroup;
}
return groups.get(group) || ROOT_SETTINGS_GROUP;
}
private mapSettingScope(scopes: string[]): string[] {
if (scopes.length === 0) {
return ['server'];
}
return scopes.map(scope => {
switch (scope) {
case 'global':
return 'server';
case 'user':
return 'client';
default:
return scope;
}
});
}
}
function convertObjectPropertyInfoType(property: ObjectPropertyInfo): ESettingsValueType {
switch (getObjectPropertyType(property)) {
case 'selector':
return ESettingsValueType.Select;
case 'checkbox':
return ESettingsValueType.Checkbox;
case 'textarea':
return ESettingsValueType.Textarea;
case 'input':
default:
return ESettingsValueType.Input;
}
}
+1
View File
@@ -25,6 +25,7 @@ export * from './WorkspaceConfigEventHandler.js';
export * from './SessionEventSource.js';
export * from './QuotasService.js';
export * from './SessionResource.js';
export * from './Settings/SettingsTransformationService.js';
export * from './Settings/ServerSettingsManagerService.js';
export * from './Settings/ServerSettingsService.js';
export * from './Settings/SESSION_SETTINGS_LAYER.js';
@@ -21,6 +21,7 @@ export const coreRootManifest: PluginManifest = {
() => import('./PermissionsService.js').then(m => m.PermissionsService),
() => import('./ServerConfigResource.js').then(m => m.ServerConfigResource),
() => import('./Settings/ServerSettingsService.js').then(m => m.ServerSettingsService),
() => import('./Settings/SettingsTransformationService.js').then(m => m.SettingsTransformationService),
() => import('./SessionActionService.js').then(m => m.SessionActionService),
() => import('./SessionDataResource.js').then(m => m.SessionDataResource),
() => import('./SessionExpireService.js').then(m => m.SessionExpireService),
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -8,14 +8,12 @@
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { LocalizationService } from '@cloudbeaver/core-localization';
import { ESettingsValueType, INTERFACE_SETTINGS_GROUP, SettingsManagerService } from '@cloudbeaver/core-settings';
import { SettingsLocalizationService } from './SettingsLocalizationService.js';
import type { ILocalizationSettingsSchema } from './SettingsLocalizationService.js';
@injectable()
export class LocalizationSettingsManagerService extends Bootstrap {
constructor(
private readonly localizationService: LocalizationService,
private readonly settingsLocalizationService: SettingsLocalizationService,
private readonly settingsManagerService: SettingsManagerService,
) {
super();
@@ -26,7 +24,7 @@ export class LocalizationSettingsManagerService extends Bootstrap {
}
private registerSettings() {
this.settingsManagerService.registerSettings(this.settingsLocalizationService.settingsProvider, () => [
this.settingsManagerService.registerSettings<ILocalizationSettingsSchema>(() => [
{
group: INTERFACE_SETTINGS_GROUP,
key: 'core.localization.language',
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -22,14 +22,15 @@ const settingsSchema = schema.object({
'core.localization.language': schema.string().default(DEFAULT_LOCALE.isoCode),
});
export type ILocalizationSettings = schema.infer<typeof settingsSchema>;
export type ILocalizationSettingsSchema = typeof settingsSchema;
export type ILocalizationSettings = schema.infer<ILocalizationSettingsSchema>;
@injectable()
export class SettingsLocalizationService extends Dependency {
get language(): string {
return this.settingsProvider.getValue('core.localization.language');
}
readonly settingsProvider: SettingsProvider<typeof settingsSchema>;
readonly settingsProvider: SettingsProvider<ILocalizationSettingsSchema>;
constructor(
private readonly settingsProviderService: SettingsProviderService,
@@ -42,11 +43,13 @@ export class SettingsLocalizationService extends Dependency {
this.settingsResolverService.addResolver(
ROOT_SETTINGS_LAYER,
/** @deprecated Use settings instead, will be removed in 23.0.0 */
createSettingsAliasResolver(this.settingsResolverService, this.settingsProvider, { 'core.localization.language': 'core.user.defaultLanguage' }),
createSettingsAliasResolver(this.settingsResolverService, this.settingsProvider, {
createSettingsAliasResolver<ILocalizationSettingsSchema>(this.settingsResolverService, {
'core.localization.language': 'core.user.defaultLanguage',
}),
createSettingsAliasResolver<ILocalizationSettingsSchema>(this.settingsResolverService, {
'core.localization.language': 'core.localization.defaultLanguage',
}),
createSettingsAliasResolver(this.settingsResolverService, this.settingsProvider, { 'core.localization.language': 'app.defaultLanguage' }),
createSettingsAliasResolver<ILocalizationSettingsSchema>(this.settingsResolverService, { 'core.localization.language': 'app.defaultLanguage' }),
);
makeObservable(this, {
@@ -88,7 +88,7 @@ export class UserSettingsService extends SettingsSource {
restoreDefaults() {
this.resetChanges();
for (const key of this.getSource().keys()) {
this.setValue(key, null);
this.resetValue(key);
}
}
@@ -0,0 +1,25 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 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 './ISettingsSource.js';
export interface IEditableSettingsSource extends ISettingsSource {
clear: () => void;
isEdited: (key?: any) => boolean;
isReadOnly: (key: any) => boolean;
getEditedValue: (key: any) => any | undefined;
setValue: (key: any, value: any) => void;
resetValue: (key: any) => void;
save: () => Promise<void>;
isOverrideDefaults?: () => boolean;
restoreDefaults?: () => void;
}
export function isEditableSettingsSource(source: ISettingsSource): source is IEditableSettingsSource {
return 'isEdited' in source;
}
@@ -1,14 +1,15 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 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 { IEditableSettingsSource } from './IEditableSettingsSource.js';
import type { ISettingsSource } from './ISettingsSource.js';
import type { ISettingsLayer } from './SettingsLayer.js';
export interface ISettingsResolverSource extends ISettingsSource {
export interface ISettingsResolverSource extends IEditableSettingsSource {
hasResolver: (layer: ISettingsLayer, resolver: ISettingsSource) => boolean;
addResolver: (layer: ISettingsLayer, ...resolvers: ISettingsSource[]) => void;
removeResolver: (layer: ISettingsLayer, resolver: ISettingsSource) => void;
@@ -15,13 +15,5 @@ export interface ISettingChangeData<T = any> {
export interface ISettingsSource {
readonly onChange: ISyncExecutor<ISettingChangeData>;
has: (key: any) => boolean;
isEdited: (key?: any) => boolean;
isReadOnly: (key: any) => boolean;
getValue: (key: any) => any | undefined;
getEditedValue: (key: any) => any | undefined;
setValue: (key: any, value: any) => void;
save: () => Promise<void>;
clear: () => void;
isOverrideDefaults?: () => boolean;
restoreDefaults?: () => void;
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@ export interface ISettingOptions {
}
export interface ISettingDescription<T = object> {
key: keyof T;
key: Exclude<keyof T, symbol | number>;
access: ISettingAccess;
type: ESettingsValueType;
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -10,7 +10,6 @@ import { computed, makeObservable, observable } from 'mobx';
import { injectable } from '@cloudbeaver/core-di';
import type { ILoadableState, schema } from '@cloudbeaver/core-utils';
import type { SettingsProvider } from '../SettingsProvider.js';
import type { ISettingDescription, SettingsDescriptionGetter } from './ISettingDescription.js';
interface SettingsProviderItem<T extends schema.SomeZodObject = any> {
@@ -41,10 +40,9 @@ export class SettingsManagerService {
}
registerSettings<TSchema extends schema.SomeZodObject>(
provider: SettingsProvider<TSchema>,
settingsGetter: SettingsDescriptionGetter<schema.infer<TSchema>>,
loaders?: ReadonlyArray<ILoadableState>,
) {
): void {
this.settings.push({
settingsGetter,
loaders,
@@ -8,12 +8,13 @@
import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
import type { schema } from '@cloudbeaver/core-utils';
import type { ISettingChangeData, ISettingsSource } from './ISettingsSource.js';
import type { ISettingChangeData } from './ISettingsSource.js';
import type { IEditableSettingsSource } from './IEditableSettingsSource.js';
export class SettingsProvider<TSchema extends schema.SomeZodObject = any> implements ISettingsSource {
export class SettingsProvider<TSchema extends schema.SomeZodObject = any> implements IEditableSettingsSource {
readonly onChange: ISyncExecutor<ISettingChangeData<keyof schema.infer<TSchema>>>;
constructor(
private readonly source: ISettingsSource,
protected readonly source: IEditableSettingsSource,
readonly schema: TSchema,
) {
this.onChange = new SyncExecutor();
@@ -69,6 +70,10 @@ export class SettingsProvider<TSchema extends schema.SomeZodObject = any> implem
this.source.setValue(key, value);
}
resetValue<TKey extends keyof schema.infer<TSchema>>(key: TKey): void {
this.source.resetValue(key);
}
clear(): void {
this.source.clear();
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -27,7 +27,7 @@ export class SettingsProviderService {
});
}
createSettings<TSchema extends schema.SomeZodObject = schema.AnyZodObject>(schema: TSchema) {
createSettings<TSchema extends schema.SomeZodObject = schema.AnyZodObject>(schema: TSchema): SettingsProvider<TSchema> {
this.mergedSchema = this.mergedSchema.merge(schema);
const settings = new SettingsProvider(this.settingsResolverService, schema);
return settings;
@@ -11,7 +11,6 @@ import { expectDeprecatedSettingMessage, expectNoDeprecatedSettingMessage, addDe
import { SettingsResolverSource } from './SettingsResolverSource.js';
import { createSettingsLayer, ROOT_SETTINGS_LAYER } from './SettingsLayer.js';
import { createSettingsAliasResolver } from './createSettingsAliasResolver.js';
import { SettingsProvider } from './SettingsProvider.js';
import { schema } from '@cloudbeaver/core-utils';
import { SettingsSource } from './SettingsSource.js';
import { initKnownConsoleMessages } from '@cloudbeaver/tests-runner';
@@ -62,7 +61,7 @@ export class MemorySettingsService extends SettingsSource {
this.update(() => {
this.clear();
for (const key of this.settings.keys()) {
this.setValue(key, null);
this.resetValue(key);
}
});
}
@@ -100,7 +99,7 @@ describe('SettingsResolverSource', () => {
settingsResolver.addResolver(MEMORY_SETTINGS_LAYER, memorySettingsSource);
settingsResolver.addResolver(
ROOT_SETTINGS_LAYER,
createSettingsAliasResolver(settingsResolver, new SettingsProvider(settingsResolver, settingsSchema), {
createSettingsAliasResolver<typeof settingsSchema>(settingsResolver, {
value: 'deprecated',
}),
);
@@ -126,10 +125,10 @@ describe('SettingsResolverSource', () => {
settingsResolver.addResolver(MEMORY_SETTINGS_LAYER, memorySettingsSource);
settingsResolver.addResolver(
ROOT_SETTINGS_LAYER,
createSettingsAliasResolver(settingsResolver, new SettingsProvider(settingsResolver, settingsSchema), {
createSettingsAliasResolver<typeof settingsSchema>(settingsResolver, {
value: 'deprecated',
}),
createSettingsAliasResolver(settingsResolver, new SettingsProvider(settingsResolver, settingsSchema), {
createSettingsAliasResolver<typeof settingsSchema>(settingsResolver, {
value: 'deprecated2',
}),
);
@@ -13,15 +13,18 @@ import { isNotNullDefined } from '@dbeaver/js-helpers';
import type { ISettingsResolverSource } from './ISettingsResolverSource.js';
import type { ISettingChangeData, ISettingsSource } from './ISettingsSource.js';
import type { ISettingsLayer } from './SettingsLayer.js';
import { isEditableSettingsSource, type IEditableSettingsSource } from './IEditableSettingsSource.js';
type SettingsSource = ISettingsSource | IEditableSettingsSource;
interface ISettingsSourcesLayer {
layer: ISettingsLayer;
sources: ISettingsSource[];
sources: SettingsSource[];
}
export class SettingsResolverSource implements ISettingsResolverSource {
readonly onChange: ISyncExecutor<ISettingChangeData>;
protected get sources(): ISettingsSource[] {
protected get sources(): SettingsSource[] {
return this.layers
.slice()
.sort((a, b) => a.layer.level - b.layer.level)
@@ -42,11 +45,11 @@ export class SettingsResolverSource implements ISettingsResolverSource {
});
}
hasResolver(layer: ISettingsLayer, resolver: ISettingsSource): boolean {
hasResolver(layer: ISettingsLayer, resolver: SettingsSource): boolean {
return this.tryGetLayerSources(layer)?.sources.includes(resolver) || false;
}
removeResolver(layer: ISettingsLayer, resolver: ISettingsSource): void {
removeResolver(layer: ISettingsLayer, resolver: SettingsSource): void {
const layerSources = this.getLayerSources(layer);
const index = layerSources.sources.indexOf(resolver);
@@ -57,7 +60,7 @@ export class SettingsResolverSource implements ISettingsResolverSource {
}
}
addResolver(layer: ISettingsLayer, ...resolvers: ISettingsSource[]): void {
addResolver(layer: ISettingsLayer, ...resolvers: SettingsSource[]): void {
if (resolvers.some(this.hasResolver.bind(this, layer))) {
return;
}
@@ -85,11 +88,15 @@ export class SettingsResolverSource implements ISettingsResolverSource {
}
isOverrideDefaults(): boolean {
return this.sources.some(r => r.isOverrideDefaults?.());
return this.sources.some(r => isEditableSettingsSource(r) && r.isOverrideDefaults?.());
}
isEdited(key?: any): boolean {
return this.sources.find(r => r.has(key))?.isEdited(key) || false;
const source = this.sources.find(r => r.has(key));
if (!source || !isEditableSettingsSource(source)) {
return false;
}
return source.isEdited(key);
}
isReadOnly(key: any, stopAt?: ISettingsSource): boolean {
@@ -102,7 +109,7 @@ export class SettingsResolverSource implements ISettingsResolverSource {
break;
}
if (source.isReadOnly(key)) {
if (!isEditableSettingsSource(source) || source.isReadOnly(key)) {
return true;
}
}
@@ -114,7 +121,8 @@ export class SettingsResolverSource implements ISettingsResolverSource {
}
getEditedValue(key: any): any {
return this.sources.find(r => r.has(key) && isNotNullDefined(r.getEditedValue(key)))?.getEditedValue(key);
const source = this.sources.filter(isEditableSettingsSource).find(r => r.has(key) && isNotNullDefined(r.getEditedValue(key)));
return source ? source.getEditedValue(key) : undefined;
}
getValue(key: any): any {
@@ -123,9 +131,9 @@ export class SettingsResolverSource implements ISettingsResolverSource {
setValue(key: any, value: any): void {
for (const source of this.sources) {
const readonly = source.isReadOnly(key);
const readonly = !isEditableSettingsSource(source) || source.isReadOnly(key);
if (source.has(key) && readonly) {
if (source.has(key) || readonly) {
throw new Error(`Can't set value for key ${key}`);
}
@@ -136,9 +144,21 @@ export class SettingsResolverSource implements ISettingsResolverSource {
}
}
resetValue(key: any): void {
for (const source of this.sources) {
const readonly = !isEditableSettingsSource(source) || source.isReadOnly(key);
if (source.has(key) || readonly) {
throw new Error(`Can't set value for key ${key}`);
}
source.resetValue(key);
return;
}
}
async save(): Promise<void> {
for (const source of this.sources) {
if (source.isEdited()) {
if (isEditableSettingsSource(source) && source.isEdited()) {
await source.save();
}
}
@@ -146,7 +166,9 @@ export class SettingsResolverSource implements ISettingsResolverSource {
clear(): void {
for (const resolver of this.sources) {
resolver.clear();
if (isEditableSettingsSource(resolver)) {
resolver.clear();
}
}
}
@@ -10,9 +10,10 @@ import { action, makeObservable, observable } from 'mobx';
import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
import { isNotNullDefined } from '@dbeaver/js-helpers';
import type { ISettingChangeData, ISettingsSource } from './ISettingsSource.js';
import type { ISettingChangeData } from './ISettingsSource.js';
import type { IEditableSettingsSource } from './IEditableSettingsSource.js';
export abstract class SettingsSource implements ISettingsSource {
export abstract class SettingsSource implements IEditableSettingsSource {
readonly onChange: ISyncExecutor<ISettingChangeData>;
private updating: boolean;
protected readonly changes: Map<any, any>;
@@ -24,6 +25,7 @@ export abstract class SettingsSource implements ISettingsSource {
makeObservable<this, 'update' | 'changes'>(this, {
changes: observable.shallow,
update: action,
clear: action,
});
}
@@ -44,6 +46,10 @@ export abstract class SettingsSource implements ISettingsSource {
abstract getValue(key: any): any;
abstract save(): Promise<void>;
resetValue(key: any): void {
this.setValue(key, null);
}
clear(): void {
this.changes.clear();
}
@@ -7,14 +7,13 @@
*/
import { describe, expect, test, beforeEach } from 'vitest';
import { SyncExecutor } from '@cloudbeaver/core-executor';
import type { IEditableSettingsSource } from './IEditableSettingsSource.js';
import {
expectDeprecatedSettingMessage,
expectNoDeprecatedSettingMessage,
addDeprecatedSettingPattern,
} from './__custom_mocks__/expectDeprecatedSettingMessage.js';
import { createSettingsAliasResolver, DEPRECATED_SETTINGS } from './createSettingsAliasResolver.js';
import type { ISettingsSource } from './ISettingsSource.js';
import { initKnownConsoleMessages } from '@cloudbeaver/tests-runner';
const deprecatedSettings = {
@@ -26,7 +25,7 @@ const newSettings = {
value: 'value',
};
function createSource(settings: Record<any, any>): ISettingsSource {
function createSource(settings: Record<any, any>): IEditableSettingsSource {
return {
onChange: new SyncExecutor(),
has(key: any): boolean {
@@ -45,13 +44,14 @@ function createSource(settings: Record<any, any>): ISettingsSource {
return undefined;
},
setValue(key: any, value: any): void {},
resetValue(key: any): void {},
async save(): Promise<void> {},
clear(): void {},
};
}
function createResolver(settings: Record<any, any>) {
return createSettingsAliasResolver(createSource(settings), null as any, {
return createSettingsAliasResolver(createSource(settings), {
value: 'deprecated',
});
}
@@ -8,8 +8,8 @@
import { type ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
import { invertObject, type schema } from '@cloudbeaver/core-utils';
import type { ISettingChangeData, ISettingsSource } from './ISettingsSource.js';
import type { SettingsProvider } from './SettingsProvider.js';
import type { ISettingChangeData } from './ISettingsSource.js';
import type { IEditableSettingsSource } from './IEditableSettingsSource.js';
export const DEPRECATED_SETTINGS = new Set();
@@ -42,10 +42,9 @@ function withLock<T>(fn: (locked: boolean) => T): T {
}
export function createSettingsAliasResolver<TTarget extends schema.SomeZodObject>(
source: ISettingsSource,
target: SettingsProvider<TTarget>,
source: IEditableSettingsSource,
mappings: SettingsMapping<schema.infer<TTarget>>,
): ISettingsSource {
): IEditableSettingsSource {
type targetSchema = schema.infer<TTarget>;
const reversed = invertObject(mappings);
@@ -122,6 +121,16 @@ export function createSettingsAliasResolver<TTarget extends schema.SomeZodObject
}
});
},
resetValue(key) {
if (!(key in mappings)) {
return;
}
withLock(locked => {
if (!locked) {
source.resetValue(key);
}
});
},
async save() {},
clear() {},
};
@@ -0,0 +1,94 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2025 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 ISyncExecutor, SyncExecutor } from '@cloudbeaver/core-executor';
import { type schema } from '@cloudbeaver/core-utils';
import { isNotNullDefined } from '@dbeaver/js-helpers';
import type { ISettingChangeData, ISettingsSource } from './ISettingsSource.js';
import type { IEditableSettingsSource } from './IEditableSettingsSource.js';
interface ISettingsOverride<TTarget, TKey extends keyof TTarget> {
key: string;
map?: (value: any) => TTarget[TKey];
filter?: (value: any) => boolean;
}
type SettingsMapping<TTarget> = Partial<{
[key in keyof TTarget]: string | ISettingsOverride<TTarget, key>;
}>;
export function createSettingsOverrideResolver<TTarget extends schema.SomeZodObject>(
source: ISettingsSource | IEditableSettingsSource,
mappings: SettingsMapping<schema.infer<TTarget>>,
): ISettingsSource {
type targetSchema = schema.infer<TTarget>;
const fullDescriptionOverrides: Record<keyof targetSchema, ISettingsOverride<targetSchema, keyof targetSchema>> = {} as any;
const reversed: Record<string, keyof targetSchema> = {};
for (const key in mappings) {
const value = mappings[key];
if (typeof value === 'string') {
reversed[value] = key;
fullDescriptionOverrides[key] = {
key: value,
filter: (value: any) => isNotNullDefined(value),
map: (value: any) => value,
};
} else if (value && typeof value === 'object' && 'key' in value) {
reversed[value.key] = key;
fullDescriptionOverrides[key] = {
key: value.key,
filter: value.filter || ((value: any) => isNotNullDefined(value)),
map: value.map || ((value: any) => value),
};
}
}
function reverseMapKey(key: string): keyof targetSchema {
return reversed[key] || (key as keyof targetSchema);
}
const onChange: ISyncExecutor<ISettingChangeData<any>> = new SyncExecutor();
source.onChange.next(
onChange,
data => ({ ...data, key: reverseMapKey(data.key) }),
data => data.key in reversed,
);
return {
onChange,
has(key) {
if (!(key in fullDescriptionOverrides)) {
return false;
}
const override = fullDescriptionOverrides[key]!;
if (source.has(override.key)) {
return override.filter!(source.getValue(override.key));
}
return false;
},
getValue(key) {
if (!(key in fullDescriptionOverrides)) {
return undefined;
}
const override = fullDescriptionOverrides[key]!;
const value = source.getValue(override.key);
if (override) {
return override.map!(value);
}
return value;
},
};
}
+3 -1
View File
@@ -1,15 +1,17 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export * from './manifest.js';
export * from './createSettingsOverrideResolver.js';
export * from './createSettingsAliasResolver.js';
export * from './SettingsScopeType.js';
export * from './SettingsResolverSource.js';
export * from './ISettingsResolverSource.js';
export * from './IEditableSettingsSource.js';
export * from './ISettingsSource.js';
export * from './SettingsLayer.js';
export * from './SettingsResolverService.js';
@@ -55,27 +55,12 @@ export class SystemThemeService extends Bootstrap {
has: function (key: IThemeSettingsKey): boolean {
return key === 'core.theming.theme';
},
isEdited: function (key?: IThemeSettingsKey): boolean {
return false;
},
isReadOnly: function (key: IThemeSettingsKey): boolean {
return true;
},
getValue: function (key: IThemeSettingsKey) {
if (key === 'core.theming.theme') {
return 'system';
}
return undefined;
},
getEditedValue: function (key: IThemeSettingsKey) {
if (key === 'core.theming.theme') {
return 'system';
}
return undefined;
},
setValue: function (key: IThemeSettingsKey, value: any): void {},
save: async function (): Promise<void> {},
clear: function (): void {},
});
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -9,13 +9,13 @@ import { Dependency, injectable } from '@cloudbeaver/core-di';
import { ESettingsValueType, INTERFACE_SETTINGS_GROUP, SettingsManagerService } from '@cloudbeaver/core-settings';
import { ThemeService } from './ThemeService.js';
import { ThemeSettingsService } from './ThemeSettingsService.js';
import type { IThemeSettingsSchema } from './ThemeSettingsService.js';
@injectable()
export class ThemeSettingsManagementService extends Dependency {
constructor(themeSettingsService: ThemeSettingsService, themeService: ThemeService, settingsManagerService: SettingsManagerService) {
constructor(themeService: ThemeService, settingsManagerService: SettingsManagerService) {
super();
settingsManagerService.registerSettings(themeSettingsService.settings, () => [
settingsManagerService.registerSettings<IThemeSettingsSchema>(() => [
{
key: 'core.theming.theme',
access: {
@@ -21,7 +21,8 @@ const settingsSchema = schema.object({
'core.theming.theme': schema.string().default(FALLBACK_THEME_ID),
});
export type IThemeSettings = schema.infer<typeof settingsSchema>;
export type IThemeSettingsSchema = typeof settingsSchema;
export type IThemeSettings = schema.infer<IThemeSettingsSchema>;
export type IThemeSettingsKey = keyof IThemeSettings;
@injectable()
@@ -29,7 +30,7 @@ export class ThemeSettingsService {
get theme(): string {
return this.settings.getValue('core.theming.theme');
}
readonly settings: SettingsProvider<typeof settingsSchema>;
readonly settings: SettingsProvider<IThemeSettingsSchema>;
constructor(
private readonly settingsProviderService: SettingsProviderService,
@@ -40,11 +41,11 @@ export class ThemeSettingsService {
this.settingsResolverService.addResolver(
ROOT_SETTINGS_LAYER,
/** @deprecated Use settings instead, will be removed in 23.0.0 */
createSettingsAliasResolver(this.settingsResolverService, this.settings, { 'core.theming.theme': 'core.user.defaultTheme' }),
createSettingsAliasResolver(this.settingsResolverService, this.settings, {
createSettingsAliasResolver<IThemeSettingsSchema>(this.settingsResolverService, { 'core.theming.theme': 'core.user.defaultTheme' }),
createSettingsAliasResolver<IThemeSettingsSchema>(this.settingsResolverService, {
'core.theming.theme': 'core.localization.defaultTheme',
}),
createSettingsAliasResolver(this.settingsResolverService, this.settings, { 'core.theming.theme': 'app.defaultTheme' }),
createSettingsAliasResolver<IThemeSettingsSchema>(this.settingsResolverService, { 'core.theming.theme': 'app.defaultTheme' }),
);
}
}
+2 -1
View File
@@ -43,7 +43,7 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
this.formState.formatTask.addHandler(executorHandlerFilter(() => this.isLoaded() && this.isChanged, this.format.bind(this)));
this.formState.validationTask.addHandler(executorHandlerFilter(() => this.isLoaded(), this.handleValidation.bind(this)));
makeObservable<this, 'loaded' | 'loading' | 'setInitialState'>(this, {
makeObservable<this, 'loaded' | 'loading' | 'setInitialState' | 'setState'>(this, {
initialState: observable,
state: observable,
exception: observable.ref,
@@ -54,6 +54,7 @@ export abstract class FormPart<TPartState, TFormState = any> implements IFormPar
setInitialState: action,
isDisabled: computed,
isChanged: computed,
setState: action,
});
}
@@ -10,7 +10,7 @@ import { observer } from 'mobx-react-lite';
import { Button, Container, Form, s, StatusMessage, useForm, useS, useTranslate } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { ENotificationType, NotificationService } from '@cloudbeaver/core-events';
import { TabList, TabPanelList, TabsState } from '@cloudbeaver/core-ui';
import { FormMode, TabList, TabPanelList, TabsState } from '@cloudbeaver/core-ui';
import { getFirstException } from '@cloudbeaver/core-utils';
import style from './TeamForm.module.css';
@@ -44,7 +44,9 @@ export const TeamForm = observer<Props>(function TeamForm({ state, onCancel, onS
notificationService.logSuccess({ title, message });
onSave?.();
onCancel?.();
if (initialMode === FormMode.Create) {
onCancel?.();
}
} else {
if (exception) {
const errorKey = state.mode === 'create' ? 'administration_teams_team_create_error' : 'administration_teams_team_save_error';
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -10,7 +10,18 @@ import { observer } from 'mobx-react-lite';
import type { AdministrationItemContentComponent } from '@cloudbeaver/core-administration';
import { s, SContext, type StyleRegistry, ToolsPanel, useS, useTranslate } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { type ITabData, Tab, TabList, TabPanel, TabPanelStyles, TabsState, TabStyles, TabTitle, TabTitleStyles } from '@cloudbeaver/core-ui';
import {
type ITabData,
Tab,
TabList,
TabPanel,
TabPanelList,
TabPanelStyles,
TabsState,
TabStyles,
TabTitle,
TabTitleStyles,
} from '@cloudbeaver/core-ui';
import style from './shared/UsersAdministration.module.css';
import tabStyle from './shared/UsersAdministrationTab.module.css';
@@ -19,6 +30,7 @@ import TabTitleModuleStyles from './shared/UsersAdministrationTabTitle.module.cs
import { TeamsPage } from './Teams/TeamsTable/TeamsPage.js';
import { EUsersAdministrationSub, UsersAdministrationNavigationService } from './UsersAdministrationNavigationService.js';
import { UsersPage } from './UsersTable/UsersPage.js';
import { UsersAdministrationService } from './UsersAdministrationService.js';
const tabPanelRegistry: StyleRegistry = [[TabPanelStyles, { mode: 'append', styles: [tabPanelStyle] }]];
@@ -30,6 +42,7 @@ const mainTabsRegistry: StyleRegistry = [
export const UsersAdministration: AdministrationItemContentComponent = observer(function UsersAdministration({ sub, param }) {
const translate = useTranslate();
const usersAdministrationNavigationService = useService(UsersAdministrationNavigationService);
const usersAdministrationService = useService(UsersAdministrationService);
const subName = sub?.name || EUsersAdministrationSub.Users;
const styles = useS(style, tabStyle);
@@ -44,18 +57,23 @@ export const UsersAdministration: AdministrationItemContentComponent = observer(
}
return (
<TabsState selectedId={subName} lazy onChange={openSub}>
<TabsState selectedId={subName} autoSelect={false} container={usersAdministrationService.tabsContainer} lazy onChange={openSub}>
<ToolsPanel bottomBorder>
<TabList className={s(styles, { tabList: true, administrationTabs: true })} aria-label="User Administration pages" underline>
<SContext registry={mainTabsRegistry}>
<SContext registry={mainTabsRegistry}>
<TabList
className={s(styles, { tabList: true, administrationTabs: true })}
aria-label={translate('authentication_administration_pages_label')}
childrenFirst
underline
>
<Tab tabId={EUsersAdministrationSub.Users}>
<TabTitle>{translate('authentication_administration_item_users')}</TabTitle>
</Tab>
<Tab tabId={EUsersAdministrationSub.Teams}>
<TabTitle>{translate('administration_teams_tab_title')}</TabTitle>
</Tab>
</SContext>
</TabList>
</TabList>
</SContext>
</ToolsPanel>
<SContext registry={tabPanelRegistry}>
<TabPanel tabId={EUsersAdministrationSub.Users}>
@@ -64,6 +82,7 @@ export const UsersAdministration: AdministrationItemContentComponent = observer(
<TabPanel tabId={EUsersAdministrationSub.Teams}>
<TeamsPage param={param} />
</TabPanel>
<TabPanelList />
</SContext>
</TabsState>
);
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -32,7 +32,7 @@ export class UsersAdministrationNavigationService {
this.navToSub(EUsersAdministrationSub.Users, ADMINISTRATION_ITEM_USER_CREATE_PARAM);
}
navToSub(sub: EUsersAdministrationSub, param?: string): void {
navToSub(sub: EUsersAdministrationSub | string, param?: string): void {
this.administrationScreenService.navigateToItemSub(UsersAdministrationNavigationService.ItemName, sub, param);
}
}
@@ -7,10 +7,11 @@
*/
import React from 'react';
import { AdministrationItemService } from '@cloudbeaver/core-administration';
import { AdministrationItemService, type IAdministrationItem } from '@cloudbeaver/core-administration';
import { type AdminUser, TeamsResource, UsersResource } from '@cloudbeaver/core-authentication';
import { PlaceholderContainer } from '@cloudbeaver/core-blocks';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { TabsContainer } from '@cloudbeaver/core-ui';
import { CreateTeamService } from './Teams/TeamsTable/CreateTeamService.js';
import { EUsersAdministrationSub, UsersAdministrationNavigationService } from './UsersAdministrationNavigationService.js';
@@ -37,7 +38,9 @@ export interface IUserDetailsInfoProps {
@injectable()
export class UsersAdministrationService extends Bootstrap {
readonly userDetailsInfoPlaceholder = new PlaceholderContainer<IUserDetailsInfoProps>();
readonly tabsContainer: TabsContainer;
readonly userDetailsInfoPlaceholder: PlaceholderContainer<IUserDetailsInfoProps>;
administrationItem!: IAdministrationItem;
constructor(
private readonly administrationItemService: AdministrationItemService,
@@ -47,10 +50,12 @@ export class UsersAdministrationService extends Bootstrap {
private readonly usersResource: UsersResource,
) {
super();
this.userDetailsInfoPlaceholder = new PlaceholderContainer();
this.tabsContainer = new TabsContainer('Access Control');
}
override register() {
this.administrationItemService.create({
override register(): void {
this.administrationItem = this.administrationItemService.create({
name: UsersAdministrationNavigationService.ItemName,
order: 4,
sub: [
@@ -1,4 +1,5 @@
export default [
['authentication_administration_pages_label', 'Pages'],
['authentication_administration_user_connections_user_add', 'User Creation'],
['authentication_administration_user_connections_user_new', 'New user'],
['authentication_administration_user_connections_access_load_fail', "User's granted connections loading failed"],
@@ -1,4 +1,5 @@
export default [
['authentication_administration_pages_label', 'Pages'],
['authentication_administration_user_connections_user_add', "Création d'utilisateur"],
['authentication_administration_user_connections_user_new', 'Nouvel utilisateur'],
['authentication_administration_user_connections_access_load_fail', "Échec du chargement des connexions accordées à l'utilisateur"],
@@ -1,4 +1,5 @@
export default [
['authentication_administration_pages_label', 'Pagine'],
['authentication_administration_user_connections_user_add', 'Creazione di Utente'],
['authentication_administration_user_connections_user_new', 'Nuovo utente'],
['authentication_administration_user_connections_access_load_fail', "Errore in fase di caricamento delle connessioni autorizzate all'utente"],
@@ -1,4 +1,5 @@
export default [
['authentication_administration_pages_label', 'Страницы'],
['authentication_administration_user_connections_user_add', 'Создание пользователя'],
['authentication_administration_user_connections_user_new', 'Новый пользователь'],
['authentication_administration_user_connections_access_load_fail', 'Не удалось загрузить доступные пользователю коннекшены'],
@@ -1,4 +1,5 @@
export default [
['authentication_administration_pages_label', 'Pages'],
['authentication_administration_user_connections_user_add', 'Tạo Người dùng'],
['authentication_administration_user_connections_user_new', 'Người dùng mới'],
['authentication_administration_user_connections_access_load_fail', 'Tải kết nối được cấp cho người dùng thất bại'],
@@ -1,4 +1,5 @@
export default [
['authentication_administration_pages_label', '页面'],
['authentication_administration_user_connections_user_add', '创建用户'],
['authentication_administration_user_connections_user_new', '新用户'],
['authentication_administration_user_connections_access_load_fail', '用户授予的连接加载失败'],
@@ -32,7 +32,7 @@ export class CustomConnectionSettingsService extends Dependency {
}
private registerSettings() {
this.settingsManagerService.registerSettings(this.settings, () => [
this.settingsManagerService.registerSettings<typeof settings>(() => [
{
key: 'plugin.connection-custom.disabled',
access: {
@@ -32,7 +32,7 @@ export class ConnectionSearchSettingsService extends Dependency {
}
private registerSettings() {
this.settingsManagerService.registerSettings(this.settings, () => [
this.settingsManagerService.registerSettings<typeof settings>(() => [
{
group: CONNECTIONS_SETTINGS_GROUP,
key: 'plugin.connection-search.disabled',
@@ -35,7 +35,7 @@ export class PluginConnectionsSettingsService extends Dependency {
private registerSettings() {
// todo: probably not working as a separate setting
this.settingsManagerService.registerSettings(this.settings, () => [
this.settingsManagerService.registerSettings<typeof defaultSettings>(() => [
{
key: 'plugin.connections.hideConnectionViewForUsers',
type: ESettingsValueType.Checkbox,
@@ -29,7 +29,7 @@ export class DataImportSettingsService {
) {
this.settings = this.settingsProviderService.createSettings(defaultSettings);
this.settingsManagerService.registerSettings(this.settings, () => [
this.settingsManagerService.registerSettings<typeof defaultSettings>(() => [
{
group: DATA_EDITOR_SETTINGS_GROUP,
key: 'plugin.data-import.disabled',
@@ -23,7 +23,8 @@ const defaultSettings = schema.object({
'plugin.data-spreadsheet.showDescriptionInHeader': schemaExtra.stringedBoolean().default(true),
});
export type DataGridSettings = schema.infer<typeof defaultSettings>;
export type DataGridSettingsSchema = typeof defaultSettings;
export type DataGridSettings = schema.infer<DataGridSettingsSchema>;
@injectable()
export class DataGridSettingsService extends Dependency {
@@ -35,7 +36,7 @@ export class DataGridSettingsService extends Dependency {
return this.settings.getValue('plugin.data-spreadsheet.showDescriptionInHeader');
}
readonly settings: SettingsProvider<typeof defaultSettings>;
readonly settings: SettingsProvider<DataGridSettingsSchema>;
constructor(
private readonly settingsProviderService: SettingsProviderService,
@@ -47,7 +48,7 @@ export class DataGridSettingsService extends Dependency {
this.settingsResolverService.addResolver(
ROOT_SETTINGS_LAYER,
/** @deprecated Use settings instead, will be removed in 23.0.0 */
createSettingsAliasResolver(this.settingsResolverService, this.settings, {
createSettingsAliasResolver<DataGridSettingsSchema>(this.settingsResolverService, {
'plugin.data-spreadsheet.hidden': 'plugin_data_spreadsheet_new.hidden',
}),
);
@@ -56,7 +57,7 @@ export class DataGridSettingsService extends Dependency {
}
private registerSettings() {
this.settingsManagerService.registerSettings(this.settings, () => [
this.settingsManagerService.registerSettings<typeof defaultSettings>(() => [
{
group: DATA_EDITOR_SETTINGS_GROUP,
key: 'plugin.data-spreadsheet.hidden',
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { Dependency, injectable } from '@cloudbeaver/core-di';
import { ServerSettingsManagerService } from '@cloudbeaver/core-root';
import { ServerSettingsManagerService, SettingsTransformationService } from '@cloudbeaver/core-root';
import {
createSettingsAliasResolver,
ESettingsValueType,
@@ -33,7 +33,8 @@ const defaultSettings = schema.object({
'plugin.data-viewer.export.disabled': schemaExtra.stringedBoolean().default(false),
});
export type DataViewerSettings = schema.infer<typeof defaultSettings>;
export type DataViewerSettingsSchema = typeof defaultSettings;
export type DataViewerSettings = schema.infer<DataViewerSettingsSchema>;
@injectable()
export class DataViewerSettingsService extends Dependency {
@@ -61,12 +62,13 @@ export class DataViewerSettingsService extends Dependency {
return this.settings.getValue('resultset.maxrows');
}
readonly settings: SettingsProvider<typeof defaultSettings>;
readonly settings: SettingsProvider<DataViewerSettingsSchema>;
constructor(
private readonly settingsProviderService: SettingsProviderService,
private readonly settingsManagerService: SettingsManagerService,
private readonly settingsResolverService: SettingsResolverService,
private readonly settingsTransformationService: SettingsTransformationService,
private readonly serverSettingsManagerService: ServerSettingsManagerService,
) {
super();
@@ -74,7 +76,7 @@ export class DataViewerSettingsService extends Dependency {
this.settingsResolverService.addResolver(
ROOT_SETTINGS_LAYER,
/** @deprecated Use settings instead, will be removed in 23.0.0 */
createSettingsAliasResolver(this.settingsResolverService, this.settings, {
createSettingsAliasResolver<DataViewerSettingsSchema>(this.settingsResolverService, {
'plugin.data-viewer.disableEdit': 'core.app.dataViewer.disableEdit',
'plugin.data-viewer.disableCopyData': 'core.app.dataViewer.disableCopyData',
'plugin.data-viewer.fetchMax': 'core.app.dataViewer.fetchMax',
@@ -82,11 +84,11 @@ export class DataViewerSettingsService extends Dependency {
'resultset.maxrows': 'core.app.dataViewer.fetchDefault',
}),
/** @deprecated Use settings instead, will be removed in 25.0.0 */
createSettingsAliasResolver(this.settingsResolverService, this.settings, {
createSettingsAliasResolver<DataViewerSettingsSchema>(this.settingsResolverService, {
'resultset.maxrows': 'plugin.data-viewer.fetchDefault',
}),
/** @deprecated Use settings instead, will be removed in 23.0.0 */
createSettingsAliasResolver(this.settingsResolverService, this.settings, {
createSettingsAliasResolver<DataViewerSettingsSchema>(this.settingsResolverService, {
'plugin.data-viewer.export.disabled': 'plugin_data_export.disabled',
}),
);
@@ -102,7 +104,7 @@ export class DataViewerSettingsService extends Dependency {
}
private registerSettings() {
this.serverSettingsManagerService.setSettingTransformer(
this.settingsTransformationService.setSettingTransformer(
'resultset.maxrows',
setting =>
({
@@ -113,7 +115,7 @@ export class DataViewerSettingsService extends Dependency {
}) as ISettingDescription<DataViewerSettings>,
);
this.settingsManagerService.registerSettings(this.settings, () => {
this.settingsManagerService.registerSettings<typeof defaultSettings>(() => {
const settings: ISettingDescription<DataViewerSettings>[] = [
{
key: 'plugin.data-viewer.disableEdit',
@@ -24,7 +24,8 @@ const defaultSettings = schema.object({
'plugin.log-viewer.disabled': schemaExtra.stringedBoolean().default(false),
});
export type LogViewerSettings = schema.infer<typeof defaultSettings>;
export type LogViewerSettingsSchema = typeof defaultSettings;
export type LogViewerSettings = schema.infer<LogViewerSettingsSchema>;
@injectable()
export class LogViewerSettingsService extends Dependency {
@@ -40,7 +41,7 @@ export class LogViewerSettingsService extends Dependency {
return this.settings.getValue('plugin.log-viewer.logBatchSize');
}
readonly settings: SettingsProvider<typeof defaultSettings>;
readonly settings: SettingsProvider<LogViewerSettingsSchema>;
constructor(
private readonly settingsProviderService: SettingsProviderService,
@@ -52,7 +53,7 @@ export class LogViewerSettingsService extends Dependency {
this.settingsResolverService.addResolver(
ROOT_SETTINGS_LAYER,
/** @deprecated Use settings instead, will be removed in 23.0.0 */
createSettingsAliasResolver(this.settingsResolverService, this.settings, {
createSettingsAliasResolver<LogViewerSettingsSchema>(this.settingsResolverService, {
'plugin.log-viewer.disabled': 'core.app.logViewer.disabled',
'plugin.log-viewer.logBatchSize': 'core.app.logViewer.logBatchSize',
'plugin.log-viewer.maxLogRecords': 'core.app.logViewer.maxLogRecords',
@@ -63,7 +64,7 @@ export class LogViewerSettingsService extends Dependency {
}
private registerSettings() {
this.settingsManagerService.registerSettings(this.settings, () => [
this.settingsManagerService.registerSettings<typeof defaultSettings>(() => [
// {
// group: LOG_VIEWER_SETTINGS_GROUP,
// key: 'plugin.log-viewer.maxLogRecords',
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { Dependency, injectable } from '@cloudbeaver/core-di';
import { injectable } from '@cloudbeaver/core-di';
import {
ESettingsValueType,
INTERFACE_SETTINGS_GROUP,
@@ -23,7 +23,7 @@ const settings = schema.object({
type Settings = typeof settings;
@injectable()
export class NavigationTabsSettingsService extends Dependency {
export class NavigationTabsSettingsService {
readonly settings: SettingsProvider<Settings>;
get hasMultipleRows(): boolean {
@@ -34,14 +34,11 @@ export class NavigationTabsSettingsService extends Dependency {
private readonly settingsProviderService: SettingsProviderService,
private readonly settingsManagerService: SettingsManagerService,
) {
super();
this.settings = this.settingsProviderService.createSettings(settings);
this.registerSettings();
}
private registerSettings() {
this.settingsManagerService.registerSettings(this.settings, () => [
registerSettingsUI() {
this.settingsManagerService.registerSettings<Settings>(() => [
{
group: INTERFACE_SETTINGS_GROUP,
key: 'plugin.navigation-tabs.multipleRows',
@@ -1,27 +1,29 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 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 React from 'react';
import { AppScreenService } from '@cloudbeaver/core-app';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { NavigationTabsSettingsService } from './NavigationTabs/NavigationTabsSettingsService.js';
import { importLazyComponent } from '@cloudbeaver/core-blocks';
const NavigationTabsBar = React.lazy(async () => {
const { NavigationTabsBar } = await import('./NavigationTabs/NavigationTabsBar/index.js');
return { default: NavigationTabsBar };
});
const NavigationTabsBar = importLazyComponent(() => import('./NavigationTabs/NavigationTabsBar/index.js').then(m => m.NavigationTabsBar));
@injectable()
export class PluginBootstrap extends Bootstrap {
constructor(private readonly appScreenService: AppScreenService) {
constructor(
private readonly appScreenService: AppScreenService,
private readonly navigationTabsSettingsService: NavigationTabsSettingsService,
) {
super();
}
override register(): void | Promise<void> {
this.appScreenService.rightAreaTop.add(NavigationTabsBar);
this.navigationTabsSettingsService.registerSettingsUI();
}
}
@@ -41,7 +41,7 @@ export class NavigationTreeSettingsService extends Dependency {
}
private registerSettings() {
this.settingsManagerService.registerSettings(this.settings, () => [
this.settingsManagerService.registerSettings<typeof defaultSettings>(() => [
{
group: NAVIGATION_TREE_SETTINGS_GROUP,
key: 'plugin.navigation-tree.disabled',
@@ -35,7 +35,7 @@ export class ResourceManagerScriptsSettingsService extends Dependency {
}
private registerSettings() {
this.settingsManagerService.registerSettings(this.settings, () => [
this.settingsManagerService.registerSettings<Settings>(() => [
{
group: RESOURCE_MANAGER_SETTINGS_GROUP,
key: 'plugin.resource-manager-scripts.disabled',
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -19,14 +19,15 @@ const defaultSettings = schema.object({
'plugin.resource-manager.disabled': schemaExtra.stringedBoolean().default(false), //! use resourceManagerEnabled in server config instead
});
export type ResourceManagerSettings = schema.infer<typeof defaultSettings>;
export type ResourceManagerSettingsSchema = typeof defaultSettings;
export type ResourceManagerSettings = schema.infer<ResourceManagerSettingsSchema>;
@injectable()
export class ResourceManagerSettingsService extends Dependency {
get disabled(): boolean {
return this.settings.getValue('plugin.resource-manager.disabled');
}
readonly settings: SettingsProvider<typeof defaultSettings>;
readonly settings: SettingsProvider<ResourceManagerSettingsSchema>;
constructor(
private readonly settingsProviderService: SettingsProviderService,
@@ -37,7 +38,7 @@ export class ResourceManagerSettingsService extends Dependency {
this.settingsResolverService.addResolver(
ROOT_SETTINGS_LAYER,
/** @deprecated Use settings instead, will be removed in 23.0.0 */
createSettingsAliasResolver(this.settingsResolverService, this.settings, {
createSettingsAliasResolver<ResourceManagerSettingsSchema>(this.settingsResolverService, {
'plugin.resource-manager.disabled': 'plugin_resource_manager.disabled',
}),
);
@@ -8,15 +8,27 @@
import { observer } from 'mobx-react-lite';
import type { AdministrationItemContentProps } from '@cloudbeaver/core-administration';
import { ColoredContainer, Form, Group, ToolsAction, ToolsPanel, useForm, useTranslate, getComputed, TextPlaceholder } from '@cloudbeaver/core-blocks';
import {
ColoredContainer,
Form,
Group,
ToolsAction,
ToolsPanel,
useForm,
useTranslate,
getComputed,
TextPlaceholder,
} from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { Settings } from '@cloudbeaver/plugin-settings-panel';
import { TabList, TabsState, type ITabData } from '@cloudbeaver/core-ui';
import { SettingsAdministrationService } from './SettingsAdministrationService.js';
import { useState } from 'react';
import { SettingsResolverService } from '@cloudbeaver/core-settings';
export const SettingsAdministration = observer<AdministrationItemContentProps>(function SettingsAdministration() {
const settingsResolverService = useService(SettingsResolverService);
const settingsAdministrationService = useService(SettingsAdministrationService);
const notificationService = useService(NotificationService);
const [tabId, setTabId] = useState<string | null>(null);
@@ -86,7 +98,11 @@ export const SettingsAdministration = observer<AdministrationItemContentProps>(f
<Group hidden={hideSingleTab} box keepSize hideOverflow>
<TabList underline />
</Group>
{settingsSource ? <Settings source={settingsSource} accessor={accessor} /> : <TextPlaceholder>{translate('plugin_settings_administration_no_settings')}</TextPlaceholder>}
{settingsSource ? (
<Settings resolver={settingsResolverService} source={settingsSource} accessor={accessor} />
) : (
<TextPlaceholder>{translate('plugin_settings_administration_no_settings')}</TextPlaceholder>
)}
</ColoredContainer>
</Form>
</TabsState>
@@ -6,11 +6,11 @@
* you may not use this file except in compliance with the License.
*/
import { injectable } from '@cloudbeaver/core-di';
import type { ISettingsSource } from '@cloudbeaver/core-settings';
import type { IEditableSettingsSource } from '@cloudbeaver/core-settings';
import { TabsContainer } from '@cloudbeaver/core-ui';
export interface IAdministrationSettingsOptions {
source: ISettingsSource;
source: IEditableSettingsSource;
accessor: string[];
}
@@ -9,32 +9,37 @@ import { observer } from 'mobx-react-lite';
import { Link, useTranslate } from '@cloudbeaver/core-blocks';
import { clsx } from '@dbeaver/ui-kit';
import { type ISettingDescription, type ISettingsSource } from '@cloudbeaver/core-settings';
import { SettingsResolverSource, type ISettingDescription, type IEditableSettingsSource } from '@cloudbeaver/core-settings';
import { SettingField } from './SettingField.js';
interface Props {
source: ISettingsSource;
resolver: SettingsResolverSource;
source: IEditableSettingsSource;
setting: ISettingDescription;
displayRestore?: boolean;
}
export const Setting = observer<Props>(function Setting({ source, setting }) {
export const Setting = observer<Props>(function Setting({ resolver, source, setting, displayRestore }) {
const translate = useTranslate();
// DODO: hide this logic until we have more than one scope
const isOverride = source.has(setting.key) && source.getEditedValue(setting.key) !== null && false;
const isOverride = source.has(setting.key) && source.getEditedValue(setting.key) !== null && displayRestore;
function handleRestore() {
source.setValue(setting.key, null);
source.resetValue(setting.key);
}
return (
<div className='tw:flex tw:relative tw:gap-2'>
<div className="tw:w-1 tw:h-full" hidden>
<div className="tw:flex tw:relative tw:gap-2">
<div className="tw:w-1 tw:h-full" hidden={!displayRestore}>
{isOverride && (
<div className={clsx('tw:h-full tw:w-full tw:bg-zinc-100 tw:dark:bg-zinc-700')} title={translate('plugin_settings_panel_setting_set_in_scope')} />
<div
className={clsx('tw:h-full tw:w-full tw:bg-zinc-100 tw:dark:bg-zinc-700')}
title={translate('plugin_settings_panel_setting_set_in_scope')}
/>
)}
</div>
<div>
<SettingField setting={setting} source={source} />
<SettingField resolver={resolver} setting={setting} source={source} />
{isOverride && (
<Link className="theme-typography--caption" title={translate('plugin_settings_panel_setting_reset_tooltip')} onClick={handleRestore}>
{translate('plugin_settings_panel_setting_reset')}
@@ -11,21 +11,21 @@ import { useService } from '@cloudbeaver/core-di';
import {
ESettingsValueType,
SettingsProviderService,
SettingsResolverService,
SettingsResolverSource,
type ISettingDescription,
type ISettingsSource,
type IEditableSettingsSource,
} from '@cloudbeaver/core-settings';
import { schemaValidationError } from '@cloudbeaver/core-utils';
import { isNotNullDefined } from '@dbeaver/js-helpers';
import { observer } from 'mobx-react-lite';
interface Props {
source: ISettingsSource;
resolver: SettingsResolverSource;
source: IEditableSettingsSource;
setting: ISettingDescription;
}
export const SettingField = observer<Props>(function SettingField({ setting, source }) {
const settingsResolverService = useService(SettingsResolverService);
export const SettingField = observer<Props>(function SettingField({ resolver, setting, source }) {
const settingsProviderService = useService(SettingsProviderService);
const translate = useTranslate();
@@ -39,7 +39,7 @@ export const SettingField = observer<Props>(function SettingField({ setting, sou
let value = source.getEditedValue(setting.key);
if (readOnly || !isNotNullDefined(value)) {
value = settingsResolverService.getEditedValue(setting.key);
value = resolver.getEditedValue(setting.key);
}
if (setting.key in settingsProviderService.schema.shape) {
@@ -8,8 +8,8 @@
import { useId, useState } from 'react';
import { observer } from 'mobx-react-lite';
import { Container, Filter, Group, s, TextPlaceholder, useTranslate } from '@cloudbeaver/core-blocks';
import { type ISettingsSource, ROOT_SETTINGS_GROUP, SettingsGroup } from '@cloudbeaver/core-settings';
import { Container, Filter, getComputed, Group, s, TextPlaceholder, useTranslate } from '@cloudbeaver/core-blocks';
import { type IEditableSettingsSource, ROOT_SETTINGS_GROUP, SettingsGroup, SettingsResolverSource } from '@cloudbeaver/core-settings';
import { useTreeData, useTreeFilter } from '@cloudbeaver/plugin-navigation-tree';
import { SyncExecutor } from '@cloudbeaver/core-executor';
@@ -20,11 +20,14 @@ import { SettingsList } from './SettingsList.js';
import { useSettings } from './useSettings.js';
export interface ISettingsProps {
source: ISettingsSource;
resolver: SettingsResolverSource;
source: IEditableSettingsSource;
accessor?: string[];
hideGroupsSettingsLimit?: number;
displayRestore?: boolean;
}
export const Settings = observer<ISettingsProps>(function Settings({ source, accessor }) {
export const Settings = observer<ISettingsProps>(function Settings({ resolver, source, accessor, hideGroupsSettingsLimit = 0, displayRestore }) {
const translate = useTranslate();
const settingsId = useId();
const settings = useSettings(accessor);
@@ -78,13 +81,15 @@ export const Settings = observer<ISettingsProps>(function Settings({ source, acc
groupSelectExecutor.execute(id);
}
const isGroupsHidden = getComputed(() => [...settings.settings.values()].flat().length <= hideGroupsSettingsLimit);
return (
<Container gap overflow noWrap>
<Group className={s(classes, { settingsGroups: true })} vertical box keepSize overflow>
<Group className={s(classes, { settingsGroups: true })} hidden={isGroupsHidden} vertical box keepSize overflow>
<SettingsGroups treeData={treeData} onClick={handleClick} />
</Group>
<Container className={s(classes, { settingsContainer: true })} overflow vertical gap noWrap>
<Container gap keepSize>
<Container hidden={isGroupsHidden} gap keepSize>
<Filter
state={treeFilter}
name="filter"
@@ -97,9 +102,12 @@ export const Settings = observer<ISettingsProps>(function Settings({ source, acc
settingsId={settingsId}
treeData={treeData}
treeFilter={treeFilter}
resolver={resolver}
source={source}
settings={settings.settings}
groupsHidden={isGroupsHidden}
groupSelectExecutor={groupSelectExecutor}
displayRestore={displayRestore}
/>
</Container>
</Container>
@@ -8,7 +8,12 @@
import { observer } from 'mobx-react-lite';
import { getComputed, Group, GroupTitle, useExecutor, useTranslate } from '@cloudbeaver/core-blocks';
import type { ISettingDescription, ISettingsSource, SettingsGroup as SettingsGroupType } from '@cloudbeaver/core-settings';
import type {
IEditableSettingsSource,
ISettingDescription,
SettingsGroup as SettingsGroupType,
SettingsResolverSource,
} from '@cloudbeaver/core-settings';
import { isArraysEqual } from '@cloudbeaver/core-utils';
import type { ITreeFilter } from '@cloudbeaver/plugin-navigation-tree';
@@ -22,13 +27,26 @@ import { getSettingGroupId } from './getSettingGroupId.js';
interface Props {
settingsId: string;
group: SettingsGroupType;
source: ISettingsSource;
resolver: SettingsResolverSource;
source: IEditableSettingsSource;
settings: Map<SettingsGroupType, ISettingDescription<any>[]>;
treeFilter: ITreeFilter;
groupsHidden?: boolean;
displayRestore?: boolean;
groupSelectExecutor: ISyncExecutor<string>;
}
export const SettingsGroup = observer<Props>(function SettingsGroup({ settingsId, group, source, settings, treeFilter, groupSelectExecutor }) {
export const SettingsGroup = observer<Props>(function SettingsGroup({
settingsId,
group,
resolver,
source,
settings,
treeFilter,
groupsHidden,
displayRestore,
groupSelectExecutor,
}) {
const ref = useRef<HTMLDivElement>(null);
const translate = useTranslate();
const groupSettings = getComputed(() => settings.get(group)?.filter(settingsFilter(translate, treeFilter.filter)) || [], isArraysEqual);
@@ -53,12 +71,12 @@ export const SettingsGroup = observer<Props>(function SettingsGroup({ settingsId
});
return (
<Group ref={ref} id={getSettingGroupId(settingsId, group.id)} hidden={hidden} vertical gap compact>
<GroupTitle sticky>
<Group ref={ref} id={getSettingGroupId(settingsId, group.id)} hidden={hidden} dense={groupsHidden} vertical gap compact>
<GroupTitle hidden={groupsHidden} sticky>
<SettingsGroupTitle group={group} />
</GroupTitle>
{groupSettings.map((setting, i) => (
<Setting key={i} source={source} setting={setting} />
{groupSettings.map(setting => (
<Setting key={setting.key} resolver={resolver} source={source} setting={setting} displayRestore={displayRestore} />
))}
</Group>
);
@@ -8,7 +8,12 @@
import { observer } from 'mobx-react-lite';
import { Group, s, TextPlaceholder, useTranslate } from '@cloudbeaver/core-blocks';
import { type ISettingDescription, type ISettingsSource, type SettingsGroup as SettingsGroupType } from '@cloudbeaver/core-settings';
import {
SettingsResolverSource,
type IEditableSettingsSource,
type ISettingDescription,
type SettingsGroup as SettingsGroupType,
} from '@cloudbeaver/core-settings';
import type { ITreeData, ITreeFilter } from '@cloudbeaver/plugin-navigation-tree';
import { getGroupsFromTree } from './getGroupsFromTree.js';
@@ -21,9 +26,12 @@ interface Props {
settingsId: string;
treeData: ITreeData;
treeFilter: ITreeFilter;
source: ISettingsSource;
source: IEditableSettingsSource;
resolver: SettingsResolverSource;
settings: Map<SettingsGroupType, ISettingDescription<any>[]>;
groupSelectExecutor: ISyncExecutor<string>;
groupsHidden?: boolean;
displayRestore?: boolean;
onSettingsOpen?: (groupId: string) => void;
}
@@ -31,8 +39,11 @@ export const SettingsList = observer<Props>(function SettingsList({
settingsId,
treeData,
treeFilter,
resolver,
source,
settings,
groupsHidden,
displayRestore,
groupSelectExecutor,
onSettingsOpen,
}) {
@@ -47,10 +58,13 @@ export const SettingsList = observer<Props>(function SettingsList({
key={group.id}
settingsId={settingsId}
group={group}
resolver={resolver}
source={source}
settings={settings}
treeFilter={treeFilter}
groupsHidden={groupsHidden}
groupSelectExecutor={groupSelectExecutor}
displayRestore={displayRestore}
/>
))}
{groups.length === 0 && <TextPlaceholder>{translate('plugin_settings_panel_no_settings')}</TextPlaceholder>}
@@ -4,6 +4,6 @@ export default [
['plugin_settings_panel_no_settings', 'No settings found'],
['plugin_settings_panel_group_empty', 'No settings available'],
['plugin_settings_panel_setting_reset', 'Reset'],
['plugin_settings_panel_setting_reset_tooltip', 'Reset to default value'],
['plugin_settings_panel_setting_reset_tooltip', 'Reset to default'],
['plugin_settings_panel_setting_set_in_scope', 'The setting has been configured in the current scope.'],
];
@@ -4,6 +4,6 @@ export default [
['plugin_settings_panel_no_settings', 'No settings found'],
['plugin_settings_panel_group_empty', 'No settings available'],
['plugin_settings_panel_setting_reset', 'Reset'],
['plugin_settings_panel_setting_reset_tooltip', 'Reset to default value'],
['plugin_settings_panel_setting_reset_tooltip', 'Reset to default'],
['plugin_settings_panel_setting_set_in_scope', 'The setting has been configured in the current scope.'],
];
@@ -4,6 +4,6 @@ export default [
['plugin_settings_panel_no_settings', 'No settings found'],
['plugin_settings_panel_group_empty', 'No settings available'],
['plugin_settings_panel_setting_reset', 'Reset'],
['plugin_settings_panel_setting_reset_tooltip', 'Reset to default value'],
['plugin_settings_panel_setting_reset_tooltip', 'Reset to default'],
['plugin_settings_panel_setting_set_in_scope', 'The setting has been configured in the current scope.'],
];
@@ -4,6 +4,6 @@ export default [
['plugin_settings_panel_no_settings', 'No settings found'],
['plugin_settings_panel_group_empty', 'No settings available'],
['plugin_settings_panel_setting_reset', 'Reset'],
['plugin_settings_panel_setting_reset_tooltip', 'Reset to default value'],
['plugin_settings_panel_setting_reset_tooltip', 'Reset to default'],
['plugin_settings_panel_setting_set_in_scope', 'The setting has been configured in the current scope.'],
];
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -30,6 +30,6 @@ export class ResourceEditorSettingsService extends Dependency {
}
private registerSettings() {
this.settingsManagerService.registerSettings(this.settings, () => []);
this.settingsManagerService.registerSettings<ResourceEditorSettings>(() => []);
}
}
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -31,12 +31,13 @@ const QUERY_GUTTER_EFFECT = StateEffect.define<{ pos: number; on: boolean; type:
map: (val, mapping) => ({ pos: mapping.mapPos(val.pos), on: val.on, type: val.type }),
});
const gutterExtension = StateField.define<RangeSet<GutterMarker>>({
const statusGutterState = StateField.define<RangeSet<GutterMarker>>({
create() {
return RangeSet.empty;
},
update(set, transaction) {
set = set.map(transaction.changes);
for (const effect of transaction.effects) {
if (effect.is(QUERY_GUTTER_EFFECT)) {
if (effect.value.on) {
@@ -68,10 +69,10 @@ export function setGutter(view: EditorView, pos: number, type: QueryGutterEffect
}
export const QUERY_STATUS_GUTTER_EXTENSION = [
gutterExtension,
statusGutterState,
gutter({
class: 'query-status',
markers: view => view.state.field(gutterExtension),
markers: view => view.state.field(statusGutterState),
initialSpacer: () => QUERY_STATUS_SIZE_MARKER,
}),
];
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -159,7 +159,7 @@ export class MenuBootstrap extends Bootstrap {
}
if (
!sqlEditorData.dataSource?.hasFeature(ESqlDataSourceFeatures.executable) &&
!sqlEditorData.isExecutionAllowed &&
[
ACTION_SQL_EDITOR_EXECUTE,
ACTION_SQL_EDITOR_EXECUTE_NEW,
@@ -207,7 +207,7 @@ export class MenuBootstrap extends Bootstrap {
contexts: [DATA_CONTEXT_SQL_EDITOR_DATA],
isBindingApplicable: (contexts, action) => {
const sqlEditorData = contexts.get(DATA_CONTEXT_SQL_EDITOR_DATA);
return action === ACTION_SQL_EDITOR_EXECUTE_SCRIPT && sqlEditorData?.dataSource?.hasFeature(ESqlDataSourceFeatures.executable) === true;
return action === ACTION_SQL_EDITOR_EXECUTE_SCRIPT && sqlEditorData?.isExecutionAllowed === true;
},
handler: this.sqlEditorActionHandler.bind(this),
});
@@ -73,16 +73,16 @@ export abstract class BaseSqlDataSource implements ISqlDataSource {
return this.executionContext?.projectId ?? null;
}
get features(): ESqlDataSourceFeatures[] {
return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable];
}
readonly icon: string;
readonly history: ISqlDataSourceHistory;
readonly onUpdate: ISyncExecutor;
readonly onSetScript: ISyncExecutor<ISetScriptData>;
readonly onDatabaseModelUpdate: ISyncExecutor<IDatabaseDataModel<QueryDataSource>[]>;
protected get features(): ESqlDataSourceFeatures[] {
return [ESqlDataSourceFeatures.script, ESqlDataSourceFeatures.query, ESqlDataSourceFeatures.executable];
}
protected outdated: boolean;
protected editing: boolean;
protected innerCursorState: ISqlEditorCursor;
@@ -46,8 +46,6 @@ export interface ISqlDataSource extends ILoadableState {
readonly databaseModels: IDatabaseDataModel<QueryDataSource>[];
readonly executionContext?: IConnectionExecutionContextInfo;
readonly features: ESqlDataSourceFeatures[];
readonly isAutoSaveEnabled: boolean;
readonly isIncomingChanges: boolean;
readonly isSaved: boolean;
@@ -32,6 +32,7 @@ export interface ISQLEditorData {
readonly isIncomingChanges: boolean;
readonly value: string;
readonly incomingValue?: string;
readonly isExecutionAllowed: boolean;
readonly dataSource: ISqlDataSource | undefined;
readonly onExecute: ISyncExecutor<boolean>;
readonly onSegmentExecute: ISyncExecutor<ISegmentExecutionData>;
@@ -1,6 +1,6 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2024 DBeaver Corp and others
* Copyright (C) 2020-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
@@ -28,7 +28,7 @@ export const SQLEditorActions = observer<Props>(function SQLEditorActions({ data
const isActiveSegmentMode = getComputed(() => data.activeSegmentMode.activeSegmentMode);
const disabled = getComputed(() => data.isScriptEmpty || data.isDisabled);
const isQuery = data.dataSource?.hasFeature(ESqlDataSourceFeatures.query);
const isExecutable = data.dataSource?.hasFeature(ESqlDataSourceFeatures.executable);
const isExecutable = data.isExecutionAllowed;
return (
<div className={s(styles, { container: true })}>
@@ -30,6 +30,7 @@ import { SqlQueryService } from '../SqlResultTabs/SqlQueryService.js';
import { SqlResultTabsService } from '../SqlResultTabs/SqlResultTabsService.js';
import type { ISQLEditorData } from './ISQLEditorData.js';
import { SQLEditorModeContext } from './SQLEditorModeContext.js';
import { SqlEditorSettingsService } from '../SqlEditorSettingsService.js';
interface ISQLEditorDataPrivate extends ISQLEditorData {
readonly sqlDialectInfoService: SqlDialectInfoService;
@@ -38,6 +39,7 @@ interface ISQLEditorDataPrivate extends ISQLEditorData {
readonly sqlEditorService: SqlEditorService;
readonly notificationService: NotificationService;
readonly sqlExecutionPlanService: SqlExecutionPlanService;
readonly sqlEditorSettingsService: SqlEditorSettingsService;
readonly commonDialogService: CommonDialogService;
readonly sqlResultTabsService: SqlResultTabsService;
readonly dataSource: ISqlDataSource | undefined;
@@ -68,6 +70,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
const sqlResultTabsService = useService(SqlResultTabsService);
const commonDialogService = useService(CommonDialogService);
const sqlDataSourceService = useService(SqlDataSourceService);
const sqlEditorSettingsService = useService(SqlEditorSettingsService);
const data = useObservableRef<ISQLEditorDataPrivate>(
() => ({
@@ -134,6 +137,10 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
return this.dataSource?.incomingScript;
},
get isExecutionAllowed(): boolean {
return !!this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable) && this.sqlEditorSettingsService.scriptExecutionEnabled;
},
onMode: new SyncExecutor(),
onExecute: new SyncExecutor(),
onSegmentExecute: new SyncExecutor(),
@@ -230,9 +237,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
async executeQuery(): Promise<void> {
const isQuery = this.dataSource?.hasFeature(ESqlDataSourceFeatures.query);
const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable);
if (!isQuery || !isExecutable) {
if (!isQuery || !this.isExecutionAllowed) {
return;
}
@@ -263,9 +269,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
async executeQueryNewTab(): Promise<void> {
const isQuery = this.dataSource?.hasFeature(ESqlDataSourceFeatures.query);
const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable);
if (!isQuery || !isExecutable) {
if (!isQuery || !this.isExecutionAllowed) {
return;
}
@@ -281,9 +286,8 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
async showExecutionPlan(): Promise<void> {
const isQuery = this.dataSource?.hasFeature(ESqlDataSourceFeatures.query);
const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable);
if (!isQuery || !isExecutable || !this.dialect?.supportsExplainExecutionPlan) {
if (!isQuery || !this.isExecutionAllowed || !this.dialect?.supportsExplainExecutionPlan) {
return;
}
@@ -302,9 +306,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
},
async executeScript(): Promise<void> {
const isExecutable = this.dataSource?.hasFeature(ESqlDataSourceFeatures.executable);
if (!isExecutable || this.isDisabled || this.isScriptEmpty) {
if (!this.isExecutionAllowed || this.isDisabled || this.isScriptEmpty) {
return;
}
@@ -410,13 +412,14 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
this.onExecute.execute(true);
const id = setTimeout(() => this.onSegmentExecute.execute({ segment, type: 'start' }), 250);
try {
const id = setTimeout(() => this.onSegmentExecute.execute({ segment, type: 'start' }), 250);
const result = await action(segment);
clearTimeout(id);
this.onSegmentExecute.execute({ segment, type: 'end' });
return result;
} catch (exception: any) {
clearTimeout(id);
this.onSegmentExecute.execute({ segment, type: 'end' });
this.onSegmentExecute.execute({ segment, type: 'error' });
throw exception;
@@ -499,6 +502,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
hintsLimitIsMet: observable.ref,
readonlyState: observable,
executingScript: observable,
sqlEditorSettingsService: observable.ref,
},
{
state,
@@ -510,6 +514,7 @@ export function useSqlEditor(state: ISqlEditorTabState): ISQLEditorData {
sqlResultTabsService,
notificationService,
commonDialogService,
sqlEditorSettingsService,
},
);
@@ -6,9 +6,16 @@
* you may not use this file except in compliance with the License.
*/
import { Dependency, injectable } from '@cloudbeaver/core-di';
import { FEATURE_GIT_ID, ServerConfigResource, ServerSettingsManagerService } from '@cloudbeaver/core-root';
import {
FEATURE_GIT_ID,
HIGHEST_SETTINGS_LAYER,
ServerConfigResource,
ServerSettingsManagerService,
SettingsTransformationService,
} from '@cloudbeaver/core-root';
import {
createSettingsAliasResolver,
createSettingsOverrideResolver,
ESettingsValueType,
type ISettingDescription,
ROOT_SETTINGS_LAYER,
@@ -45,6 +52,7 @@ const ASSISTANT_MODE_OPTIONS_LOCALIZED = [
];
const defaultSettings = schema.object({
'plugin.sql-editor.script.executionEnabled': schemaExtra.stringedBoolean().default(true),
'plugin.sql-editor.maxFileSize': schema.coerce.number().default(10 * 1024), // kilobyte
'plugin.sql-editor.disabled': schemaExtra.stringedBoolean().default(false),
'plugin.sql-editor.autoSave': schemaExtra.stringedBoolean().default(true),
@@ -63,20 +71,27 @@ const defaultSettings = schema.object({
.pipe(schema.enum(TABLE_ALIAS_OPTIONS))
.default('PLAIN'),
'SQLEditor.ContentAssistant.proposals.long.name': schema.coerce.boolean().default(false),
'SQLEditor.ContentAssistant.experimental.mode': schema.coerce.string().transform(value => {
switch (value) {
case 'DEFAULT':
return 'DEFAULT';
default:
return 'NEW';
}
}).pipe(schema.enum(ASSISTANT_MODE_OPTIONS).default('NEW')),
'SQLEditor.ContentAssistant.experimental.mode': schema.coerce
.string()
.transform(value => {
switch (value) {
case 'DEFAULT':
return 'DEFAULT';
default:
return 'NEW';
}
})
.pipe(schema.enum(ASSISTANT_MODE_OPTIONS).default('NEW')),
});
export type SqlEditorSettings = schema.infer<typeof defaultSettings>;
type SqlEditorSettingsSchema = typeof defaultSettings;
export type SqlEditorSettings = schema.infer<SqlEditorSettingsSchema>;
@injectable()
export class SqlEditorSettingsService extends Dependency {
get scriptExecutionEnabled(): boolean {
return this.settings.getValue('plugin.sql-editor.script.executionEnabled');
}
get maxFileSize(): number {
return this.settings.getValue('plugin.sql-editor.maxFileSize');
}
@@ -103,6 +118,7 @@ export class SqlEditorSettingsService extends Dependency {
private readonly settingsProviderService: SettingsProviderService,
private readonly settingsManagerService: SettingsManagerService,
private readonly settingsResolverService: SettingsResolverService,
private readonly settingsTransformationService: SettingsTransformationService,
private readonly serverSettingsManagerService: ServerSettingsManagerService,
private readonly serverConfigResource: ServerConfigResource,
) {
@@ -111,18 +127,27 @@ export class SqlEditorSettingsService extends Dependency {
this.settingsResolverService.addResolver(
ROOT_SETTINGS_LAYER,
/** @deprecated Use settings instead, will be removed in 23.0.0 */
createSettingsAliasResolver(this.settingsResolverService, this.settings, {
createSettingsAliasResolver<SqlEditorSettingsSchema>(this.settingsResolverService, {
'plugin.sql-editor.autoSave': 'core.app.sqlEditor.autoSave',
'plugin.sql-editor.maxFileSize': 'core.app.sqlEditor.maxFileSize',
'plugin.sql-editor.disabled': 'core.app.sqlEditor.disabled',
}),
);
this.settingsResolverService.addResolver(
HIGHEST_SETTINGS_LAYER,
createSettingsOverrideResolver<SqlEditorSettingsSchema>(this.settingsResolverService, {
'plugin.sql-editor.script.executionEnabled': {
key: 'permission.sql.script.execution',
filter: value => !value,
},
}),
);
this.registerSettings();
}
private registerSettings() {
this.serverSettingsManagerService.setGroupOverride('editors/sqlEditor', SQL_EDITOR_SETTINGS_GROUP);
this.serverSettingsManagerService.setSettingTransformer(
this.settingsTransformationService.setGroupOverride('editors/sqlEditor', SQL_EDITOR_SETTINGS_GROUP);
this.settingsTransformationService.setSettingTransformer(
'sql.proposals.insert.table.alias',
setting =>
({
@@ -133,20 +158,19 @@ export class SqlEditorSettingsService extends Dependency {
options: [...(setting.options?.filter(option => !TABLE_ALIAS_OPTIONS.includes(option.value as any)) || []), ...TABLE_ALIAS_SETTING_OPTIONS],
}) as ISettingDescription<SqlEditorSettings>,
);
this.serverSettingsManagerService.setSettingTransformer(
this.settingsTransformationService.setSettingTransformer(
'SQLEditor.ContentAssistant.experimental.mode',
setting => {
return {
setting =>
({
...setting,
group: SQL_EDITOR_SETTINGS_GROUP,
name: 'sql_editor_settings_content_assistant_experimental_mode_name',
description: 'sql_editor_settings_content_assistant_experimental_mode_desc',
options: ASSISTANT_MODE_OPTIONS_LOCALIZED,
} as ISettingDescription<SqlEditorSettings>;
},
)
}) as ISettingDescription<SqlEditorSettings>,
);
this.settingsManagerService.registerSettings(this.settings, () => {
this.settingsManagerService.registerSettings<typeof defaultSettings>(() => {
const settings: ISettingDescription<SqlEditorSettings>[] = [
{
group: SQL_EDITOR_SETTINGS_GROUP,
@@ -204,7 +228,7 @@ export class SqlEditorSettingsService extends Dependency {
group: SQL_EDITOR_SETTINGS_GROUP,
type: ESettingsValueType.Select,
name: 'sql_editor_settings_content_assistant_experimental_mode_name',
options: ASSISTANT_MODE_OPTIONS_LOCALIZED
options: ASSISTANT_MODE_OPTIONS_LOCALIZED,
});
}
return settings;
@@ -21,6 +21,7 @@ import { OUTPUT_LOGS_MENU } from './OUTPUT_LOGS_MENU.js';
import { OUTPUT_LOGS_SETTINGS_MENU } from './OUTPUT_LOGS_SETTINGS_MENU.js';
import { OutputLogsService } from './OutputLogsService.js';
import { ACTION_SQL_EDITOR_CLEAR_OUTPUT_LOGS } from './ACTION_SQL_EDITOR_CLEAR_OUTPUT_LOGS.js';
import { DATA_CONTEXT_SQL_EDITOR_DATA } from '../../SqlEditor/DATA_CONTEXT_SQL_EDITOR_DATA.js';
@injectable()
export class OutputMenuBootstrap extends Bootstrap {
@@ -120,13 +121,14 @@ export class OutputMenuBootstrap extends Bootstrap {
this.actionService.addHandler({
id: 'output-logs-handler',
actions: [ACTION_SHOW_OUTPUT_LOGS, ACTION_SQL_EDITOR_CLEAR_OUTPUT_LOGS],
contexts: [DATA_CONTEXT_SQL_EDITOR_STATE],
contexts: [DATA_CONTEXT_SQL_EDITOR_STATE, DATA_CONTEXT_SQL_EDITOR_DATA],
isActionApplicable: (context): boolean => {
const state = context.get(DATA_CONTEXT_SQL_EDITOR_STATE)!;
const sqlEditorData = context.get(DATA_CONTEXT_SQL_EDITOR_DATA);
const sqlDataSource = this.sqlDataSourceService.get(state.editorId);
const isQuery = sqlDataSource?.hasFeature(ESqlDataSourceFeatures.query);
const isExecutable = sqlDataSource?.hasFeature(ESqlDataSourceFeatures.executable);
const isExecutable = sqlEditorData?.isExecutionAllowed;
return !!isQuery && !!isExecutable;
},
@@ -27,6 +27,8 @@ import type { IResultGroup, ISqlEditorTabState } from '../ISqlEditorTabState.js'
import { QueryDataSource } from '../QueryDataSource.js';
import { SqlDataSourceService } from '../SqlDataSource/SqlDataSourceService.js';
import { SqlQueryResultService } from './SqlQueryResultService.js';
import { SqlEditorSettingsService } from '../SqlEditorSettingsService.js';
import { ESqlDataSourceFeatures } from '../SqlDataSource/ESqlDataSourceFeatures.js';
import { Executor, type IExecutor } from '@cloudbeaver/core-executor';
interface IQueryExecutionOptions {
@@ -60,6 +62,7 @@ export class SqlQueryService {
private readonly dataViewerService: DataViewerService,
private readonly sqlDataSourceService: SqlDataSourceService,
private readonly dataViewerSettingsService: DataViewerSettingsService,
private readonly sqlEditorSettingsService: SqlEditorSettingsService,
) {
this.statisticsMap = new Map();
this.onQueryExecution = new Executor();
@@ -115,118 +118,43 @@ export class SqlQueryService {
async executeEditorQuery(editorState: ISqlEditorTabState, query: string, inNewTab: boolean): Promise<void> {
await this.onQueryExecution.execute(editorState);
const dataSource = this.sqlDataSourceService.get(editorState.editorId);
const contextInfo = dataSource?.executionContext;
const executionContext = contextInfo && this.connectionExecutionContextService.get(contextInfo.id);
if (!contextInfo || !executionContext) {
console.error('executeEditorQuery executionContext is not provided');
return;
}
let source: QueryDataSource;
let model: IDatabaseDataModel<QueryDataSource>;
let source: QueryDataSource | undefined;
let tabGroup: IResultGroup | null = null;
let isNewTabCreated = false;
const connectionKey = createConnectionParam(contextInfo.projectId, contextInfo.connectionId);
const connectionInfo = await this.connectionInfoResource.load(connectionKey);
let tabGroup = this.sqlQueryResultService.getSelectedGroup(editorState);
if (inNewTab || !tabGroup) {
source = new QueryDataSource(this.serviceProvider, this.graphQLService, this.asyncTaskInfoService);
model = this.tableViewerStorageService.add(new DatabaseDataModel(source));
this.dataViewerDataChangeConfirmationService.trackTableDataUpdate(model.id);
tabGroup = this.sqlQueryResultService.createGroup(editorState, model.id, query);
this.switchTabToActiveRequest(editorState, tabGroup, model);
isNewTabCreated = true;
} else {
model = this.tableViewerStorageService.get(tabGroup.modelId)!;
source = model.source as QueryDataSource;
tabGroup.query = query;
}
const editable = this.dataViewerService.isDataEditable(connectionInfo);
const isOutputLogsTabOpened = !!editorState.outputLogsTab;
model
.setAccess(editable ? DatabaseDataAccessMode.Default : DatabaseDataAccessMode.Readonly)
.source.setExecutionContext(executionContext)
.setSupportedDataFormats(connectionInfo.supportedDataFormats)
.setKeepExecutionContextOnDispose(true)
.setOptions({
query: query,
connectionKey,
constraints: [],
whereFilter: '',
readLogs: isOutputLogsTabOpened,
});
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId, true);
try {
await model.setCountGain(this.dataViewerSettingsService.getDefaultRowsCount()).setSlice(0).request();
const dataSource = this.sqlDataSourceService.get(editorState.editorId);
if (!this.sqlEditorSettingsService.scriptExecutionEnabled || !dataSource?.hasFeature(ESqlDataSourceFeatures.executable)) {
throw new Error('Script execution is not allowed');
}
const contextInfo = dataSource?.executionContext;
const executionContext = contextInfo && this.connectionExecutionContextService.get(contextInfo.id);
model.setName(this.sqlQueryResultService.getTabNameForOrder(tabGroup.nameOrder, 0, model.source.getResults().length));
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId);
} catch (exception: any) {
// remove group if execution was cancelled
if (source.currentTask?.cancelled && isNewTabCreated) {
this.sqlQueryResultService.removeGroup(editorState, tabGroup.groupId);
const message = 'Query execution has been canceled';
this.notificationService.logException(exception, 'Query execution Error', message);
if (!contextInfo || !executionContext) {
console.error('executeEditorQuery executionContext is not provided');
return;
}
throw exception;
}
}
async executeQueries(editorState: ISqlEditorTabState, queries: string[], options?: IQueryExecutionOptions): Promise<void> {
await this.onQueryExecution.execute(editorState);
const dataSource = this.sqlDataSourceService.get(editorState.editorId);
const contextInfo = dataSource?.executionContext;
const executionContext = contextInfo && this.connectionExecutionContextService.get(contextInfo.id);
let model: IDatabaseDataModel<QueryDataSource>;
if (!contextInfo || !executionContext) {
console.error('executeEditorQuery executionContext is not provided');
return;
}
const connectionKey = createConnectionParam(contextInfo.projectId, contextInfo.connectionId);
const groupNameOrder = this.sqlQueryResultService.getGroupNameOrder(editorState);
const connectionKey = createConnectionParam(contextInfo.projectId, contextInfo.connectionId);
const connectionInfo = await this.connectionInfoResource.load(connectionKey);
tabGroup = this.sqlQueryResultService.getSelectedGroup(editorState);
const connectionInfo = await this.connectionInfoResource.load(connectionKey);
const statisticsTab = this.sqlQueryResultService.createStatisticsTab(editorState);
this.statisticsMap.set(statisticsTab.tabId, {
queries: queries.length,
executedQueries: 0,
executeTime: 0,
updatedRows: 0,
modelId: null,
});
editorState.currentTabId = statisticsTab.tabId;
const statistics = this.getStatistics(statisticsTab.tabId)!;
let source: QueryDataSource | undefined;
let model: IDatabaseDataModel<QueryDataSource> | undefined;
let resultCount = 0;
for (let i = 0; i < queries.length; i++) {
const query = queries[i]!;
options?.onQueryExecutionStart?.(query, i);
if (!model || !source) {
if (inNewTab || !tabGroup) {
source = new QueryDataSource(this.serviceProvider, this.graphQLService, this.asyncTaskInfoService);
model = this.tableViewerStorageService.add(new DatabaseDataModel(source));
this.dataViewerDataChangeConfirmationService.trackTableDataUpdate(model.id);
tabGroup = this.sqlQueryResultService.createGroup(editorState, model.id, query);
this.switchTabToActiveRequest(editorState, tabGroup, model);
isNewTabCreated = true;
} else {
model = this.tableViewerStorageService.get(tabGroup.modelId)!;
source = model.source as QueryDataSource;
tabGroup.query = query;
}
statistics.modelId = model.id;
const editable = this.dataViewerService.isDataEditable(connectionInfo);
const isOutputLogsTabOpened = !!editorState.outputLogsTab;
@@ -237,58 +165,148 @@ export class SqlQueryService {
.setSupportedDataFormats(connectionInfo.supportedDataFormats)
.setKeepExecutionContextOnDispose(true)
.setOptions({
query,
query: query,
connectionKey,
constraints: [],
whereFilter: '',
readLogs: isOutputLogsTabOpened,
});
try {
await model.setCountGain(this.dataViewerSettingsService.getDefaultRowsCount()).setSlice(0).request();
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId, true);
const cancelled = model.source.cancelled;
await model.setCountGain(this.dataViewerSettingsService.getDefaultRowsCount()).setSlice(0).request();
statistics.executedQueries++;
statistics.executeTime += source.requestInfo.requestDuration;
model.setName(this.sqlQueryResultService.getTabNameForOrder(tabGroup.nameOrder, 0, model.source.getResults().length));
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId);
} catch (exception: any) {
// remove group if execution was cancelled
if (source?.currentTask?.cancelled && isNewTabCreated) {
this.sqlQueryResultService.removeGroup(editorState, tabGroup!.groupId);
const message = 'Query execution has been canceled';
this.notificationService.logException(exception, 'Query execution Error', message);
return;
} else if (!source) {
this.notificationService.logException(exception, 'Query execution Error', exception.message);
}
throw exception;
}
}
for (const result of source.results) {
statistics.updatedRows += result.updateRowCount;
async executeQueries(editorState: ISqlEditorTabState, queries: string[], options?: IQueryExecutionOptions): Promise<void> {
await this.onQueryExecution.execute(editorState);
try {
const dataSource = this.sqlDataSourceService.get(editorState.editorId);
if (!this.sqlEditorSettingsService.scriptExecutionEnabled || !dataSource?.hasFeature(ESqlDataSourceFeatures.executable)) {
throw new Error('Script execution is not allowed');
}
const contextInfo = dataSource?.executionContext;
const executionContext = contextInfo && this.connectionExecutionContextService.get(contextInfo.id);
if (!contextInfo || !executionContext) {
console.error('executeEditorQuery executionContext is not provided');
return;
}
const groupNameOrder = this.sqlQueryResultService.getGroupNameOrder(editorState);
const connectionKey = createConnectionParam(contextInfo.projectId, contextInfo.connectionId);
const connectionInfo = await this.connectionInfoResource.load(connectionKey);
const statisticsTab = this.sqlQueryResultService.createStatisticsTab(editorState);
this.statisticsMap.set(statisticsTab.tabId, {
queries: queries.length,
executedQueries: 0,
executeTime: 0,
updatedRows: 0,
modelId: null,
});
editorState.currentTabId = statisticsTab.tabId;
const statistics = this.getStatistics(statisticsTab.tabId)!;
let source: QueryDataSource | undefined;
let model: IDatabaseDataModel<QueryDataSource> | undefined;
let resultCount = 0;
for (let i = 0; i < queries.length; i++) {
const query = queries[i]!;
options?.onQueryExecutionStart?.(query, i);
if (!model || !source) {
source = new QueryDataSource(this.serviceProvider, this.graphQLService, this.asyncTaskInfoService);
model = this.tableViewerStorageService.add(new DatabaseDataModel(source));
this.dataViewerDataChangeConfirmationService.trackTableDataUpdate(model.id);
}
statistics.modelId = model.id;
if (source.results.some(result => result.data)) {
resultCount = resultCount + 1;
const editable = this.dataViewerService.isDataEditable(connectionInfo);
const isOutputLogsTabOpened = !!editorState.outputLogsTab;
const tabGroup = this.sqlQueryResultService.createGroup(editorState, model.id, query, groupNameOrder);
model.setName(this.sqlQueryResultService.getTabNameForOrder(tabGroup.nameOrder, 0, model.source.getResults().length, resultCount));
this.switchTabToActiveRequest(editorState, tabGroup, model);
model
.setAccess(editable ? DatabaseDataAccessMode.Default : DatabaseDataAccessMode.Readonly)
.source.setExecutionContext(executionContext)
.setSupportedDataFormats(connectionInfo.supportedDataFormats)
.setKeepExecutionContextOnDispose(true)
.setOptions({
query,
connectionKey,
constraints: [],
whereFilter: '',
readLogs: isOutputLogsTabOpened,
});
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId, false, resultCount);
try {
await model.setCountGain(this.dataViewerSettingsService.getDefaultRowsCount()).setSlice(0).request();
model = source = undefined;
}
const cancelled = model.source.cancelled;
options?.onQueryExecuted?.(query, i, true);
statistics.executedQueries++;
statistics.executeTime += source.requestInfo.requestDuration;
if (cancelled) {
for (const result of source.results) {
statistics.updatedRows += result.updateRowCount;
}
if (source.results.some(result => result.data)) {
resultCount = resultCount + 1;
const tabGroup = this.sqlQueryResultService.createGroup(editorState, model.id, query, groupNameOrder);
model.setName(this.sqlQueryResultService.getTabNameForOrder(tabGroup.nameOrder, 0, model.source.getResults().length, resultCount));
this.switchTabToActiveRequest(editorState, tabGroup, model);
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId, false, resultCount);
model = source = undefined;
}
options?.onQueryExecuted?.(query, i, true);
if (cancelled) {
break;
}
} catch (exception: any) {
if (model) {
const tabGroup = this.sqlQueryResultService.createGroup(editorState, model.id, query);
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId, true, resultCount);
model = source = undefined;
}
options?.onQueryExecuted?.(query, i, false);
break;
}
} catch (exception: any) {
if (model) {
const tabGroup = this.sqlQueryResultService.createGroup(editorState, model.id, query);
this.sqlQueryResultService.updateGroupTabs(editorState, model, tabGroup.groupId, true, resultCount);
model = source = undefined;
}
options?.onQueryExecuted?.(query, i, false);
break;
}
}
statistics.modelId = null;
statistics.modelId = null;
if (model) {
this.tableViewerStorageService.remove(model.id);
if (model) {
this.tableViewerStorageService.remove(model.id);
}
} catch (exception: any) {
this.notificationService.logException(exception, 'Query execution Error', exception.message);
throw exception;
}
}
@@ -36,7 +36,7 @@ export class ToolsPanelSettingsService extends Dependency {
}
private registerSettings() {
this.settingsManagerService.registerSettings(this.settings, () => [
this.settingsManagerService.registerSettings<Settings>(() => [
{
key: 'plugin.tools-panel.disabled',
access: {
@@ -25,6 +25,7 @@
"@cloudbeaver/core-events": "workspace:*",
"@cloudbeaver/core-executor": "workspace:*",
"@cloudbeaver/core-localization": "workspace:*",
"@cloudbeaver/core-settings": "workspace:*",
"@cloudbeaver/core-settings-user": "workspace:*",
"@cloudbeaver/core-view": "workspace:*",
"@cloudbeaver/plugin-settings-menu": "workspace:*",
@@ -12,11 +12,13 @@ import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { UserSettingsService } from '@cloudbeaver/core-settings-user';
import { Settings } from '@cloudbeaver/plugin-settings-panel';
import { SettingsResolverService } from '@cloudbeaver/core-settings';
const clientScope = ['client'];
export const UserProfileSettings = observer(function UserProfileSettings() {
const translate = useTranslate();
const settingsResolverService = useService(SettingsResolverService);
const userSettingsService = useService(UserSettingsService);
const notificationService = useService(NotificationService);
@@ -70,7 +72,7 @@ export const UserProfileSettings = observer(function UserProfileSettings() {
</ToolsAction>
</ToolsPanel>
</Group>
<Settings source={userSettingsService} accessor={clientScope} />
<Settings resolver={settingsResolverService} source={userSettingsService} accessor={clientScope} />
</ColoredContainer>
</Form>
);
@@ -28,6 +28,9 @@
{
"path": "../core-localization"
},
{
"path": "../core-settings"
},
{
"path": "../core-settings-user"
},
+1
View File
@@ -4036,6 +4036,7 @@ __metadata:
"@cloudbeaver/core-events": "workspace:*"
"@cloudbeaver/core-executor": "workspace:*"
"@cloudbeaver/core-localization": "workspace:*"
"@cloudbeaver/core-settings": "workspace:*"
"@cloudbeaver/core-settings-user": "workspace:*"
"@cloudbeaver/core-view": "workspace:*"
"@cloudbeaver/plugin-settings-menu": "workspace:*"